From 8453493fc930c1d3579bef3e8b41639380f018fb Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: moved out of vue plugin --- README.md | 54 +++++++++++++++ src/auth.js | 113 ++++++++++++++++++++++++++++++ src/socket.js | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/utils.js | 11 +++ 4 files changed, 395 insertions(+) create mode 100644 README.md create mode 100644 src/auth.js create mode 100644 src/socket.js create mode 100644 src/utils.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a61e7a --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# The Basic Graffiti Javascript Library + +This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/csail-graffiti/server). +We recommend not using the vanilla library itself but instead using framework plugins built on top of this library like the [Graffiti plugin for Vue.JS](https://github.com/csail-graffiti/graffiti-js-vue). + +With this library: + +```javascript +import GraffitiSocket from "https://csail-graffiti.github.io/graffiti-js-vanilla/socket.js" + +// You can initialize a connection to the graffiti server +const gs = GraffitiSocket() +await gs.initialize() + +// You can subscribe to queries +const queryID = await gs.subscribe({ + type: 'post', + content: { $type: 'string' } + } + // With an arbitrary update callback + (obj) => console.log(`An object has been created: {obj}`) + // and delete callback + (obj) => console.log(`An object with id {obj._id} by user {obj._by} has been deleted`. +) + +// And then unsubscribe to those queries +await gs.unsubscribe(queryID) + +// You can log in and out and check your logged-in status +gs.logIn() +gs.logOut() +if (gs.loggedIn) { + ... +} + +// When you are logged in you reference your user ID +console.log(gs.myID) + +// And when you are logged in you can +// create objects, +const myCoolPost = { + type: 'post', + content: 'hello world' +} +gs.complete(myCoolPost) +await gs.update(myCoolPost) + +// replace objects, +myCoolPost.content += '!!!' +await gs.update(myCoolPost) + +// and delete objects. +await gs.delete(myCoolPost) +``` diff --git a/src/auth.js b/src/auth.js new file mode 100644 index 0000000..697c61e --- /dev/null +++ b/src/auth.js @@ -0,0 +1,113 @@ +import { randomString, sha256 } from './utils.js' + +export default { + + async logIn(origin) { + // Generate a random client secret and state + const clientSecret = randomString() + const state = randomString() + + // The client ID is the secret's hex hash + const clientID = await 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 authURL = new URL(origin) + authURL.searchParams.set('client_id', clientID) + authURL.searchParams.set('redirect_uri', window.location.href) + authURL.searchParams.set('state', state) + window.location.href = authURL + }, + + async connect(origin) { + origin = new URL(origin) + origin.host = "auth." + origin.host + + // Check to see if we are already logged in + let token = window.localStorage.getItem('graffitiToken') + let myID = window.localStorage.getItem('graffitiID') + + if (!token || !myID) { + + // 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 = new URL('token', origin) + 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) { + 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) + } + } + + const loggedIn = (token != null) && (myID != null), + + return { loggedIn, myID, token } + + }, + + logOut() { + window.localStorage.removeItem('graffitiToken') + window.localStorage.removeItem('graffitiID') + window.location.reload() + }, + +} diff --git a/src/socket.js b/src/socket.js new file mode 100644 index 0000000..d324b78 --- /dev/null +++ b/src/socket.js @@ -0,0 +1,217 @@ +import Auth from './auth.js' +import { randomString } from './utils.js' + +export default class { + + constructor(origin="https://graffiti.csail.mit.edu") { + this.origin = origin + this.open = false + this.subscriptionData = {} + this.eventTarget = new EventTarget() + } + + // CALL THIS BEFORE DOING ANYTHING ELSE + async initialize() { + // Perform authorization + this.authParams = await Auth.connect(this.origin) + + // Rewrite the URL + this.wsURL = new URL(origin) + 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) + } + + // And commence connection + this.connect() + } + + 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 + logIn() { Auth.logIn(this.origin) } + logOut() { Auth.logOut() } + get myID() { return this.authParams.myID } + get loggedIn() { return this.authParams.loggedIn } + + async onClose() { + this.open = false + console.error("lost connection to graffiti server, attemping reconnect soon...") + await new Promise(resolve => setTimeout(resolve, 2000)) + this.connect() + } + + async request(msg) { + // Create a random message ID + const messageID = randomString() + + // Create a listener for the reply + const dataPromise = new Promise(resolve => { + this.eventTarget.addEventListener(messageID, (e) => { + resolve(e.data) + }) + }) + + // Wait for the socket to open + if (!this.open) { + await new Promise(resolve => { + this.eventTarget.addEventListener("graffitiOpen", () => resolve() ) + }) + } + + // Send the request + msg.messageID = messageID + this.ws.send(JSON.stringify(msg)) + + // Await the reply + const data = await dataPromise + delete data.messageID + + if (data.type == 'error' ) { + throw data + } else { + return data + } + } + + 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 (['updates', 'deletes'].includes(data.type)) { + // Subscription data + if (data.queryID in this.subscriptionData) { + const sd = this.subscriptionData[data.queryID] + + // For each data point, either add or remove it + for (const r of data.results) { + if (data.type == 'updates') { + sd.updateCallback(r) + } else { + sd.deleteCallback(r) + } + } + + // And update this query's notion of "now" + if (data.complete) { + if (data.historical) { + sd.historyComplete = true + } + if (sd.historyComplete) { + sd.since = data.now + } + } + } + } + } + + async update(object) { + const data = await this.request({ + type: "update", + object + }) + return data.objectID + } + + async delete(objectID) { + await this.request({ + type: "delete", + objectID + }) + } + + async subscribe( + query, + updateCallback, + deleteCallback, + since=null, + queryID=null) { + + // Create a random query ID + if (!queryID) queryID = randomString() + + // Send the request + await this.request({ + type: "subscribe", + queryID, query, since + }) + + // Store the subscription in case of disconnections + this.subscriptionData[queryID] = { + query, since, updateCallback, deleteCallback, + historyComplete: false + } + + return queryID + } + + async unsubscribe(queryID) { + // Remove allocated space + delete this.subscriptionData[queryID] + + // And unsubscribe + const data = await this.request({ + type: "unsubscribe", + queryID + }) + } + + async onOpen() { + console.log("connected to the graffiti socket") + this.open = true + this.eventTarget.dispatchEvent(new Event("graffitiOpen")) + // Resubscribe to hanging queries + for (const queryID in this.subscriptionData) { + const sd = this.subscriptionData[queryID] + await this.subscribe( + sd.query, + sd.updateCallback, + sd.deleteCallback, + sd.since, + queryID) + } + } + + // Adds required fields to an object. + // You should probably call this before 'update' + completeObject(object) { + // Add by/to fields + object._by = this.myID + if ('_to' in object && !Array.isArray(object._to)) { + throw new Error("_to must be an array") + } + + // Pre-generate the object's ID if it does not already exist + if (!object._id) object._id = randomString() + } + + // Utility function to get a universally unique string + // that represents a particular object + objectUUID(object) { + if (!object._id || !object._by) { + throw { + type: 'error', + content: 'the object you are trying to identify does not have an ID or owner', + object + } + } + return object._id + object._by + } + +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..ed21d46 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,11 @@ +export function randomString() { + return Math.random().toString(36).substr(2) +} + +export async function 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('') +} -- cgit v1.2.3-70-g09d2 From 25f68fec75d103044d7d72003c3fc56ddaa9ca9c Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: typos in readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7a61e7a..4ac0a73 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/csail-graffiti/server). We recommend not using the vanilla library itself but instead using framework plugins built on top of this library like the [Graffiti plugin for Vue.JS](https://github.com/csail-graffiti/graffiti-js-vue). -With this library: +Example usage: ```javascript import GraffitiSocket from "https://csail-graffiti.github.io/graffiti-js-vanilla/socket.js" @@ -18,9 +18,9 @@ const queryID = await gs.subscribe({ content: { $type: 'string' } } // With an arbitrary update callback - (obj) => console.log(`An object has been created: {obj}`) + (obj) => console.log(`An object has been created: {obj}`), // and delete callback - (obj) => console.log(`An object with id {obj._id} by user {obj._by} has been deleted`. + (obj) => console.log(`An object with id {obj._id} by user {obj._by} has been deleted`.) ) // And then unsubscribe to those queries @@ -30,10 +30,10 @@ await gs.unsubscribe(queryID) gs.logIn() gs.logOut() if (gs.loggedIn) { - ... + // ... } -// When you are logged in you reference your user ID +// When you are logged in you can reference your user ID console.log(gs.myID) // And when you are logged in you can -- cgit v1.2.3-70-g09d2 From dd352a3a167b272ab0c2c81c9942b8a9a636944d Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: slight doc updates --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ac0a73..c9ab192 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# The Basic Graffiti Javascript Library +# Graffiti for Vanilla Javascript This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/csail-graffiti/server). -We recommend not using the vanilla library itself but instead using framework plugins built on top of this library like the [Graffiti plugin for Vue.JS](https://github.com/csail-graffiti/graffiti-js-vue). +We recommend not using this vanilla library itself but instead using framework plugins that are built on top of it like the [Graffiti plugin for Vue.JS](https://github.com/csail-graffiti/graffiti-js-vue). Example usage: @@ -42,6 +42,9 @@ const myCoolPost = { type: 'post', content: 'hello world' } +// ("completing" an object annotates +// it with your user ID and a random +// object ID, required by the server) gs.complete(myCoolPost) await gs.update(myCoolPost) -- cgit v1.2.3-70-g09d2 From 7c33c03c2613651b775218b363f38c3493006789 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: differences to url naming --- src/auth.js | 27 ++++++++++++++++----------- src/socket.js | 10 +++++----- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/auth.js b/src/auth.js index 697c61e..76cb554 100644 --- a/src/auth.js +++ b/src/auth.js @@ -2,7 +2,7 @@ import { randomString, sha256 } from './utils.js' export default { - async logIn(origin) { + async logIn(graffitiURL) { // Generate a random client secret and state const clientSecret = randomString() const state = randomString() @@ -16,16 +16,14 @@ export default { window.localStorage.setItem('graffitiAuthState', state) // Redirect to the login window - const authURL = new URL(origin) - authURL.searchParams.set('client_id', clientID) - authURL.searchParams.set('redirect_uri', window.location.href) - authURL.searchParams.set('state', state) - window.location.href = authURL + 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(origin) { - origin = new URL(origin) - origin.host = "auth." + origin.host + async connect(graffitiURL) { // Check to see if we are already logged in let token = window.localStorage.getItem('graffitiToken') @@ -64,7 +62,8 @@ export default { form.append('code', code) // Ask to exchange the code for a token - const tokenURL = new URL('token', origin) + const tokenURL = this.authURL(graffitiURL) + tokenURL.pathname = '/token' const response = await fetch(tokenURL, { method: 'post', body: form @@ -98,7 +97,7 @@ export default { } } - const loggedIn = (token != null) && (myID != null), + const loggedIn = (token != null) && (myID != null) return { loggedIn, myID, token } @@ -110,4 +109,10 @@ export default { window.location.reload() }, + authURL(graffitiURL) { + const url = new URL(graffitiURL) + url.host = "auth." + url.host + return url + }, + } diff --git a/src/socket.js b/src/socket.js index d324b78..1f762db 100644 --- a/src/socket.js +++ b/src/socket.js @@ -3,8 +3,8 @@ import { randomString } from './utils.js' export default class { - constructor(origin="https://graffiti.csail.mit.edu") { - this.origin = origin + constructor(graffitiURL="https://graffiti.csail.mit.edu") { + this.graffitiURL = graffitiURL this.open = false this.subscriptionData = {} this.eventTarget = new EventTarget() @@ -13,10 +13,10 @@ export default class { // CALL THIS BEFORE DOING ANYTHING ELSE async initialize() { // Perform authorization - this.authParams = await Auth.connect(this.origin) + this.authParams = await Auth.connect(this.graffitiURL) // Rewrite the URL - this.wsURL = new URL(origin) + this.wsURL = new URL(this.graffitiURL) this.wsURL.host = "app." + this.wsURL.host if (this.wsURL.protocol == 'https:') { this.wsURL.protocol = 'wss:' @@ -39,7 +39,7 @@ export default class { } // authorization functions - logIn() { Auth.logIn(this.origin) } + logIn() { Auth.logIn(this.graffitiURL) } logOut() { Auth.logOut() } get myID() { return this.authParams.myID } get loggedIn() { return this.authParams.loggedIn } -- cgit v1.2.3-70-g09d2 From a7491acf18c560646872efae2fac18c647e15cdd Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: delete->remove for js compatability --- README.md | 8 ++++---- src/socket.js | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c9ab192..03b98e6 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ const queryID = await gs.subscribe({ } // With an arbitrary update callback (obj) => console.log(`An object has been created: {obj}`), - // and delete callback - (obj) => console.log(`An object with id {obj._id} by user {obj._by} has been deleted`.) + // and remove callback + (obj) => console.log(`An object with id {obj._id} by user {obj._by} has been removed.`) ) // And then unsubscribe to those queries @@ -52,6 +52,6 @@ await gs.update(myCoolPost) myCoolPost.content += '!!!' await gs.update(myCoolPost) -// and delete objects. -await gs.delete(myCoolPost) +// and remove objects. +await gs.remove(myCoolPost) ``` diff --git a/src/socket.js b/src/socket.js index 1f762db..5c7760c 100644 --- a/src/socket.js +++ b/src/socket.js @@ -104,7 +104,7 @@ export default class { if (data.type == 'updates') { sd.updateCallback(r) } else { - sd.deleteCallback(r) + sd.removeCallback(r) } } @@ -129,7 +129,7 @@ export default class { return data.objectID } - async delete(objectID) { + async remove(objectID) { await this.request({ type: "delete", objectID @@ -139,7 +139,7 @@ export default class { async subscribe( query, updateCallback, - deleteCallback, + removeCallback, since=null, queryID=null) { @@ -154,7 +154,7 @@ export default class { // Store the subscription in case of disconnections this.subscriptionData[queryID] = { - query, since, updateCallback, deleteCallback, + query, since, updateCallback, removeCallback, historyComplete: false } @@ -182,7 +182,7 @@ export default class { await this.subscribe( sd.query, sd.updateCallback, - sd.deleteCallback, + sd.removeCallback, sd.since, queryID) } -- cgit v1.2.3-70-g09d2 From bdf04ed4382f153ebf7b80732e7d9c6156ba58a2 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: added note about update query --- README.md | 20 ++++++++++++++++++-- src/socket.js | 4 ++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 03b98e6..c1c8d1b 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,28 @@ const myCoolPost = { // it with your user ID and a random // object ID, required by the server) gs.complete(myCoolPost) -await gs.update(myCoolPost) +await gs.update(myCoolPost, {}) // replace objects, myCoolPost.content += '!!!' -await gs.update(myCoolPost) +await gs.update(myCoolPost, {}) // and remove objects. await gs.remove(myCoolPost) + +// The second argument in the update +// function is a query. If the object you +// try to add does not match the query +// it will be rejected. This prevents +// you from accidentally creating data +// that gets "lost". +const query = { type: 'post' } +const myPost = { type: 'post' } +const myNotPost = { type: 'notpost' } +gs.complete(myNotPost) +// This works +await gs.update(myPost, query) +// But this won't work! +await gs.update(myNotPost, query) + ``` diff --git a/src/socket.js b/src/socket.js index 5c7760c..be17304 100644 --- a/src/socket.js +++ b/src/socket.js @@ -121,10 +121,10 @@ export default class { } } - async update(object) { + async update(object, query) { const data = await this.request({ type: "update", - object + object, query }) return data.objectID } -- cgit v1.2.3-70-g09d2 From d041233b98236ec26fc7150beb23a75cf1d1e54e Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: delete->remove serverside --- src/socket.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/socket.js b/src/socket.js index be17304..59834d8 100644 --- a/src/socket.js +++ b/src/socket.js @@ -94,7 +94,7 @@ export default class { messageEvent.data = data this.eventTarget.dispatchEvent(messageEvent) - } else if (['updates', 'deletes'].includes(data.type)) { + } else if (['updates', 'removes'].includes(data.type)) { // Subscription data if (data.queryID in this.subscriptionData) { const sd = this.subscriptionData[data.queryID] @@ -131,7 +131,7 @@ export default class { async remove(objectID) { await this.request({ - type: "delete", + type: "remove", objectID }) } -- cgit v1.2.3-70-g09d2 From 8ca207d31ef3eb9c3102f7fdb23f85b53d16924f Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: if authorization error, log out --- src/socket.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/socket.js b/src/socket.js index 59834d8..06cd7c6 100644 --- a/src/socket.js +++ b/src/socket.js @@ -77,7 +77,7 @@ export default class { const data = await dataPromise delete data.messageID - if (data.type == 'error' ) { + if (data.type == 'error') { throw data } else { return data @@ -118,6 +118,11 @@ export default class { } } } + } else if (data.type == 'error') { + if (data.reason == 'authorization') { + this.logOut() + } + throw data } } -- cgit v1.2.3-70-g09d2 From 33a84bb2acf9626fa86c2c165b91b66a718e1526 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: log in is now just a toggle function, no loggedIn bool --- auth.js | 119 +++++++++++++++++++++++++++++++ graffiti.js | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/auth.js | 118 ------------------------------- src/socket.js | 222 ---------------------------------------------------------- src/utils.js | 11 --- utils.js | 11 +++ 6 files changed, 352 insertions(+), 351 deletions(-) create mode 100644 auth.js create mode 100644 graffiti.js delete mode 100644 src/auth.js delete mode 100644 src/socket.js delete mode 100644 src/utils.js create mode 100644 utils.js diff --git a/auth.js b/auth.js new file mode 100644 index 0000000..108b42a --- /dev/null +++ b/auth.js @@ -0,0 +1,119 @@ +import { randomString, sha256 } from './utils.js' + +export default { + + async logIn(graffitiURL) { + // Generate a random client secret and state + const clientSecret = randomString() + const state = randomString() + + // The client ID is the secret's hex hash + const clientID = await 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 + }, + +} diff --git a/graffiti.js b/graffiti.js new file mode 100644 index 0000000..7a9c168 --- /dev/null +++ b/graffiti.js @@ -0,0 +1,222 @@ +import Auth from './auth.js' +import { randomString } from './utils.js' + +export default class { + + constructor(graffitiURL="https://graffiti.csail.mit.edu") { + this.graffitiURL = graffitiURL + this.open = false + this.subscriptionData = {} + this.eventTarget = new EventTarget() + } + + // CALL THIS BEFORE DOING ANYTHING ELSE + 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) + } + + // And commence connection + this.connect() + } + + 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() { + this.open = false + console.error("lost connection to graffiti server, attemping reconnect soon...") + await new Promise(resolve => setTimeout(resolve, 2000)) + this.connect() + } + + async request(msg) { + // Create a random message ID + const messageID = randomString() + + // Create a listener for the reply + const dataPromise = new Promise(resolve => { + this.eventTarget.addEventListener(messageID, (e) => { + resolve(e.data) + }) + }) + + // Wait for the socket to open + if (!this.open) { + await new Promise(resolve => { + this.eventTarget.addEventListener("graffitiOpen", () => resolve() ) + }) + } + + // Send the request + msg.messageID = messageID + this.ws.send(JSON.stringify(msg)) + + // Await the reply + const data = await dataPromise + delete data.messageID + + if (data.type == 'error') { + throw data + } else { + return data + } + } + + 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 (['updates', 'removes'].includes(data.type)) { + // Subscription data + if (data.queryID in this.subscriptionData) { + const sd = this.subscriptionData[data.queryID] + + // For each data point, either add or remove it + for (const r of data.results) { + if (data.type == 'updates') { + sd.updateCallback(r) + } else { + sd.removeCallback(r) + } + } + + // And update this query's notion of "now" + if (data.complete) { + if (data.historical) { + sd.historyComplete = true + } + if (sd.historyComplete) { + sd.since = data.now + } + } + } + } else if (data.type == 'error') { + if (data.reason == 'authorization') { + this.logOut() + } + throw data + } + } + + async update(object, query) { + const data = await this.request({ + type: "update", + object, query + }) + return data.objectID + } + + async remove(objectID) { + await this.request({ + type: "remove", + objectID + }) + } + + async subscribe( + query, + updateCallback, + removeCallback, + since=null, + queryID=null) { + + // Create a random query ID + if (!queryID) queryID = randomString() + + // Send the request + await this.request({ + type: "subscribe", + queryID, query, since + }) + + // Store the subscription in case of disconnections + this.subscriptionData[queryID] = { + query, since, updateCallback, removeCallback, + historyComplete: false + } + + return queryID + } + + async unsubscribe(queryID) { + // Remove allocated space + delete this.subscriptionData[queryID] + + // And unsubscribe + const data = await this.request({ + type: "unsubscribe", + queryID + }) + } + + async onOpen() { + console.log("connected to the graffiti socket") + this.open = true + this.eventTarget.dispatchEvent(new Event("graffitiOpen")) + // Resubscribe to hanging queries + for (const queryID in this.subscriptionData) { + const sd = this.subscriptionData[queryID] + await this.subscribe( + sd.query, + sd.updateCallback, + sd.removeCallback, + sd.since, + queryID) + } + } + + // Adds required fields to an object. + // You should probably call this before 'update' + completeObject(object) { + // Add by/to fields + object._by = this.myID + if ('_to' in object && !Array.isArray(object._to)) { + throw new Error("_to must be an array") + } + + // Pre-generate the object's ID if it does not already exist + if (!object._id) object._id = randomString() + } + + // Utility function to get a universally unique string + // that represents a particular object + objectUUID(object) { + if (!object._id || !object._by) { + throw { + type: 'error', + content: 'the object you are trying to identify does not have an ID or owner', + object + } + } + return object._id + object._by + } + +} diff --git a/src/auth.js b/src/auth.js deleted file mode 100644 index 76cb554..0000000 --- a/src/auth.js +++ /dev/null @@ -1,118 +0,0 @@ -import { randomString, sha256 } from './utils.js' - -export default { - - async logIn(graffitiURL) { - // Generate a random client secret and state - const clientSecret = randomString() - const state = randomString() - - // The client ID is the secret's hex hash - const clientID = await 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) { - - // 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) { - 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) - } - } - - const loggedIn = (token != null) && (myID != null) - - return { loggedIn, 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 - }, - -} diff --git a/src/socket.js b/src/socket.js deleted file mode 100644 index 06cd7c6..0000000 --- a/src/socket.js +++ /dev/null @@ -1,222 +0,0 @@ -import Auth from './auth.js' -import { randomString } from './utils.js' - -export default class { - - constructor(graffitiURL="https://graffiti.csail.mit.edu") { - this.graffitiURL = graffitiURL - this.open = false - this.subscriptionData = {} - this.eventTarget = new EventTarget() - } - - // CALL THIS BEFORE DOING ANYTHING ELSE - 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) - } - - // And commence connection - this.connect() - } - - 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 - logIn() { Auth.logIn(this.graffitiURL) } - logOut() { Auth.logOut() } - get myID() { return this.authParams.myID } - get loggedIn() { return this.authParams.loggedIn } - - async onClose() { - this.open = false - console.error("lost connection to graffiti server, attemping reconnect soon...") - await new Promise(resolve => setTimeout(resolve, 2000)) - this.connect() - } - - async request(msg) { - // Create a random message ID - const messageID = randomString() - - // Create a listener for the reply - const dataPromise = new Promise(resolve => { - this.eventTarget.addEventListener(messageID, (e) => { - resolve(e.data) - }) - }) - - // Wait for the socket to open - if (!this.open) { - await new Promise(resolve => { - this.eventTarget.addEventListener("graffitiOpen", () => resolve() ) - }) - } - - // Send the request - msg.messageID = messageID - this.ws.send(JSON.stringify(msg)) - - // Await the reply - const data = await dataPromise - delete data.messageID - - if (data.type == 'error') { - throw data - } else { - return data - } - } - - 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 (['updates', 'removes'].includes(data.type)) { - // Subscription data - if (data.queryID in this.subscriptionData) { - const sd = this.subscriptionData[data.queryID] - - // For each data point, either add or remove it - for (const r of data.results) { - if (data.type == 'updates') { - sd.updateCallback(r) - } else { - sd.removeCallback(r) - } - } - - // And update this query's notion of "now" - if (data.complete) { - if (data.historical) { - sd.historyComplete = true - } - if (sd.historyComplete) { - sd.since = data.now - } - } - } - } else if (data.type == 'error') { - if (data.reason == 'authorization') { - this.logOut() - } - throw data - } - } - - async update(object, query) { - const data = await this.request({ - type: "update", - object, query - }) - return data.objectID - } - - async remove(objectID) { - await this.request({ - type: "remove", - objectID - }) - } - - async subscribe( - query, - updateCallback, - removeCallback, - since=null, - queryID=null) { - - // Create a random query ID - if (!queryID) queryID = randomString() - - // Send the request - await this.request({ - type: "subscribe", - queryID, query, since - }) - - // Store the subscription in case of disconnections - this.subscriptionData[queryID] = { - query, since, updateCallback, removeCallback, - historyComplete: false - } - - return queryID - } - - async unsubscribe(queryID) { - // Remove allocated space - delete this.subscriptionData[queryID] - - // And unsubscribe - const data = await this.request({ - type: "unsubscribe", - queryID - }) - } - - async onOpen() { - console.log("connected to the graffiti socket") - this.open = true - this.eventTarget.dispatchEvent(new Event("graffitiOpen")) - // Resubscribe to hanging queries - for (const queryID in this.subscriptionData) { - const sd = this.subscriptionData[queryID] - await this.subscribe( - sd.query, - sd.updateCallback, - sd.removeCallback, - sd.since, - queryID) - } - } - - // Adds required fields to an object. - // You should probably call this before 'update' - completeObject(object) { - // Add by/to fields - object._by = this.myID - if ('_to' in object && !Array.isArray(object._to)) { - throw new Error("_to must be an array") - } - - // Pre-generate the object's ID if it does not already exist - if (!object._id) object._id = randomString() - } - - // Utility function to get a universally unique string - // that represents a particular object - objectUUID(object) { - if (!object._id || !object._by) { - throw { - type: 'error', - content: 'the object you are trying to identify does not have an ID or owner', - object - } - } - return object._id + object._by - } - -} diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index ed21d46..0000000 --- a/src/utils.js +++ /dev/null @@ -1,11 +0,0 @@ -export function randomString() { - return Math.random().toString(36).substr(2) -} - -export async function 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/utils.js b/utils.js new file mode 100644 index 0000000..ed21d46 --- /dev/null +++ b/utils.js @@ -0,0 +1,11 @@ +export function randomString() { + return Math.random().toString(36).substr(2) +} + +export async function 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('') +} -- cgit v1.2.3-70-g09d2 From 8ace93096ad0116d3d30790cd6ed3a09266b50eb Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: add context if not declared --- graffiti.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/graffiti.js b/graffiti.js index 7a9c168..1741452 100644 --- a/graffiti.js +++ b/graffiti.js @@ -202,6 +202,11 @@ export default class { throw new Error("_to must be an array") } + // Add an open context if none is declared + if (!object._inContextIf) { + object._inContextIf = [{}] + } + // Pre-generate the object's ID if it does not already exist if (!object._id) object._id = randomString() } -- cgit v1.2.3-70-g09d2 From 8414fee43a83f46846ad075acb8c620369eaa0f7 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: added arbitrary flags, like audit --- graffiti.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/graffiti.js b/graffiti.js index 1741452..0180a87 100644 --- a/graffiti.js +++ b/graffiti.js @@ -145,6 +145,7 @@ export default class { query, updateCallback, removeCallback, + flags={}, since=null, queryID=null) { @@ -154,12 +155,12 @@ export default class { // Send the request await this.request({ type: "subscribe", - queryID, query, since + queryID, query, since, ...flags }) // Store the subscription in case of disconnections this.subscriptionData[queryID] = { - query, since, updateCallback, removeCallback, + query, since, flags, updateCallback, removeCallback, historyComplete: false } @@ -188,6 +189,7 @@ export default class { sd.query, sd.updateCallback, sd.removeCallback, + sd.flags, sd.since, queryID) } -- cgit v1.2.3-70-g09d2 From 5ae06038ffe150638ac472e4c643fdee6dd202d6 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: Update README.md --- README.md | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c1c8d1b..1d7357d 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ # Graffiti for Vanilla Javascript This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/csail-graffiti/server). -We recommend not using this vanilla library itself but instead using framework plugins that are built on top of it like the [Graffiti plugin for Vue.JS](https://github.com/csail-graffiti/graffiti-js-vue). +We recommend not using this vanilla library itself but instead using framework plugins that are built on top of it like the [Graffiti plugin for Vue.JS](https://github.com/csail-graffiti/graffiti-x-vue). Example usage: ```javascript -import GraffitiSocket from "https://csail-graffiti.github.io/graffiti-js-vanilla/socket.js" +import Graffiti from "https://csail-graffiti.github.io/graffiti-x-js/graffiti.js" // You can initialize a connection to the graffiti server -const gs = GraffitiSocket() -await gs.initialize() +const graffiti = Graffiti() +await graffiti.initialize() // You can subscribe to queries -const queryID = await gs.subscribe({ +const queryID = await graffiti.subscribe({ type: 'post', content: { $type: 'string' } } @@ -24,17 +24,17 @@ const queryID = await gs.subscribe({ ) // And then unsubscribe to those queries -await gs.unsubscribe(queryID) +await graffiti.unsubscribe(queryID) // You can log in and out and check your logged-in status -gs.logIn() -gs.logOut() -if (gs.loggedIn) { +graffiti.logIn() +graffiti.logOut() +if (graffiti.loggedIn) { // ... } // When you are logged in you can reference your user ID -console.log(gs.myID) +console.log(graffiti.myID) // And when you are logged in you can // create objects, @@ -45,15 +45,15 @@ const myCoolPost = { // ("completing" an object annotates // it with your user ID and a random // object ID, required by the server) -gs.complete(myCoolPost) -await gs.update(myCoolPost, {}) +graffiti.complete(myCoolPost) +await graffiti.update(myCoolPost, {}) // replace objects, myCoolPost.content += '!!!' -await gs.update(myCoolPost, {}) +await graffiti.update(myCoolPost, {}) // and remove objects. -await gs.remove(myCoolPost) +await graffiti.remove(myCoolPost) // The second argument in the update // function is a query. If the object you @@ -64,10 +64,9 @@ await gs.remove(myCoolPost) const query = { type: 'post' } const myPost = { type: 'post' } const myNotPost = { type: 'notpost' } -gs.complete(myNotPost) +graffiti.complete(myNotPost) // This works -await gs.update(myPost, query) +await graffiti.update(myPost, query) // But this won't work! -await gs.update(myNotPost, query) - +await graffiti.update(myNotPost, query) ``` -- cgit v1.2.3-70-g09d2 From 2b63dac5258cf7feea5d580f56b394476d20ee3e Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: removed utils --- auth.js | 20 +++++++++++++++----- graffiti.js | 7 +++---- utils.js | 11 ----------- 3 files changed, 18 insertions(+), 20 deletions(-) delete mode 100644 utils.js diff --git a/auth.js b/auth.js index 108b42a..872b4aa 100644 --- a/auth.js +++ b/auth.js @@ -1,14 +1,12 @@ -import { randomString, sha256 } from './utils.js' - export default { async logIn(graffitiURL) { // Generate a random client secret and state - const clientSecret = randomString() - const state = randomString() + const clientSecret = this.randomString() + const state = this.randomString() // The client ID is the secret's hex hash - const clientID = await sha256(clientSecret) + const clientID = await this.sha256(clientSecret) // Store the client secret as a local variable window.localStorage.setItem('graffitiClientSecret', clientSecret) @@ -116,4 +114,16 @@ export default { return url }, + randomString() { + return Math.random().toString(36).substr(2) + }, + + 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/graffiti.js b/graffiti.js index 0180a87..1b3365e 100644 --- a/graffiti.js +++ b/graffiti.js @@ -1,5 +1,4 @@ import Auth from './auth.js' -import { randomString } from './utils.js' export default class { @@ -53,7 +52,7 @@ export default class { async request(msg) { // Create a random message ID - const messageID = randomString() + const messageID = Auth.randomString() // Create a listener for the reply const dataPromise = new Promise(resolve => { @@ -150,7 +149,7 @@ export default class { queryID=null) { // Create a random query ID - if (!queryID) queryID = randomString() + if (!queryID) queryID = Auth.randomString() // Send the request await this.request({ @@ -210,7 +209,7 @@ export default class { } // Pre-generate the object's ID if it does not already exist - if (!object._id) object._id = randomString() + if (!object._id) object._id = Auth.randomString() } // Utility function to get a universally unique string diff --git a/utils.js b/utils.js deleted file mode 100644 index ed21d46..0000000 --- a/utils.js +++ /dev/null @@ -1,11 +0,0 @@ -export function randomString() { - return Math.random().toString(36).substr(2) -} - -export async function 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('') -} -- cgit v1.2.3-70-g09d2 From e8fea737dd6baaca9ddcb3917af1af08729bd938 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: Update README.md --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1d7357d..4c27ffa 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,8 @@ const queryID = await graffiti.subscribe({ // And then unsubscribe to those queries await graffiti.unsubscribe(queryID) -// You can log in and out and check your logged-in status -graffiti.logIn() -graffiti.logOut() -if (graffiti.loggedIn) { - // ... -} +// You can toggle logging in and out +graffiti.toggleLogIn() // When you are logged in you can reference your user ID console.log(graffiti.myID) -- cgit v1.2.3-70-g09d2 From c35d4f96a092e8526238762fecbd604ce048ff8a Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: using crypto for random string --- auth.js | 8 ++------ graffiti.js | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/auth.js b/auth.js index 872b4aa..8ee803d 100644 --- a/auth.js +++ b/auth.js @@ -2,8 +2,8 @@ export default { async logIn(graffitiURL) { // Generate a random client secret and state - const clientSecret = this.randomString() - const state = this.randomString() + const clientSecret = crypto.randomUUID() + const state = crypto.randomUUID() // The client ID is the secret's hex hash const clientID = await this.sha256(clientSecret) @@ -114,10 +114,6 @@ export default { return url }, - randomString() { - return Math.random().toString(36).substr(2) - }, - async sha256(input) { const encoder = new TextEncoder() const inputBytes = encoder.encode(input) diff --git a/graffiti.js b/graffiti.js index 1b3365e..94e1ebe 100644 --- a/graffiti.js +++ b/graffiti.js @@ -52,7 +52,7 @@ export default class { async request(msg) { // Create a random message ID - const messageID = Auth.randomString() + const messageID = crypto.randomUUID() // Create a listener for the reply const dataPromise = new Promise(resolve => { @@ -149,7 +149,7 @@ export default class { queryID=null) { // Create a random query ID - if (!queryID) queryID = Auth.randomString() + if (!queryID) queryID = crypto.randomUUID() // Send the request await this.request({ @@ -209,7 +209,7 @@ export default class { } // Pre-generate the object's ID if it does not already exist - if (!object._id) object._id = Auth.randomString() + if (!object._id) object._id = crypto.randomUUID() } // Utility function to get a universally unique string -- cgit v1.2.3-70-g09d2 From 2348e4796eef07418177508204aad5476da01e30 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: added logoot crdt for ordered lists --- logoot.js | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 logoot.js diff --git a/logoot.js b/logoot.js new file mode 100644 index 0000000..f183cbe --- /dev/null +++ b/logoot.js @@ -0,0 +1,134 @@ +export default { + + maxInt: 9007199254740992, + + begin() { + return [] + }, + + end() { + return [this.maxInt] + }, + + lengthWithoutZeros(a) { + let length = a.length + while (length > 0 && a[length - 1] == 0) { + length-- + } + return length + }, + + between(a, b) { + // 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 + } + } + } + + // Finally, sample between the upper and + // lower bounds + out.push(Math.floor(Math.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 + } + }, +} -- cgit v1.2.3-70-g09d2 From 16cbe07449420b94d6121de96f27c02ca5d1bec8 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: added query for logoot --- logoot.js | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/logoot.js b/logoot.js index f183cbe..bcd5f6d 100644 --- a/logoot.js +++ b/logoot.js @@ -1,21 +1,24 @@ export default { - maxInt: 9007199254740992, - - begin() { - return [] + query(property) { + return { + [property]: { + $type: 'array', + $type: ['int', 'long'], + }, + $nor: [ + { [property]: { $gt: this.maxInt } }, + { [property]: { $lt: 0 } }, + ] + } }, - end() { - return [this.maxInt] + get before() { + return [] }, - lengthWithoutZeros(a) { - let length = a.length - while (length > 0 && a[length - 1] == 0) { - length-- - } - return length + get after() { + return [this.maxInt+1] }, between(a, b) { @@ -131,4 +134,15 @@ export default { return 0 } }, + + + lengthWithoutZeros(a) { + let length = a.length + while (length > 0 && a[length - 1] == 0) { + length-- + } + return length + }, + + maxInt: 9007199254740991, } -- cgit v1.2.3-70-g09d2 From e72db6cb8300e0b63e00157f4bfb140b8221137b Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4c27ffa..bf80fd5 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Graffiti for Vanilla Javascript -This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/csail-graffiti/server). -We recommend not using this vanilla library itself but instead using framework plugins that are built on top of it like the [Graffiti plugin for Vue.JS](https://github.com/csail-graffiti/graffiti-x-vue). +This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/digital-graffiti/server). +We recommend not using this vanilla library itself but instead using framework plugins that are built on top of it like the [Graffiti plugin for Vue.JS](https://github.com/digital-graffiti/graffiti-x-vue). Example usage: ```javascript -import Graffiti from "https://csail-graffiti.github.io/graffiti-x-js/graffiti.js" +import Graffiti from "https://digital-graffiti.github.io/graffiti-x-js/graffiti.js" // You can initialize a connection to the graffiti server const graffiti = Graffiti() -- cgit v1.2.3-70-g09d2 From 0b6af85a818fa3c2a8dd8d78ccc29aa099dc40e8 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: anonymized and biased logoot --- README.md | 6 +++--- logoot.js | 13 +++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4c27ffa..bf80fd5 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Graffiti for Vanilla Javascript -This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/csail-graffiti/server). -We recommend not using this vanilla library itself but instead using framework plugins that are built on top of it like the [Graffiti plugin for Vue.JS](https://github.com/csail-graffiti/graffiti-x-vue). +This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/digital-graffiti/server). +We recommend not using this vanilla library itself but instead using framework plugins that are built on top of it like the [Graffiti plugin for Vue.JS](https://github.com/digital-graffiti/graffiti-x-vue). Example usage: ```javascript -import Graffiti from "https://csail-graffiti.github.io/graffiti-x-js/graffiti.js" +import Graffiti from "https://digital-graffiti.github.io/graffiti-x-js/graffiti.js" // You can initialize a connection to the graffiti server const graffiti = Graffiti() diff --git a/logoot.js b/logoot.js index bcd5f6d..c8c7c02 100644 --- a/logoot.js +++ b/logoot.js @@ -21,7 +21,7 @@ export default { return [this.maxInt+1] }, - between(a, b) { + between(a, b, scale=100) { // Strip zeros and find common length const aLength = this.lengthWithoutZeros(a) const bLength = this.lengthWithoutZeros(b) @@ -103,9 +103,14 @@ export default { } } - // Finally, sample between the upper and - // lower bounds - out.push(Math.floor(Math.random() * (upperBound + 1 - lowerBound)) + lowerBound) + // 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 }, -- cgit v1.2.3-70-g09d2 From 28b3586fc4a1e5c1f5bd7f7865a33d6d34dfd6a2 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:46:08 -0400 Subject: anonymized --- graffiti.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graffiti.js b/graffiti.js index 94e1ebe..1e84ae6 100644 --- a/graffiti.js +++ b/graffiti.js @@ -2,7 +2,7 @@ import Auth from './auth.js' export default class { - constructor(graffitiURL="https://graffiti.csail.mit.edu") { + constructor(graffitiURL="https://graffiti.garden") { this.graffitiURL = graffitiURL this.open = false this.subscriptionData = {} -- cgit v1.2.3-70-g09d2 From aefc3f8895e465efab59421c027622e50509d60a Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 21 Sep 2022 12:52:22 -0400 Subject: anonymized --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bf80fd5..06812e5 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Graffiti for Vanilla Javascript -This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/digital-graffiti/server). -We recommend not using this vanilla library itself but instead using framework plugins that are built on top of it like the [Graffiti plugin for Vue.JS](https://github.com/digital-graffiti/graffiti-x-vue). +This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/graffiti-garden/server). +We recommend not using this vanilla library itself but instead using framework plugins that are built on top of it like the [Graffiti plugin for Vue.JS](https://github.com/graffiti-garden/graffiti-x-vue). Example usage: ```javascript -import Graffiti from "https://digital-graffiti.github.io/graffiti-x-js/graffiti.js" +import Graffiti from "https://graffiti-garden.github.io/graffiti-x-js/graffiti.js" // You can initialize a connection to the graffiti server const graffiti = Graffiti() -- cgit v1.2.3-70-g09d2