summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md5
-rw-r--r--components/chat.js (renamed from demo/components/chat.js)0
-rw-r--r--components/comment.js (renamed from demo/components/comment.js)0
-rw-r--r--components/like-button.js (renamed from demo/components/like-button.js)0
-rw-r--r--components/moderation.js (renamed from demo/components/moderation.js)0
-rw-r--r--components/name.js (renamed from demo/components/name.js)0
-rw-r--r--components/private-messaging.js (renamed from demo/components/private-messaging.js)0
-rw-r--r--graffiti.js351
-rw-r--r--index.html (renamed from demo/index.html)0
-rw-r--r--plugins/vue/plugin.js76
-rw-r--r--src/array.js61
-rw-r--r--src/auth.js125
-rw-r--r--src/logoot.js153
-rw-r--r--style.css (renamed from demo/style.css)0
14 files changed, 0 insertions, 771 deletions
diff --git a/README.md b/README.md
deleted file mode 100644
index 05412b9..0000000
--- a/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Graffiti Javascript Library
-
-This library enables any webpage to interface with the [Graffiti server](https://github.com/graffiti-garden/graffiti-server). It also includes a plugin that extends it to operate with the [Vue.js framework](https://vuejs.org/).
-
-Check out the live [demo](https://graffiti.garden/graffiti-js/demo) of the library and plugin in action. The demo's source code is in the [`/demo`](https://github.com/graffiti-garden/graffiti-js/tree/main/demo) folder.
diff --git a/demo/components/chat.js b/components/chat.js
index 5095dcd..5095dcd 100644
--- a/demo/components/chat.js
+++ b/components/chat.js
diff --git a/demo/components/comment.js b/components/comment.js
index d838ee1..d838ee1 100644
--- a/demo/components/comment.js
+++ b/components/comment.js
diff --git a/demo/components/like-button.js b/components/like-button.js
index 6f65ac6..6f65ac6 100644
--- a/demo/components/like-button.js
+++ b/components/like-button.js
diff --git a/demo/components/moderation.js b/components/moderation.js
index 5e12fb0..5e12fb0 100644
--- a/demo/components/moderation.js
+++ b/components/moderation.js
diff --git a/demo/components/name.js b/components/name.js
index ea44f95..ea44f95 100644
--- a/demo/components/name.js
+++ b/components/name.js
diff --git a/demo/components/private-messaging.js b/components/private-messaging.js
index 61f783b..61f783b 100644
--- a/demo/components/private-messaging.js
+++ b/components/private-messaging.js
diff --git a/graffiti.js b/graffiti.js
deleted file mode 100644
index 65c6ece..0000000
--- a/graffiti.js
+++ /dev/null
@@ -1,351 +0,0 @@
-import Auth from './src/auth.js'
-import GraffitiArray from './src/array.js'
-
-export default class {
-
- // There needs to be a new object map for each tag
- constructor(
- graffitiURL="https://graffiti.garden",
- objectConstructor=()=>({})) {
-
- this.graffitiURL = graffitiURL
- this.open = false
- this.eventTarget = new EventTarget()
- this.tagMap = objectConstructor() // tag->{count, Set(uuid)}
- this.objectMap = objectConstructor() // uuid->object
- this.GraffitiArray = GraffitiArray(this)
-
- this.#initialize()
- }
-
- async #initialize() {
- // Perform authorization
- this.authParams = await Auth.connect(this.graffitiURL)
-
- // Rewrite the URL
- this.wsURL = new URL(this.graffitiURL)
- this.wsURL.host = "app." + this.wsURL.host
- if (this.wsURL.protocol == 'https:') {
- this.wsURL.protocol = 'wss:'
- } else {
- this.wsURL.protocol = 'ws:'
- }
- if (this.authParams.token) {
- this.wsURL.searchParams.set("token", this.authParams.token)
- }
-
- // Commence connection
- this.#connect()
- }
-
- // Wait for the connection to be
- // open (state=true) or closed (state=false)
- async connectionState(state) {
- if (this.open != state) {
- await new Promise(resolve => {
- this.eventTarget.addEventListener(
- state? "open": "closed", ()=> resolve())
- })
- }
- }
-
- #connect() {
- this.ws = new WebSocket(this.wsURL)
- this.ws.onmessage = this.#onMessage.bind(this)
- this.ws.onclose = this.#onClose.bind(this)
- this.ws.onopen = this.#onOpen.bind(this)
- }
-
- // authorization functions
- get myID() { return this.authParams.myID }
- toggleLogIn() {
- this.myID? Auth.logOut() : Auth.logIn(this.graffitiURL)
- }
-
- async #onClose() {
- console.error("lost connection to graffiti server, attemping reconnect soon...")
- this.open = false
- this.eventTarget.dispatchEvent(new Event("closed"))
- await new Promise(resolve => setTimeout(resolve, 2000))
- this.#connect()
- }
-
- async #request(msg) {
- if (!this.open) {
- throw "Can't make request! Not connected to graffiti server"
- }
-
- // Create a random message ID
- const messageID = crypto.randomUUID()
-
- // Create a listener for the reply
- const dataPromise = new Promise(resolve => {
- this.eventTarget.addEventListener('$'+messageID, (e) => {
- resolve(e.data)
- })
- })
-
- // Send the request
- msg.messageID = messageID
- this.ws.send(JSON.stringify(msg))
-
- // Await the reply
- const data = await dataPromise
- delete data.messageID
-
- if ('error' in data) {
- throw data
- } else {
- return data.reply
- }
- }
-
- #onMessage(event) {
- const data = JSON.parse(event.data)
-
- if ('messageID' in data) {
- // It's a reply
- // Forward it back to the sender
- const messageEvent = new Event('$'+data.messageID)
- messageEvent.data = data
- this.eventTarget.dispatchEvent(messageEvent)
-
- } else if ('update' in data) {
- this.#updateCallback(data['update'])
-
- } else if ('remove' in data) {
- this.#removeCallback(data['remove'])
-
- } else if (data.type == 'error') {
- if (data.reason == 'authorization') {
- Auth.logOut()
- }
- throw data
- }
- }
-
- #updateCallback(object) {
- const uuid = this.#objectUUID(object)
-
- // Add the UUID to the tag map
- let subscribed = false
- for (const tag of object._tags) {
- if (!(tag in this.tagMap)) continue
- this.tagMap[tag].uuids.add(uuid)
- subscribed = true
- }
-
- if (!subscribed) return
-
- // Define object specific properties
- if (!('_id' in object)) {
- // Assign the object UUID
- Object.defineProperty(object, '_id', { value: uuid })
-
- // Add proxy functions so object modifications
- // sync with the server
- object = new Proxy(object, this.#objectHandler(object, true))
- }
-
- this.objectMap[uuid] = object
- }
-
- #removeCallback(object) {
- const uuid = this.#objectUUID(object)
-
- // Remove the UUID from all relevant tag maps
- let supported = false
- for (const tag in this.tagMap) {
- if (this.tagMap[tag].uuids.has(uuid)) {
- if (object._tags.includes(tag)) {
- this.tagMap[tag].uuids.delete(uuid)
- } else {
- supported = true
- }
- }
- }
-
- // If all tags have been removed, delete entirely
- if (!supported && uuid in this.objectMap) {
- delete this.objectMap[uuid]
- }
- }
-
- async update(object) {
- object._by = this.myID
- if (!object._key) object._key = crypto.randomUUID()
-
- // Immediately replace the object
- this.#updateCallback(object)
-
- // Send it to the server
- try {
- await this.#request({ update: object })
- } catch(e) {
- // Delete the temp object
- this.#removeCallback(object)
- throw e
- }
- }
-
- #objectHandler(object, root) {
- return {
- get: (target, prop, receiver)=>
- this.#getObjectProperty(object, target, prop, receiver),
- set: (target, prop, val, receiver)=>
- this.#setObjectProperty(object, root, target, prop, val, receiver),
- deleteProperty: (target, prop)=>
- this.#deleteObjectProperty(object, root, target, prop)
- }
- }
-
- #getObjectProperty(object, target, prop, receiver) {
- if (typeof target[prop] === 'object' && target[prop] !== null) {
- return new Proxy(Reflect.get(target, prop, receiver), this.#objectHandler(object, false))
- } else {
- return Reflect.get(target, prop, receiver)
- }
- }
-
- #setObjectProperty(object, root, target, prop, val, receiver) {
- // Store the original, perform the update,
- // sync with server and restore original if error
- const originalObject = Object.assign({}, object)
- if (Reflect.set(target, prop, val, receiver)) {
- this.#removeCallback(originalObject)
- this.#updateCallback(object)
- this.#request({ update: object }).catch(e=> {
- this.#removeCallback(object)
- this.#updateCallback(originalObject)
- throw e
- })
- return true
- } else { return false }
- }
-
- #deleteObjectProperty(object, root, target, prop) {
- const originalObject = Object.assign({}, object)
- if (root && ['_key', '_by', '_tags'].includes(prop)) {
- // This is a deletion of the whole object
- this.#removeCallback(object)
- this.#request({ remove: object._key }).catch(e=> {
- this.#updateCallback(originalObject)
- throw e
- })
- return true
- } else {
- if (Reflect.deleteProperty(target, prop)) {
- this.#request({ update: object }).catch(e=> {
- this.#updateCallback(originalObject)
- throw e
- })
- return true
- } else { return false }
- }
- }
-
- async myTags() {
- return await this.#request({ ls: null })
- }
-
- async objectByKey(userID, objectKey) {
- return await this.#request({ get: {
- _by: userID,
- _key: objectKey
- }})
- }
-
- objects(...tags) {
- tags = tags.filter(tag=> tag!=null)
- for (const tag of tags) {
- if (!(tag in this.tagMap)) {
- throw `You are not subscribed to '${tag}'`
- }
- }
-
- // Merge by UUIDs from all tags and
- // convert to relevant objects
- const uuids = new Set(tags.map(tag=>[...this.tagMap[tag].uuids]).flat())
- const objects = [...uuids].map(uuid=> this.objectMap[uuid])
-
- // Return an array wrapped with graffiti functions
- return new this.GraffitiArray(...objects)
- }
-
- async subscribe(...tags) {
- tags = tags.filter(tag=> tag!=null)
- // Look at what is already subscribed to
- const subscribingTags = []
- for (const tag of tags) {
- if (tag in this.tagMap) {
- // Increase the count
- this.tagMap[tag].count++
- } else {
- // Create a new slot
- this.tagMap[tag] = {
- uuids: new Set(),
- count: 1
- }
- subscribingTags.push(tag)
- }
- }
-
- // Try subscribing in the background
- // but don't raise an error since
- // the subscriptions will happen once connected
- if (subscribingTags.length)
- try {
- await this.#request({ subscribe: subscribingTags })
- } catch {}
- }
-
- async unsubscribe(...tags) {
- tags = tags.filter(tag=> tag!=null)
- // Decrease the count of each tag,
- // removing and marking if necessary
- const unsubscribingTags = []
- for (const tag of tags) {
- this.tagMap[tag].count--
-
- if (!this.tagMap[tag].count) {
- unsubscribingTags.push(tag)
- delete this.tagMap[tag]
- }
- }
-
- // Unsubscribe from all remaining tags
- if (unsubscribingTags.length)
- try {
- await this.#request({ unsubscribe: unsubscribingTags })
- } catch {}
- }
-
- async #onOpen() {
- console.log("connected to the graffiti socket")
- this.open = true
- this.eventTarget.dispatchEvent(new Event("open"))
-
- // Clear data
- for (let tag in this.tagMap) {
- this.tagMap[tag].uuids = new Set()
- }
- for (let uuid in this.objectMap) delete this.objectMap[uuid]
-
- // Resubscribe
- const tags = Object.keys(this.tagMap)
- if (tags.length) await this.#request({ subscribe: tags })
- }
-
- // Utility function to get a universally unique string
- // that represents a particular object
- #objectUUID(object) {
- if (!object._by || !object._key) {
- throw {
- type: 'error',
- content: 'the object you are trying to identify does not have an owner or key',
- object
- }
- }
- return object._by + object._key
- }
-}
diff --git a/demo/index.html b/index.html
index d1b5991..d1b5991 100644
--- a/demo/index.html
+++ b/index.html
diff --git a/plugins/vue/plugin.js b/plugins/vue/plugin.js
deleted file mode 100644
index f04098e..0000000
--- a/plugins/vue/plugin.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import { ref, reactive } from 'vue'
-import Graffiti from '../../graffiti.js'
-
-export default {
- install(app, options) {
-
- const graffitiURL = options && 'url' in options?
- options.url : 'https://graffiti.garden'
-
- // Initialize graffiti with reactive entries
- const graffiti = new Graffiti(graffitiURL, ()=>reactive({}))
-
- // Create a reactive variable that
- // tracks connection state
- const connectionState = ref(false)
- ;(function waitForState(state) {
- graffiti.connectionState(state).then(()=> {
- connectionState.value = state
- waitForState(!state)
- })})(true)
- Object.defineProperty(app.config.globalProperties, "$graffitiConnected", {
- get: ()=> connectionState.value
- })
-
- // Latch on to the graffiti ID
- // when the connection state first becomes true
- let myID = null
- Object.defineProperty(app.config.globalProperties, "$graffitiMyID", {
- get: ()=> {
- if (connectionState.value) myID = graffiti.myID
- return myID
- }
- })
-
- // Add static functions
- for (const key of ['toggleLogIn', 'update', 'myTags', 'objectByKey']) {
- const vueKey = '$graffiti' + key.charAt(0).toUpperCase() + key.slice(1)
- app.config.globalProperties[vueKey] = graffiti[key].bind(graffiti)
- }
-
- // A component for subscribing and
- // unsubscribing to tags that returns
- // a reactive array of the results
- app.component('GraffitiObjects', {
-
- props: ['tags'],
-
- watch: {
- tags: {
- async handler(newTags, oldTags=[]) {
- // Subscribe to the new tags
- await graffiti.subscribe(...newTags)
- // Unsubscribe to the existing tags
- await graffiti.unsubscribe(...oldTags)
- },
- immediate: true,
- deep: true
- }
- },
-
- // Handle unmounting too
- unmount() {
- graffiti.unsubscribe(this.tags)
- },
-
- computed: {
- objects() {
- return graffiti.objects(...this.tags)
- }
- },
-
- template: '<slot :objects="objects"></slot>'
- })
-
- }
-}
diff --git a/src/array.js b/src/array.js
deleted file mode 100644
index ae30012..0000000
--- a/src/array.js
+++ /dev/null
@@ -1,61 +0,0 @@
-// Extend the array class to expose update
-// functionality, plus provide some
-// useful helper methods
-export default function(graffiti) {
-
- return class GraffitiArray extends Array {
-
- get mine() {
- return this.filter(o=> o._by==graffiti.myID)
- }
-
- get notMine() {
- return this.filter(o=> o._by!=graffiti.myID)
- }
-
- get authors() {
- return [...new Set(this.map(o=> o._by))]
- }
-
- removeMine() {
- this.mine.map(o=> delete o._key)
- }
-
- #getProperty(obj, propertyPath) {
- // Split it up by periods
- propertyPath = propertyPath.match(/([^\.]+)/g)
- // Traverse down the path tree
- for (const property of propertyPath) {
- obj = obj[property]
- }
- return obj
- }
-
- sortBy(propertyPath) {
-
- const sortOrder = propertyPath[0] == '-'? -1 : 1
- if (sortOrder < 0) propertyPath = propertyPath.substring(1)
-
- return this.sort((a, b)=> {
- const propertyA = this.#getProperty(a, propertyPath)
- const propertyB = this.#getProperty(b, propertyPath)
- return sortOrder * (
- propertyA < propertyB? -1 :
- propertyA > propertyB? 1 : 0 )
- })
- }
-
- groupBy(propertyPath) {
- return this.reduce((chain, obj)=> {
- const property = this.#getProperty(obj, propertyPath)
- if (property in chain) {
- chain[property].push(obj)
- } else {
- chain[property] = new GraffitiArray(obj)
- }
- return chain
- }, {})
- }
-
- }
-}
diff --git a/src/auth.js b/src/auth.js
deleted file mode 100644
index 8ee803d..0000000
--- a/src/auth.js
+++ /dev/null
@@ -1,125 +0,0 @@
-export default {
-
- async logIn(graffitiURL) {
- // Generate a random client secret and state
- const clientSecret = crypto.randomUUID()
- const state = crypto.randomUUID()
-
- // The client ID is the secret's hex hash
- const clientID = await this.sha256(clientSecret)
-
- // Store the client secret as a local variable
- window.localStorage.setItem('graffitiClientSecret', clientSecret)
- window.localStorage.setItem('graffitiClientID', clientID)
- window.localStorage.setItem('graffitiAuthState', state)
-
- // Redirect to the login window
- const loginURL = this.authURL(graffitiURL)
- loginURL.searchParams.set('client_id', clientID)
- loginURL.searchParams.set('redirect_uri', window.location.href)
- loginURL.searchParams.set('state', state)
- window.location.href = loginURL
- },
-
- async connect(graffitiURL) {
-
- // Check to see if we are already logged in
- let token = window.localStorage.getItem('graffitiToken')
- let myID = window.localStorage.getItem('graffitiID')
-
- if (!token || !myID) {
- // Remove them both in case one exists
- // and the other does not
- token = myID = null
-
- // Check to see if we are redirecting back
- const url = new URL(window.location)
- if (url.searchParams.has('code')) {
-
- // Extract the code and state from the URL and strip it from the history
- const code = url.searchParams.get('code')
- const state = url.searchParams.get('state')
- url.searchParams.delete('code')
- url.searchParams.delete('state')
- window.history.replaceState({}, '', url)
-
- // Get stored variables and remove them
- const clientSecret = window.localStorage.getItem('graffitiClientSecret')
- const clientID = window.localStorage.getItem('graffitiClientID')
- const storedState = window.localStorage.getItem('graffitiAuthState')
- window.localStorage.removeItem('graffitiClientSecret')
- window.localStorage.removeItem('graffitiClientID')
- window.localStorage.removeItem('graffitiAuthState')
-
- // Make sure state has been preserved
- if (state != storedState) {
- throw new Error("The state in local storage does not match the state sent by the server")
- }
-
- // Construct the body of the POST
- let form = new FormData()
- form.append('client_id', clientID)
- form.append('client_secret', clientSecret)
- form.append('code', code)
-
- // Ask to exchange the code for a token
- const tokenURL = this.authURL(graffitiURL)
- tokenURL.pathname = '/token'
- const response = await fetch(tokenURL, {
- method: 'post',
- body: form
- })
-
- // Make sure the response is OK
- if (!response.ok) {
- let reason = response.status + ": "
- try {
- reason += (await response.json()).detail
- } catch (e) {
- reason += response.statusText
- }
-
- throw new Error(`The authorization code could not be exchanged for a token.\n\n${reason}`)
- }
-
- // Parse out the token
- const data = await response.json()
- token = data.access_token
- myID = data.owner_id
-
- // And make sure that the token is valid
- if (!token || !myID) {
- throw new Error(`The authorization token could not be parsed from the response.\n\n${data}`)
- }
-
- // Store the token and ID
- window.localStorage.setItem('graffitiToken', token)
- window.localStorage.setItem('graffitiID', myID)
- }
- }
-
- return { myID, token }
-
- },
-
- logOut() {
- window.localStorage.removeItem('graffitiToken')
- window.localStorage.removeItem('graffitiID')
- window.location.reload()
- },
-
- authURL(graffitiURL) {
- const url = new URL(graffitiURL)
- url.host = "auth." + url.host
- return url
- },
-
- async sha256(input) {
- const encoder = new TextEncoder()
- const inputBytes = encoder.encode(input)
- const outputBuffer = await crypto.subtle.digest('SHA-256', inputBytes)
- const outputArray = Array.from(new Uint8Array(outputBuffer))
- return outputArray.map(b => b.toString(16).padStart(2, '0')).join('')
- }
-
-}
diff --git a/src/logoot.js b/src/logoot.js
deleted file mode 100644
index c8c7c02..0000000
--- a/src/logoot.js
+++ /dev/null
@@ -1,153 +0,0 @@
-export default {
-
- query(property) {
- return {
- [property]: {
- $type: 'array',
- $type: ['int', 'long'],
- },
- $nor: [
- { [property]: { $gt: this.maxInt } },
- { [property]: { $lt: 0 } },
- ]
- }
- },
-
- get before() {
- return []
- },
-
- get after() {
- return [this.maxInt+1]
- },
-
- between(a, b, scale=100) {
- // Strip zeros and find common length
- const aLength = this.lengthWithoutZeros(a)
- const bLength = this.lengthWithoutZeros(b)
- const minLength = Math.min(aLength, bLength)
-
- // Initialize output
- const out = []
-
- // Find the break point where a[i] != b[i]
- let i = 0
- while (i < minLength && a[i] == b[i]) {
- out.push(a[i])
- i++
- }
-
- // Initialize upper and lower bounds for
- // sampling the last digit
- let lowerBound = 1
- let upperBound = this.maxInt
-
- if (i < minLength) {
- // If the break happened before we hit
- // the end of one of the arrays
-
- if (Math.abs(a[i] - b[i]) > 1) {
- // If a[i] and b[i] are more than one
- // away from each other, just sample
- // between them
- lowerBound = Math.min(a[i], b[i]) + 1
- upperBound = Math.max(a[i], b[i]) - 1
- } else {
- // If they are one away no integers
- // will fit in between, so add new layer
- const lesser = (a[i] < b[i])? a : b
- out.push(lesser[i])
- i++
-
- while (i < lesser.length && lesser[i] >= this.maxInt) {
- // If the lesser is at it's limit,
- // we will need to add even more layers
- out.push(lesser[i])
- i++
- }
-
- if (i < lesser.length) {
- // Sample something greater than
- // the lesser digit
- lowerBound = lesser[i] + 1
- }
- }
- } else {
- // The break happened because we hit
- // the end of one of the arrays.
-
- if (aLength == bLength) {
- // If they are entirely equal,
- // there is nothing in between
- // just return what we have
- return out
- }
-
- const longerLength = Math.max(aLength, bLength)
- const longer = (a.length == longerLength)? a : b
- while (i < longerLength && longer[i] == 0) {
- // Skip past the zeros because we can't sample
- // for digits less than zero
- out.push(0)
- i++
- }
-
- if (i < longerLength) {
- if (longer[i] == 1) {
- // If longer is at it's limit,
- // we still need to add another layer
- out.push(0)
- } else {
- upperBound = longer[i] - 1
- }
- }
- }
-
- // Create a random number in [0,1] but bias it to be small,
- // so that numbers tend to increase by a small amount.
- let random = Math.random()
- random = -Math.log(1-random)/scale
- random = Math.min(random, 1)
-
- // Finally, sample between the upper and lower bounds
- out.push(Math.floor(random * (upperBound + 1 - lowerBound)) + lowerBound)
- return out
- },
-
- compare(a, b) {
- // Strip zeros and find common length
- const aLength = this.lengthWithoutZeros(a)
- const bLength = this.lengthWithoutZeros(b)
- const minLength = Math.min(aLength, bLength)
-
- // See if there are any differences
- for (let i = 0; i < minLength; i++) {
- if (a[i] > b[i]) {
- return 1
- } else if (a[i] < b[i]) {
- return -1
- }
- }
-
- // If they are all the same up til now,
- // the longer one is bigger
- if (aLength > bLength) {
- return 1
- } else if (aLength < bLength) {
- return -1
- } else {
- return 0
- }
- },
-
-
- lengthWithoutZeros(a) {
- let length = a.length
- while (length > 0 && a[length - 1] == 0) {
- length--
- }
- return length
- },
-
- maxInt: 9007199254740991,
-}
diff --git a/demo/style.css b/style.css
index 7bdf592..7bdf592 100644
--- a/demo/style.css
+++ b/style.css