From 099e625e0149fb2a6c5b06f0ad0ddaf9678e1b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Dietrich?= Date: Thu, 7 Sep 2023 19:23:23 +0200 Subject: [PATCH] Add initial websocket support --- client/components/Modules.vue | 69 +++++++++++++++++++++++----- server/data_web.ts | 85 +++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 10 deletions(-) diff --git a/client/components/Modules.vue b/client/components/Modules.vue index 794c683..c677ed3 100644 --- a/client/components/Modules.vue +++ b/client/components/Modules.vue @@ -38,7 +38,7 @@ export default { name: "Modules", props: ["role", "username", "liveClassProxy"], data() { - return {}; + return { ws: null }; }, computed: { roomName() { @@ -93,16 +93,65 @@ export default { } }, async sendMessage(subject, body, module_url) { - if (body !== undefined) - await this.$axios.$post( - "/data/sendMessage/" + this.$store.state.class_.id, - { - from: this.username /* Email if teacher, name if station */, - subject: subject, - body: body, - module: module_url, - } + if (this.ws === null) { + const self = this; + const socket = new WebSocket( + "ws://localhost:8000/data/wss/" + + this.$store.state.class_.id + + "/" + + this.username ); + + socket.onopen = function (e) { + console.warn("[open] Connection established"); + self.sendMessage(subject, body, module_url); + }; + + const iframes = document.getElementsByTagName("iframe"); + + socket.onmessage = function (event) { + const data = JSON.parse(event.data); + data["event"] = "message"; + + for (let i = 0; i < iframes.length; i++) { + iframes[i].contentWindow.postMessage(data, "*"); + } + }; + + socket.onclose = function (event) { + if (event.wasClean) { + console.warn( + `[close] Connection closed cleanly, code=${event.code} reason=${event.reason}` + ); + } else { + console.warn("[close] Connection died"); + } + }; + + socket.onerror = function (error) { + console.warn("error =>", error); + }; + + this.ws = socket; + } + + if (body !== undefined) { + const data = { + from: this.username /* Email if teacher, name if station */, + subject: subject, + body: body, + module: module_url, + }; + + if (this.ws) { + this.ws.send(JSON.stringify(data)); + } else { + await this.$axios.$post( + "/data/sendMessage/" + this.$store.state.class_.id, + data + ); + } + } }, }, }; diff --git a/server/data_web.ts b/server/data_web.ts index 85dab53..d89b730 100644 --- a/server/data_web.ts +++ b/server/data_web.ts @@ -6,6 +6,7 @@ import * as env from './env.ts' * Main in-memory data storage for all current classes on this instance */ const classes: data.LiveClasses = {} +const ws: Record = {} export const router = new oak.Router() /** @@ -496,6 +497,52 @@ export const router = new oak.Router() ctx.response.status = 200 }) + + .get('/wss/:class_id/:from', async (ctx) => { + const class_id = ctx?.params?.class_id + const from = ctx?.params?.from + + if (!ctx.isUpgradable) { + ctx.throw(501) + } + + const user_role = + classes[class_id]?.users[ctx.state.user]?.role || data.RoleName.Student + + if (from) { + const socket = ctx.upgrade() + + socket.onopen = () => {} + + socket.onmessage = (message) => { + message = JSON.parse(message.data) + + if ( + !class_id || + !data.validate_message(message, user_role) + //data.validate_email(message.from) || + //(!data.validate_email(message.from) && user_role == 'student') + ) { + return + } + + broadcastMessage(class_id, message) + } + + socket.onclose = () => { + console.log('Disconnected from client') + ws[from] = undefined + } + + socket.onerror = (e) => { + console.log('Error from client', e) + ws[from] = undefined + } + + ws[from] = socket + } + }) + /** * Broadcast message within a class room * @param message JSON formatted: {from, subject, body} @@ -606,3 +653,41 @@ function sendMessage(class_id: string, message: data.LiveMessage): boolean { return true } + +/** + * Broadcast message within a class + * @param class_id Only this class will be updated + * @param message Message to broadcase + * @returns Success + */ +function broadcastMessage(class_id: string, message: data.LiveMessage) { + const live_class = classes[class_id] + + if (!live_class) return false + + /* + const info = JSON.stringify(message) + + log.debug( + `Message to be sent (${class_id}) => ${ + info.length > 100 ? `${info.slice(0, 100)}...` : info + }` + ) + */ + + /* Don't send message if not in room in class */ + const user_from = live_class.users[message.from] + + if (!user_from) return true + + /* Send message to users within the target room */ + const user_conns_in_room = Object.entries(classes[class_id]?.users || []) + .filter((u) => u[1].room == user_from.room) + .flatMap((u) => u[0]) + + for (const user_id of user_conns_in_room) { + if (user_id !== message.from) { + ws[user_id].send(JSON.stringify(message)) + } + } +}