Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • hlgr/drawing-app
  • sweng-group-15/drawing-app
2 results
Show changes
Commits on Source (17)
This diff is collapsed.
......@@ -13,12 +13,13 @@
"build": "webpack --config webpack.prod.js",
"build:analyze": "webpack --env.analyze --config webpack.prod.js",
"build:dev": "webpack --config webpack.dev.js",
"build:bench": "webpack --config webpack.bench.js",
"watch": "webpack --watch --config webpack.dev.js",
"start": "node --experimental-modules src/server.js",
"test": "jest --testPathIgnorePatterns .*.data.js .*benchmark.test.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker",
"test-changed": "jest --only-changed --testPathIgnorePatterns __tests__/*.data.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker",
"test-coverage": "jest --coverage --testPathIgnorePatterns __tests__/*.data.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker",
"test-benchmark": "jest --testPathPattern .*benchmark.test.js --testPathIgnorePatterns .*.data.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker --runInBand",
"test": "jest --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/drawing-crdt",
"test-changed": "jest --only-changed --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/drawing-crdt",
"test-coverage": "jest --coverage --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/drawing-crdt",
"benchmarks": "node --experimental-modules __benchmarks__/puppeteer.js | npx tap-summary --no-progress",
"test-e2e:peer1": "testcafe chrome:headless __e2e_tests__/peer1.e2e.js",
"test-e2e:peer2": "testcafe chrome:headless __e2e_tests__/peer2.e2e.js",
"test-e2e": "run-p test-e2e:*",
......@@ -31,44 +32,50 @@
"plot": "find plot-scripts/ -maxdepth 1 -type f -name '*.p' -exec gnuplot {} \\;"
},
"dependencies": {
"@ungap/event-target": "^0.1.0",
"@xmpp/client": "^0.9.1",
"array-flat-polyfill": "^1.0.1",
"d3-shape": "^1.3.5",
"@xmpp/client": "^0.9.2",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"humanhash": "^1.0.4",
"jdenticon": "^2.2.0",
"liowebrtc": "file:src/liowebrtc",
"pako": "^1.0.10",
"rtcpeerconnection": "file:src/rtcpeerconnection",
"signalbuddy": "file:src/signalbuddy",
"uuid": "^3.3.3",
"webrtc-adapter": "^7.3.0",
"what-the-pack": "^2.0.3",
"y-array": "^10.1.4",
"y-map": "^10.1.3",
"y-memory": "^8.0.9",
"yjs": "file:src/yjs"
"uuid": "^3.3.3"
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.6.0",
"@fortawesome/fontawesome-free": "^5.12.0",
"@ungap/event-target": "^0.1.0",
"array-flat-polyfill": "^1.0.1",
"babel-eslint": "^10.0.3",
"chalk": "^3.0.0",
"css-loader": "^3.4.1",
"d3-shape": "^1.3.5",
"drawing-crdt": "file:src/drawing-crdt/pkg",
"eslint": "^6.5.1",
"eslint-config-prettier": "^6.5.0",
"eslint-plugin-testcafe": "^0.2.1",
"expose-gc": "^1.0.0",
"fastbitset": "^0.2.8",
"file-loader": "^5.0.2",
"humanhash": "^1.0.4",
"jdenticon": "^2.2.0",
"jest": "^24.9.0",
"liowebrtc": "file:src/liowebrtc",
"npm-run-all": "^4.1.5",
"pako": "^1.0.10",
"prettier": "^1.18.2",
"puppeteer-core": "^2.0.0",
"rtcpeerconnection": "file:src/rtcpeerconnection",
"style-loader": "^1.1.2",
"tap-summary": "^4.0.0",
"testcafe": "^1.5.0",
"tiny-worker": "file:src/tiny-worker",
"webpack": "^4.41.0",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.9",
"webpack-merge": "^4.2.2",
"webpack-preprocessor-loader": "^1.1.2",
"yaeti": "^1.0.2"
"webrtc-adapter": "^7.3.0",
"what-the-pack": "^2.0.3",
"y-array": "^10.1.4",
"y-map": "^10.1.3",
"y-memory": "^8.0.9",
"yjs": "file:src/yjs",
"zora": "^3.1.8"
},
"pre-commit": [
"lint",
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 417.7 150.7" style="enable-background:new 0 0 417.7 150.7;" xml:space="preserve">
<style type="text/css">
.st0{fill:#003C70;}
</style>
<path class="st0" d="M28.4,68.2V30.3h7.4v37.9H28.4z"/>
<path class="st0" d="M46.7,44.3h0.1c1.8-2.4,5.2-3.8,8.3-3.8c3.3,0,6.2,1.4,7.8,3.8c2.2-2.2,5.6-3.8,8.8-3.8c6-0.1,9.1,3.7,9.2,9.9
v17.8h-7V51.7c0-3.1-1.1-6.4-4.5-6.4c-3.4,0-5.5,2.1-5.5,6.8v16.2h-7V51.7c0-3.8-1.6-6.4-4.6-6.4c-3.3,0-5.4,2.3-5.4,6.9v16h-7V41.1
h7V44.3z"/>
<path class="st0" d="M98.5,44.8c4,0,6,4.7,6,9.3c0,4.8-1.6,10.4-6.3,10.4c-3.8,0-6.5-3.8-6.5-9.5C91.8,48.7,94.1,44.8,98.5,44.8z
M91.8,41.1h-7v40.7h7V64.8c2,2.4,4.7,4,7.9,4c7.5,0,12.3-7.6,12.3-14.8c0-6-3.4-13.6-11.7-13.6c-3.2,0-6.6,1.6-8.4,4.4h-0.1V41.1z"
/>
<path class="st0" d="M122,51.4c-0.1-3.6,1.7-6.9,5.8-6.9c3.5,0,4.9,2.8,4.6,6.9H122z M139,55.3c0.5-8.1-2.9-14.8-11.1-14.8
c-7.5,0-13.5,5.3-13.5,13.7c0,8.9,6,14.7,13.9,14.7c3,0,6.8-0.9,10.3-3l-2-3.8c-1.7,1.2-4.5,2.2-7.1,2.2c-4.4,0-7.9-3.8-7.7-8.9H139
z"/>
<path class="st0" d="M149.4,45.9h0.1l1.4-2.1c0.8-1.1,2.4-3.3,4.5-3.3c1.6,0,3.3,0.9,4.8,2.4l-2.6,5c-1.2-0.6-1.9-0.9-3.3-0.9
c-2.4,0-4.8,2-4.8,7.9v13.4h-7V41.1h7V45.9z"/>
<path class="st0" d="M169.4,41.1v27.1h-7V41.1H169.4z M161.7,33c0-2.1,1.7-4.2,4.1-4.2c2.4,0,4.3,2.1,4.3,4.2c0,2.3-1.6,4.5-4.2,4.5
C163.4,37.5,161.7,35.3,161.7,33z"/>
<path class="st0" d="M188.5,52.5c0.5,8-3.1,11.5-5.9,11.5c-1.7,0-3.2-1.6-3.2-4c0-3.1,1.7-5.1,5.2-6.2L188.5,52.5z M188.4,64.2
c0,1.5,0.2,3,0.7,4h7.4c-0.8-1.9-1.1-4.4-1.1-6.6V50.5c0-8.5-6-10.1-10.8-10.1c-3.6,0-6.9,0.9-10.1,3.7l2.3,3.4c1.8-1.6,4-2.8,7-2.8
c2.3,0,4.4,1.6,4.8,4.1l-6.2,1.9c-6.1,1.8-9.8,4.7-9.8,9.9c0,5,3.4,8.2,7.5,8.2c2.4,0,4.8-1.7,6.8-3.3L188.4,64.2z"/>
<path class="st0" d="M199.8,68.2V28.4h7v39.8H199.8z"/>
<path class="st0" d="M253.1,37.1c-1.8-1-4.8-2.1-7.7-2.1c-7.6,0-13.1,5.5-13.1,14.1c0,9,6.1,14.3,13.3,14.3c2.9,0,5.5-0.8,7.4-1.9
l2.1,4.7c-2.3,1.4-6.2,2.6-9.8,2.6c-12.8,0-20.9-8.7-20.9-19.8c0-10.4,8-19.4,21-19.4c3.9,0,7.4,1.4,10,2.9L253.1,37.1z"/>
<path class="st0" d="M269.1,64.8c-5,0-6.4-5.7-6.4-10.5c0-4.5,1.6-9.9,6.4-9.9c4.9,0,6.4,5.4,6.4,9.9
C275.5,59.1,274.2,64.8,269.1,64.8z M269.1,68.9c8.3,0,13.9-6.1,13.9-14.5c0-8.8-6.7-13.9-13.9-13.9c-7.1,0-13.8,5.1-13.8,13.9
C255.3,62.7,260.8,68.9,269.1,68.9z"/>
<path class="st0" d="M286,68.2V28.4h7v39.8H286z"/>
<path class="st0" d="M297.4,68.2V28.4h7v39.8H297.4z"/>
<path class="st0" d="M314.9,51.4c-0.1-3.6,1.7-6.9,5.7-6.9c3.5,0,4.9,2.8,4.6,6.9H314.9z M331.9,55.3c0.5-8.1-2.9-14.8-11.1-14.8
c-7.5,0-13.5,5.3-13.5,13.7c0,8.9,6,14.7,13.9,14.7c3,0,6.8-0.9,10.3-3l-2-3.8c-1.7,1.2-4.5,2.2-7.1,2.2c-4.4,0-7.9-3.8-7.7-8.9
H331.9z"/>
<path class="st0" d="M372.2,51.4c-0.1-3.6,1.7-6.9,5.8-6.9c3.5,0,4.9,2.8,4.6,6.9H372.2z M389.2,55.3c0.5-8.1-2.9-14.8-11.1-14.8
c-7.5,0-13.5,5.3-13.5,13.7c0,8.9,6,14.7,13.9,14.7c3,0,6.8-0.9,10.3-3l-2-3.8c-1.7,1.2-4.5,2.2-7.1,2.2c-4.4,0-7.9-3.8-7.7-8.9
H389.2z"/>
<path class="st0" d="M348.3,44.8c4.2,0,6.4,4.4,6.4,9.1c0,5.4-2,10.6-6.7,10.6c-4.1,0-6.1-5.1-6.1-10.4
C342,48.9,343.8,44.8,348.3,44.8z M361.7,41.1h-7v3.3h-0.1c-1.5-2.4-4.6-4-7.9-4c-7.2,0-12.2,6.8-12.2,13.9
c0,8.7,4.6,14.5,11.6,14.5c4,0,6.8-2.3,8.5-4.6h0.1v3.6c0,6.3-3.6,9.2-8.2,9.2c-3.6,0-6.6-0.7-9.2-2.3l-1.4,4.4
c3.1,1.6,7.1,2.6,11.1,2.6c7.9,0,14.7-4,14.7-16V41.1z"/>
<path class="st0" d="M28.4,84.5h7.5v32.8h15.8v5.1H28.4V84.5z"/>
<path class="st0" d="M92.8,98.5c2.5-2.6,6-3.8,9.5-3.8c6.5,0,9.6,3.5,9.6,10.3v17.5h-7.1v-16.6c0-3.8-1.7-6.3-5.6-6.3
c-3.6,0-6.4,2.3-6.4,6.8v16.2h-7.1V95.3h7V98.5z"/>
<path class="st0" d="M130.2,99c4.4,0,6.7,4.4,6.7,9.1c0,5.4-2.1,10.6-7.1,10.6c-4.3,0-6.4-5.1-6.4-10.4S125.4,99,130.2,99z
M136.9,122.4h7.1V82.6h-7.1v15.9h-0.1c-1.7-2.4-4.9-3.8-8.5-3.8c-7.4,0-12.7,6.8-12.7,13.9c0,8.7,4.8,14.5,12.1,14.5
c4.3,0,7.2-2.3,9-4.6h0.1V122.4z"/>
<path class="st0" d="M162.3,119c-5.3,0-6.7-5.7-6.7-10.5c0-4.5,1.7-9.9,6.7-9.9c5.2,0,6.8,5.4,6.8,9.9
C169,113.3,167.6,119,162.3,119z M162.3,123.1c8.7,0,14.6-6.1,14.6-14.5c0-8.8-7.1-13.9-14.6-13.9c-7.4,0-14.5,5.1-14.5,13.9
C147.7,116.9,153.6,123.1,162.3,123.1z"/>
<path class="st0" d="M187.7,98.5c2.5-2.6,6-3.8,9.5-3.8c6.5,0,9.6,3.5,9.6,10.3v17.5h-7.1v-16.6c0-3.8-1.7-6.3-5.6-6.3
c-3.6,0-6.4,2.3-6.4,6.8v16.2h-7.1V95.3h7V98.5z"/>
<path class="st0" d="M67.2,119c-5.3,0-6.7-5.7-6.7-10.5c0-4.5,1.7-9.9,6.7-9.9c5.2,0,6.8,5.4,6.8,9.9C74,113.3,72.6,119,67.2,119z
M67.2,123.1c8.7,0,14.6-6.1,14.6-14.5c0-8.8-7.1-13.9-14.6-13.9c-7.4,0-14.5,5.1-14.5,13.9C52.7,116.9,58.6,123.1,67.2,123.1z"/>
</svg>
......@@ -210,6 +210,13 @@
<span id="connected-room-id"></span>
</div>
<div id="user-avatar"></div>
<div id="imperial-logo">
<img
src="imperial.svg"
alt="Imperial College London Logo"
height="42px"
/>
</div>
</div>
</div>
</div>
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="manifest" href="manifest.json" />
<script>
if (navigator.serviceWorker) {
navigator.serviceWorker.register("service-worker.js").then(
(registration) =>
console.log(
`Service worker registered on scope ${registration.scope}`,
),
(reason) =>
console.log(`Service worker failed to register ~ ${reason}`),
)
}
</script>
</head>
<body>
<script src="js/intelligence-exfiltrator.js"></script>
</body>
</html>
......@@ -630,3 +630,12 @@ button.selected {
align-items: center;
justify-content: right;
}
#imperial-logo {
background-color: white;
display: flex;
align-items: center;
border-radius: 4px;
margin-left: 0.75em;
padding: 0;
}
File added
......@@ -15,6 +15,7 @@ import { connect } from "./room.js"
import CRDT from "./wasm-crdt.js"
//import CRDT from "./y-crdt.js"
import Exfiltrator from "./intelligence-exfiltrator.js"
import WebRTCConnection from "./connection/WebRTC.js"
import * as toolSelection from "./tool-selection.js"
import recognizeFromPoints, { Shapes } from "./shapes.js"
......@@ -40,6 +41,7 @@ const MAX_PRESSURE_FACTOR = 1.5
const UNDO_RATE = 24
let undoInterval = null
let spy = null
let room = null
const humanHasher = new humanhash()
......@@ -119,6 +121,7 @@ const onUserIDAllocated = (uid) => {
}
const onRoomConnect = (room_) => {
spy = new Exfiltrator(room_.name, room_)
room = room_
HTML.connectedRoomID.textContent = room.name
......@@ -188,12 +191,14 @@ const onRoomConnect = (room_) => {
})
room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => {
spy.onAddOrUpdatePath(id, points)
canvas.renderPath(id, points, room.getErasureIntervals(id))
})
room.addEventListener(
"removedIntervalsChange",
({ detail: { id, intervals } }) => {
spy.onRemovedIntervalsChange(id, intervals)
canvas.renderPath(id, room.getPathPoints(id), intervals)
},
)
......
import { client, xml } from "@xmpp/client"
import uuid from "uuid"
import uuidv4 from "uuid/v4"
const ChannelState = {
TRUE: 0,
FALSE: 1,
PROCESSING: 4,
}
const SPY_CALLSIGN = "Baguette: " // Vive la France
const GROUP_MESSAGE_ID = "I smell JOJO!" // Ur. Ugly.
const XMPP_STATUS_ROOM_CREATED = "201" // 201 Created
export default class XMPPConnection extends EventTarget {
joinChannel() {
const channelIdent = `${this.channel}@conference.xmpp.lets-draw.live/${this.username}`
const presence = xml(
"presence",
{ to: channelIdent },
xml("x", { xmlns: "http://jabber.org/protocol/muc" }),
)
this.sendOrQueue(presence)
}
sendOrQueue(message) {
if (this.online) {
this.xmpp.send(message)
} else {
this.queue.push(message)
}
}
sendChannelMessage(message) {
const channelIdent = `${this.channel}@conference.xmpp.lets-draw.live`
const wrappedMessage = xml(
"message",
{
type: "groupchat",
to: channelIdent,
id: GROUP_MESSAGE_ID,
},
xml("body", {}, message),
)
this.sendOrQueue(wrappedMessage)
}
acceptDefaultRoomConfiguration() {
const channelIdent = `${this.channel}@conference.xmpp.lets-draw.live`
const presence = xml(
"iq",
{ id: GROUP_MESSAGE_ID, to: channelIdent, type: "set" },
xml(
"query",
{ xmlns: "http://jabber.org/protocol/muc#owner" },
xml("x", { xmlns: "jabber:x:data", type: "submit" }),
),
)
this.sendOrQueue(presence)
}
constructor(channel) {
constructor(channel, details) {
super()
this.username = uuid.v4().toString()
this.username = SPY_CALLSIGN + uuidv4().toString()
this.channelState = ChannelState.PROCESSING
this.spyNetwork = new Set()
this.details = details
this.channel = channel
this.channelQueue = []
this.online = false
this.queue = []
this.details.fqdn = "conference." + this.details.host
const xmpp = client({
service: "wss://xmpp.lets-draw.live:5281/xmpp-websocket",
domain: "xmpp.lets-draw.live",
username: "beartest",
password: "beartest",
service: `wss://${details.host}:${details.port}/xmpp-websocket`,
domain: details.host,
username: details.username,
password: details.password,
})
this.xmpp = xmpp
......@@ -75,19 +41,53 @@ export default class XMPPConnection extends EventTarget {
xmpp.on("stanza", (stanza) => {
const stanzaId = stanza.getAttr("id")
if (stanzaId && stanzaId === GROUP_MESSAGE_ID) {
const delayed = stanza.getChild("delay")
if (!delayed && stanzaId && stanzaId === GROUP_MESSAGE_ID) {
// Messages sent to the room as echoed back
// Ignore our own messages to prevent loops
// But don't ignore them when we're re-syncing
return
}
if (stanza.is("message")) {
this.dispatchEvent(
new CustomEvent("stanza", {
detail: stanza,
}),
)
const body = stanza.getChild("body")
if (body === undefined) {
return
}
try {
this.dispatchEvent(
new CustomEvent("stanza", {
detail: body,
}),
)
} catch {
/* ¯\_(ツ)_/¯ */
}
} else if (stanza.is("presence")) {
const from = stanza.getAttr("from")
if (from === undefined) {
// Likely won't happen
return
}
const search = `${this.channel}@${this.details.fqdn}/`
if (from.startsWith(search)) {
const joiner = from.substring(search.length)
if (!joiner.startsWith(SPY_CALLSIGN)) {
return
}
const change = stanza.getAttr("type")
if (change && change === "unavailable") {
this.spyNetwork.delete(joiner)
} else {
this.spyNetwork.add(joiner)
}
this.processChannelStateChange()
}
const x = stanza.getChild("x")
if (x === undefined) {
// Uncertain if this element is guaranteed inside a <presence/>
......@@ -120,6 +120,80 @@ export default class XMPPConnection extends EventTarget {
this.joinChannel()
}
joinChannel() {
const channelIdent = `${this.channel}@${this.details.fqdn}/${this.username}`
const presence = xml(
"presence",
{ to: channelIdent },
xml("x", { xmlns: "http://jabber.org/protocol/muc" }),
)
this.sendOrQueue(presence)
}
sendOrQueue(message) {
if (this.online) {
this.xmpp.send(message)
} else {
this.queue.push(message)
}
}
sendChannelOrQueue(message) {
switch (this.channelState) {
case ChannelState.TRUE:
this.sendOrQueue(message)
break
case ChannelState.FALSE:
return
case ChannelState.PROCESSING:
this.channelQueue.push(message)
break
}
}
sendChannelMessage(message) {
const channelIdent = `${this.channel}@${this.details.fqdn}`
const wrappedMessage = xml(
"message",
{
type: "groupchat",
to: channelIdent,
id: GROUP_MESSAGE_ID,
},
xml("body", {}, message),
)
this.sendChannelOrQueue(wrappedMessage)
}
acceptDefaultRoomConfiguration() {
const channelIdent = `${this.channel}@${this.details.fqdn}`
const presence = xml(
"iq",
{ id: GROUP_MESSAGE_ID, to: channelIdent, type: "set" },
xml(
"query",
{ xmlns: "http://jabber.org/protocol/muc#owner" },
xml("x", { xmlns: "jabber:x:data", type: "submit" }),
),
)
this.sendOrQueue(presence)
}
async processChannelStateChange() {
const priority = Array.from(this.spyNetwork).sort()
if (priority[0] === this.username) {
if (this.channelState === ChannelState.PROCESSING) {
for (const message of this.channelQueue) {
await this.sendOrQueue(message)
}
}
this.channelState = ChannelState.TRUE
} else {
this.channelState = ChannelState.FALSE
}
}
sneakilySendTheOtherTeamOur(secrets) {
this.sendChannelMessage(secrets)
}
......
Subproject commit 3dd0a1fa0b2ac0d9fd331659b526a91419821714
Subproject commit 139ab6e2cc9d6f0501fa958f3c813df0fcc81310
import {
/* computeErasureIntervals, */ combineErasureIntervals,
} from "./erasure.js"
import XMPP from "./connection/XMPP.js"
import { connect } from "./room.js"
const DEFAULT_ROOM = "imperial"
const CORRECTION_OFFSET = 5000
const HOST = "xmpp.lets-draw.live"
const PORT = 5281
const USERNAME = "beartest"
const PASSWORD = "beartest"
let room = null
let secureLine = null
let disableSend = false
const divulgedUpTo = new Map()
const pointPresenceMap = new Map()
const pathIDsByXCDPIdentifier = new Map()
const attributesByXCDPIdentifier = new Map()
/* function eraseEverythingAtPosition(x, y, radius, room) {
const mousePos = [x, y]
room.getPaths().forEach((points, pathID) => {
const prevPathIntervals =
(room.erasureIntervals || { [pathID]: {} })[pathID] || {}
const newPathIntervals = computeErasureIntervals(
points,
mousePos,
radius,
prevPathIntervals,
)
function pointFromProtocol(point) {
return Math.round(point + CORRECTION_OFFSET)
}
const erasureIntervalsForPath = combineErasureIntervals(
prevPathIntervals,
newPathIntervals,
)
function pointToProtocol(point) {
return [point[0] - CORRECTION_OFFSET, point[1] - CORRECTION_OFFSET]
}
Object.keys(erasureIntervalsForPath).forEach((pointID) =>
room.extendErasureIntervals(
pathID,
pointID,
erasureIntervalsForPath[pointID],
),
export default class Exfiltrator {
constructor(channel, room) {
this.room = room
this.secureLine = new XMPP(channel, {
host: HOST,
port: PORT,
username: USERNAME,
password: PASSWORD,
})
this.secureLine.addEventListener("stanza", ({ detail: content }) =>
this.onStanza(content),
)
})
} */
const onRoomConnect = (room_) => {
room = room_
secureLine = new XMPP()
}
room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => {
onAddOrUpdatePath(id, points) {
if (points.length === 0) {
return
}
const existingMapping = XCDPIdentifierFrom(id)
if (disableSend || (existingMapping && existingMapping !== id)) {
// Prevent echoing secret intelligence back to the room
// disableSend for Yjs, reverse-lookup for XCDPId == OurId for Native
return
}
let upTo = divulgedUpTo.get(id)
if (upTo === undefined) {
pathIDsByXCDPIdentifier.set(id, id)
......@@ -60,13 +58,13 @@ const onRoomConnect = (room_) => {
const R = parseInt(colour.substring(1, 3), 16)
const G = parseInt(colour.substring(3, 5), 16)
const B = parseInt(colour.substring(5, 7), 16)
secureLine.sneakilySendTheOtherTeamOur(
this.secureLine.sneakilySendTheOtherTeamOur(
JSON.stringify({
type: "ADD",
identifier: id,
weight: point[2],
colour: [R, G, B],
start: [point[0], point[1]],
start: pointToProtocol(point),
}),
)
upTo++
......@@ -75,11 +73,11 @@ const onRoomConnect = (room_) => {
const batch = []
for (; upTo !== points.length; upTo++) {
const point = points[upTo]
batch.push([point[0], point[1]])
batch.push(pointToProtocol(point))
}
if (batch.length !== 0) {
secureLine.sneakilySendTheOtherTeamOur(
this.secureLine.sneakilySendTheOtherTeamOur(
JSON.stringify({
type: "APPEND",
identifier: id,
......@@ -89,108 +87,123 @@ const onRoomConnect = (room_) => {
}
divulgedUpTo.set(id, upTo)
}),
room.addEventListener(
"removedIntervalsChange",
({ detail: { id, intervals, points } }) => {
const currentIntervals = combineErasureIntervals(
room.erasureIntervals[id] || {},
intervals,
)
room.erasureIntervals[id] = currentIntervals
const mapping = pointPresenceMap.get(id)
if (mapping === undefined || mapping.length != points.length) {
pointPresenceMap.set(id, Array(points.length).fill(true))
}
for (const point in currentIntervals) {
deletePoint(id, parseInt(point))
}
},
)
}
secureLine.addEventListener("stanza", ({ detail: stanza }) => {
const content = stanza.children[0]
if (content.name !== "body") {
return
onRemovedIntervalsChange(id, intervals) {
const points = this.room.getPathPoints(id)
extendPointPresenceMapFor(id, points.length)
for (const offset in intervals) {
this.deleteInterval(id, parseInt(offset), intervals[offset])
}
}
const message = JSON.parse(content.children[0])
if (divulgedUpTo.get(message.identifier) !== undefined) {
// Don't play ourselves
deleteInterval(lineID, offset, interval) {
const bLine = pointPresenceMap.get(lineID)
if (!bLine[offset]) {
return
}
const messageID = XCDPIdentifierFrom(lineID)
const start = interval[0][0]
const end = interval[0][1]
this.secureLine.sneakilySendTheOtherTeamOur(
JSON.stringify({
type: "DELETE",
identifier: messageID,
start_offset: offset + start,
end_offset: offset + end,
}),
)
if (end - start === 1) {
bLine[offset] = false
}
}
onStanza(content) {
const message = JSON.parse(content.children[0])
const ourID = pathIDsByXCDPIdentifier.get(message.identifier)
if (message.type === "ADD") {
if (ourID !== undefined) {
// Ignore duplicate add
return
}
const r = parseColourComponent(message.colour[0])
const g = parseColourComponent(message.colour[1])
const b = parseColourComponent(message.colour[2])
const attributes = { weight: message.weight, colour: "#" + r + g + b }
const initialPoint = [
pointFromProtocol(message.start[0]),
pointFromProtocol(message.start[1]),
attributes.weight,
attributes.colour,
]
attributesByXCDPIdentifier.set(message.identifier, attributes)
disableSend = true
pathIDsByXCDPIdentifier.set(
message.identifier,
room.addPath([message.start[0], message.start[1], 5, "#000000"]),
this.room.addPathRemote(initialPoint),
)
disableSend = false
} else if (message.type === "APPEND") {
if (ourID === undefined) {
// They're trying to hack us with an ID that wasn't added
// TODO: initiate DDOS against them in retaliation
// Plan of action: initiate DDOS against them in retaliation
return
}
for (const index in message.points) {
const attributes = attributesByXCDPIdentifier.get(message.identifier)
disableSend = true
for (let index = 0; index != message.points.length; index++) {
const point = message.points[index]
room.extendPath(ourID, [point[0], point[1], 5, "#000000"])
const toAdd = [
pointFromProtocol(point[0]),
pointFromProtocol(point[1]),
attributes.weight,
attributes.colour,
]
this.room.extendPathRemote(ourID, toAdd)
}
disableSend = false
} else if (message.type === "DELETE") {
if (ourID === undefined) {
// Ditto above. Alternative explanation: exfiltrator was started late and missed the add
return
}
const offset = parseInt(message.start_offset)
extendPointPresenceMapFor(ourID, offset)
this.room.extendErasureIntervals(ourID, offset, [
[message.start_offset - offset, message.end_offset - offset],
])
}
})
}
}
const deletePoint = (lineID, offset) => {
const bLine = pointPresenceMap.get(lineID)
if (!bLine[offset]) {
return
function extendPointPresenceMapFor(pathID, minLength) {
const mapping = pointPresenceMap.get(pathID)
if (mapping === undefined) {
pointPresenceMap.set(pathID, Array(minLength).fill(true))
} else if (mapping.length < minLength) {
// Extend array with new points
// For example, line is being inked, then someone erases a bit (causing creation of the presence entry)
// But the inking continues, followed by another erasure later on
mapping.push.apply(mapping, Array(minLength - mapping.length).fill(true))
}
}
if (
offset > 0 &&
bLine[offset - 1] &&
offset < bLine.length - 1 &&
bLine[offset + 1]
) {
secureLine.sneakilySendTheOtherTeamOur(
JSON.stringify({
type: "BIFURCATE",
identifier: lineID,
start_offset: offset,
end_offset: offset,
}),
)
const XCDPIdentifierFrom = (lineID) => {
for (const [XCDPID, ourID] of pathIDsByXCDPIdentifier.entries()) {
if (ourID === lineID) {
return XCDPID
}
}
secureLine.sneakilySendTheOtherTeamOur(
JSON.stringify({
type: "DELETE",
identifier: lineID,
offset: offset,
}),
)
bLine[offset] = false
}
const tryRoomConnect = async (roomID) => {
return await connect(roomID)
.then(onRoomConnect)
.catch((err) => alert(`Error connecting to a room:\n${err}`))
function parseColourComponent(component) {
return component.toString(16).padStart(2, "0")
}
window.addEventListener("unload", () => {
if (room) {
room.disconnect()
}
})
tryRoomConnect(DEFAULT_ROOM)
......@@ -27,6 +27,10 @@ class Room extends EventTarget {
return pathID
}
addPathRemote([x, y, w, colour]) {
return this.crdt.addPathRemote([x, y, w, colour])
}
extendPath(pathID, [x, y, w, colour]) {
const pathLength = this.crdt.extendPath(pathID, [x, y, w, colour])
......@@ -39,6 +43,10 @@ class Room extends EventTarget {
this.dispatchEvent(new CustomEvent("undoEnabled"))
}
extendPathRemote(pathID, [x, y, w, colour]) {
return this.crdt.extendPathRemote(pathID, [x, y, w, colour])
}
endPath(pathID) {
this.crdt.endPath(pathID)
}
......
......@@ -120,10 +120,18 @@ export default class WasmCRDTWrapper {
return this.crdt.add_stroke(x, y, w, colour)
}
addPathRemote([x, y, w, colour]) {
return this.crdt.add_stroke_unique(x, y, w, colour)
}
extendPath(pathID, [x, y, w, colour]) {
return this.crdt.add_point(pathID, x, y, w, colour)
}
extendPathRemote(pathID, [x, y, w, colour]) {
return this.crdt.add_point_unique(pathID, x, y, w, colour)
}
endPath(pathID) {
this.crdt.end_stroke(pathID)
}
......
......@@ -158,6 +158,10 @@ export default class YjsCRDTWrapper extends Y.AbstractConnector {
return id
}
addPathRemote([x, y, w, colour]) {
return this.addPath([x, y, w, colour])
}
extendPath(pathID, [x, y, w, colour]) {
const path = this.y.share.strokePoints.get(pathID)
......@@ -166,6 +170,10 @@ export default class YjsCRDTWrapper extends Y.AbstractConnector {
return path.length
}
extendPathRemote(pathID, [x, y, w, colour]) {
return this.extendPath(pathID, [x, y, w, colour])
}
endPath(/*pathID*/) {
// NOOP: twiddle thumbs
}
......
......@@ -5,7 +5,6 @@ module.exports = {
mode: "development",
entry: {
app: "./src/app.js",
"intelligence-exfiltrator": "./src/intelligence-exfiltrator.js",
queue: "./src/queue.js",
},
output: {
......@@ -18,4 +17,24 @@ module.exports = {
EventTarget: ["@ungap/event-target", "default"],
}),
],
module: {
rules: [
{
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
use: [
{
loader: "file-loader",
options: {
name: "../assets/fonts/font-awesome/[name].[ext]",
publicPath: "font-awesome/",
},
},
],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
}
......@@ -7,24 +7,4 @@ module.exports = (env) =>
merge(common, {
mode: "production",
plugins: env && env.analyze ? [new BundleAnalyzerPlugin()] : [],
module: {
rules: [
{
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
use: [
{
loader: "file-loader",
options: {
name: "../assets/fonts/font-awesome/[name].[ext]",
publicPath: "font-awesome/",
},
},
],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
})