diff --git a/package-lock.json b/package-lock.json index bd591f5..7ef4053 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "nodemailer": "^6.10.0", "pg": "^8.13.1", "qrcode": "^1.5.4", + "randomcolor": "^0.6.2", "reflect-metadata": "^0.2.2", "rss-parser": "^3.13.0", "socket.io": "^4.8.1", @@ -53,6 +54,7 @@ "@types/node-schedule": "^2.1.6", "@types/nodemailer": "^6.4.17", "@types/qrcode": "~1.5.5", + "@types/randomcolor": "^0.5.9", "@types/speakeasy": "^2.0.10", "@types/uuid": "^9.0.2", "ts-node": "10.7.0", @@ -480,6 +482,13 @@ "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", "dev": true }, + "node_modules/@types/randomcolor": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/randomcolor/-/randomcolor-0.5.9.tgz", + "integrity": "sha512-k58cfpkK15AKn1m+oRd9nh5BnuiowhbyvBBdAzcddtARMr3xRzP0VlFaAKovSG6N6Knx08EicjPlOMzDejerrQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -3471,6 +3480,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/randomcolor": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/randomcolor/-/randomcolor-0.6.2.tgz", + "integrity": "sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==", + "license": "CC0" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", diff --git a/package.json b/package.json index edf5a42..12b5e3c 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "nodemailer": "^6.10.0", "pg": "^8.13.1", "qrcode": "^1.5.4", + "randomcolor": "^0.6.2", "reflect-metadata": "^0.2.2", "rss-parser": "^3.13.0", "socket.io": "^4.8.1", @@ -68,6 +69,7 @@ "@types/node-schedule": "^2.1.6", "@types/nodemailer": "^6.4.17", "@types/qrcode": "~1.5.5", + "@types/randomcolor": "^0.5.9", "@types/speakeasy": "^2.0.10", "@types/uuid": "^9.0.2", "ts-node": "10.7.0", diff --git a/src/helpers/missionDocHelper.ts b/src/helpers/missionDocHelper.ts index f979059..da1073b 100644 --- a/src/helpers/missionDocHelper.ts +++ b/src/helpers/missionDocHelper.ts @@ -19,9 +19,13 @@ export default abstract class MissionDocHelper { // const ytext = ydoc.getText('meinText'); // ytext.insert(0, 'Hier ist ein initialer Text'); - const awareness = 0; // TODO Awareness - - MissionMap.write(missionId, { missionId, doc, awareness, timestamp: 0 }); + MissionMap.write(missionId, { + missionId, + doc, + editors: new Map(), + editorStates: new Map(), + timestamp: 0, + }); console.log(`created local doc ${missionId}`); } @@ -40,6 +44,16 @@ export default abstract class MissionDocHelper { return mission.timestamp; } + public static getAwarenessState(missionId: string) { + const mission = MissionMap.read(missionId); + return Array.from(mission.editorStates.entries()); + } + + public static getAwarenessEditors(missionId: string) { + const mission = MissionMap.read(missionId); + return Array.from(mission.editors.entries()); + } + public static async saveDoc(missionId: string, update: Uint8Array) { MissionMap.updateState(missionId, update); const mission = MissionMap.read(missionId); diff --git a/src/storage/missionMap.ts b/src/storage/missionMap.ts index 57d7e9f..9e99eda 100644 --- a/src/storage/missionMap.ts +++ b/src/storage/missionMap.ts @@ -4,10 +4,24 @@ import ms from "ms"; export interface missionStoreModel { missionId: string; doc: Y.Doc; - awareness: any; + editors: Map; + editorStates: Map; timestamp: number; } +export interface Editor { + username: string; + firstname: string; + lastname: string; + color: string; +} + +export interface EditorState { + field: string; + cursor: any; + range: any; +} + /** * @description store credentials to socket to prevent auth data change while connected */ @@ -32,12 +46,30 @@ export abstract class MissionMap { this.write(identifier, mission, true); } - public static updateAwareness(identifier: string, data: any): void { + public static updateAwareness( + identifier: string, + data: { action: "update" | "remove"; socketId: string } & ( + | { store: "editors"; update: Editor } + | { store: "state"; update: EditorState } + ) + ): void { this.monitorAccess(identifier); let mission = this.read(identifier); - // TODO save awareness + if (data.store == "editors") { + if (data.action == "update") { + mission.editors.set(data.socketId, data.update); + } else { + mission.editors.delete(data.socketId); + } + } else { + if (data.action == "update") { + mission.editorStates.set(data.socketId, data.update); + } else { + mission.editorStates.delete(data.socketId); + } + } this.write(identifier, mission, true); } diff --git a/src/storage/socketMap.ts b/src/storage/socketMap.ts index 87ea0b1..e5bfeec 100644 --- a/src/storage/socketMap.ts +++ b/src/storage/socketMap.ts @@ -4,6 +4,8 @@ export interface socketStoreModel { socketId: string; userId: string; username: string; + firstname: string; + lastname: string; isOwner: string; permissions: PermissionObject; isWebApiRequest: boolean; diff --git a/src/websocket/endpoints/missionManagement.ts b/src/websocket/endpoints/missionManagement.ts index 53a4dd2..63b586d 100644 --- a/src/websocket/endpoints/missionManagement.ts +++ b/src/websocket/endpoints/missionManagement.ts @@ -1,8 +1,10 @@ import { Server, Socket } from "socket.io"; import { handleEvent } from "../handleEvent"; -import { MissionMap, missionStoreModel } from "../../storage/missionMap"; +import { EditorState, MissionMap, missionStoreModel } from "../../storage/missionMap"; import * as Y from "yjs"; import MissionDocHelper from "../../helpers/missionDocHelper"; +import randomColor from "randomcolor"; +import { SocketMap } from "../../storage/socketMap"; export default (io: Server, socket: Socket) => { socket.on( @@ -45,9 +47,36 @@ export default (io: Server, socket: Socket) => { }); } - socket.emit("package-sync-awareness", { - update: [], // do awareness self - source: "server", + console.log(MissionDocHelper.getAwarenessEditors(missionId)); + socket.emit("package-init-awareness", { + update: MissionDocHelper.getAwarenessEditors(missionId), + type: "editors", + }); + socket.emit("package-init-awareness", { + update: MissionDocHelper.getAwarenessState(missionId), + type: "state", + }); + + let newEditor = { + socketId: socket.id, + data: { + username: SocketMap.read(socket.id).username, + firstname: SocketMap.read(socket.id).firstname, + lastname: SocketMap.read(socket.id).lastname, + color: randomColor({ format: "hex" }), + }, + }; + MissionMap.updateAwareness(missionId, { + action: "update", + socketId: newEditor.socketId, + store: "editors", + update: newEditor.data, + }); + socket.to(missionId).emit("package-sync-awareness", { + update: newEditor.data, + socketId: socket.id, + action: "update", + type: "editors", }); return { @@ -124,16 +153,23 @@ export default (io: Server, socket: Socket) => { "mission:sync-client-awareness", handleEvent( { type: "create", section: "operation", module: "mission" }, - async (data: { update: Array; timestamp: number }) => { + async (data: EditorState) => { const socketRooms = Array.from(socket.rooms).filter((room) => room !== socket.id && room !== "home"); try { - MissionMap.updateAwareness(socketRooms[0], new Uint8Array(data.update)); + MissionMap.updateAwareness(socketRooms[0], { + action: "update", + store: "state", + update: data, + socketId: socket.id, + }); return { type: "package-sync-awareness", answer: { - update: data.update, - source: socket.id, + type: "state", + action: "update", + update: data, + socketId: socket.id, }, room: socketRooms[0], }; @@ -163,6 +199,32 @@ export default (io: Server, socket: Socket) => { MissionMap.delete(socketRooms[0]); } + MissionMap.updateAwareness(socketRooms[0], { + action: "remove", + store: "editors", + update: undefined, + socketId: socket.id, + }); + io.to(socketRooms[0]).emit("package-sync-awareness", { + type: "editors", + update: undefined, + action: "remove", + socketId: socket.id, + }); + + MissionMap.updateAwareness(socketRooms[0], { + action: "remove", + store: "state", + update: undefined, + socketId: socket.id, + }); + io.to(socketRooms[0]).emit("package-sync-awareness", { + type: "state", + update: undefined, + action: "remove", + socketId: socket.id, + }); + return { type: "deleted", answer: "leave instance", diff --git a/src/websocket/middleware/authenticateSocket.ts b/src/websocket/middleware/authenticateSocket.ts index 2899914..a8a70ca 100644 --- a/src/websocket/middleware/authenticateSocket.ts +++ b/src/websocket/middleware/authenticateSocket.ts @@ -39,6 +39,8 @@ export default async function authenticateSocket(socket: Socket, next: Function) socketId: socket.id, userId: decoded.userId, username: decoded.username, + firstname: decoded.firstname, + lastname: decoded.lastname, isOwner: decoded.isOwner, permissions: decoded.permissions, isWebApiRequest: decoded?.sub == "webapi_access_token",