diff --git a/package-lock.json b/package-lock.json index 43258e1..09e6f05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@fails-components/config": "^1.3.0", - "@fails-components/data": "^1.2.0", - "@fails-components/security": "^1.3.2", + "@fails-components/data": "^1.4.0", + "@fails-components/security": "^1.4.1", "@socket.io/redis-adapter": "^7.1.0", "bson": "^4.7.0", "cidr-matcher": "^2.1.1", @@ -254,10 +254,9 @@ } }, "node_modules/@fails-components/data": { - "version": "1.2.0", - "resolved": "https://npm.pkg.github.com/download/@fails-components/data/1.2.0/9d035893b0ddf245b060891475390fc6075b65db", - "integrity": "sha512-r0LOYLeBSzJwrMEMHNxztXuBAGfh9y/w7ZTE76/qlq+gB+D0C3NcMSoTxda+SJmGYRalVPC7shipewpN0mE7GQ==", - "license": "AGPL-3.0-or-later", + "version": "1.4.0", + "resolved": "https://npm.pkg.github.com/download/@fails-components/data/1.4.0/024fbaa36ef7cc10e60fde6e80d4e65e3afaa155", + "integrity": "sha512-7/Ll9fqLrvrvI5mI4/GWGxnsh2oGtxsixCd5QWzqkIIsyQjH+5Kq+vp6wUz7bheeq8pexV45MeZ1ieqIF9KYhQ==", "dependencies": { "color": "^3.1.3" }, @@ -266,14 +265,14 @@ } }, "node_modules/@fails-components/security": { - "version": "1.3.2", - "resolved": "https://npm.pkg.github.com/download/@fails-components/security/1.3.2/d8a7478a088e79f3d371790686438dd4c350d1fd", - "integrity": "sha512-Moo7P66q+HzLaLd/nGTuU4wLYWTGJi9+1cWZpe9LyNW5U0MO34wkAJbQ9+zjHKHcDvYHa1Or3ejOU0wTJsEJqw==", - "license": "AGPL-3.0-or-later", + "version": "1.4.1", + "resolved": "https://npm.pkg.github.com/download/@fails-components/security/1.4.1/4b2a291ff0b44220dfe7756cbcd2a5ce172ab53f", + "integrity": "sha512-RKC6ovqR9GYtrKXshaOTOl6xWQqfW/MyCrY9vvfZJhzcPRIb6BGZQk90u6gbjbyAX7RkuyvZG0GSQkNj4VIEwQ==", "dependencies": { - "axios": "^1.2.1", + "@fastify/busboy": "^3.1.1", + "axios": "^1.7.4", "express-jwt": "^8.2.0", - "fast-xml-parser": "^4.0.13", + "fast-xml-parser": "^4.4.1", "jsonwebtoken": "^9.0.0", "redlock": "^4.2.0" }, @@ -281,6 +280,11 @@ "node": ">=16" } }, + "node_modules/@fastify/busboy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", + "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -945,9 +949,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -2710,9 +2714,9 @@ } }, "node_modules/npm-run-all/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "dependencies": { "nice-try": "^1.0.4", @@ -3983,25 +3987,31 @@ "integrity": "sha512-7ZRDk0owPaXy0npaF+gDrPgi+pNO9vJuD7rAuOQpyR+lf4Mo6cvo0nTvnSKY0SLIHQVYcgN6E+Y6ZnBrpGqAwg==" }, "@fails-components/data": { - "version": "1.2.0", - "resolved": "https://npm.pkg.github.com/download/@fails-components/data/1.2.0/9d035893b0ddf245b060891475390fc6075b65db", - "integrity": "sha512-r0LOYLeBSzJwrMEMHNxztXuBAGfh9y/w7ZTE76/qlq+gB+D0C3NcMSoTxda+SJmGYRalVPC7shipewpN0mE7GQ==", + "version": "1.4.0", + "resolved": "https://npm.pkg.github.com/download/@fails-components/data/1.4.0/024fbaa36ef7cc10e60fde6e80d4e65e3afaa155", + "integrity": "sha512-7/Ll9fqLrvrvI5mI4/GWGxnsh2oGtxsixCd5QWzqkIIsyQjH+5Kq+vp6wUz7bheeq8pexV45MeZ1ieqIF9KYhQ==", "requires": { "color": "^3.1.3" } }, "@fails-components/security": { - "version": "1.3.2", - "resolved": "https://npm.pkg.github.com/download/@fails-components/security/1.3.2/d8a7478a088e79f3d371790686438dd4c350d1fd", - "integrity": "sha512-Moo7P66q+HzLaLd/nGTuU4wLYWTGJi9+1cWZpe9LyNW5U0MO34wkAJbQ9+zjHKHcDvYHa1Or3ejOU0wTJsEJqw==", + "version": "1.4.1", + "resolved": "https://npm.pkg.github.com/download/@fails-components/security/1.4.1/4b2a291ff0b44220dfe7756cbcd2a5ce172ab53f", + "integrity": "sha512-RKC6ovqR9GYtrKXshaOTOl6xWQqfW/MyCrY9vvfZJhzcPRIb6BGZQk90u6gbjbyAX7RkuyvZG0GSQkNj4VIEwQ==", "requires": { - "axios": "^1.2.1", + "@fastify/busboy": "^3.1.1", + "axios": "^1.7.4", "express-jwt": "^8.2.0", - "fast-xml-parser": "^4.0.13", + "fast-xml-parser": "^4.4.1", "jsonwebtoken": "^9.0.0", "redlock": "^4.2.0" } }, + "@fastify/busboy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", + "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==" + }, "@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -4517,9 +4527,9 @@ } }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -5791,9 +5801,9 @@ }, "dependencies": { "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "requires": { "nice-try": "^1.0.4", diff --git a/package.json b/package.json index 1293f78..fdffa1f 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ ], "dependencies": { "@fails-components/config": "^1.3.0", - "@fails-components/data": "^1.2.0", - "@fails-components/security": "^1.3.2", + "@fails-components/data": "^1.4.0", + "@fails-components/security": "^1.4.1", "@socket.io/redis-adapter": "^7.1.0", "bson": "^4.7.0", "cidr-matcher": "^2.1.1", diff --git a/src/commonhandler.js b/src/commonhandler.js index 54bd57b..cc4c8f9 100644 --- a/src/commonhandler.js +++ b/src/commonhandler.js @@ -24,51 +24,58 @@ import { serialize as BSONserialize } from 'bson' import { createHash, webcrypto as crypto } from 'crypto' export class CommonConnection { - async getBgpdf(notepadscreenid) { + async getUsedAssets(notepadscreenid) { let lecturedoc = {} try { const lecturescol = this.mongo.collection('lectures') lecturedoc = await lecturescol.findOne( { uuid: notepadscreenid.lectureuuid }, { - projection: { _id: 0, backgroundpdfuse: 1, backgroundpdf: 1 } + projection: { + _id: 0, + usedpictures: 1, + backgroundpdfuse: 1, + backgroundpdf: 1, + usedipynbs: 1 + } } ) - // console.log("lecturedoc",lecturedoc); - if ( - !lecturedoc.backgroundpdfuse || - !lecturedoc.backgroundpdf || - !lecturedoc.backgroundpdf.sha - ) - return null - return this.getFileURL(lecturedoc.backgroundpdf.sha, 'application/pdf') - } catch (err) { - console.log('error in getBgpdf pictures', err) - } - } - async getUsedPicts(notepadscreenid) { - let lecturedoc = {} - try { - const lecturescol = this.mongo.collection('lectures') - lecturedoc = await lecturescol.findOne( - { uuid: notepadscreenid.lectureuuid }, - { - projection: { _id: 0, usedpictures: 1 } - } - ) - // console.log("lecturedoc",lecturedoc); - if (!lecturedoc.usedpictures) return [] - - return lecturedoc.usedpictures.map((el) => { - return { - name: el.name, - mimetype: el.mimetype, - sha: el.sha.buffer.toString('hex'), - url: this.getFileURL(el.sha.buffer, el.mimetype), - urlthumb: this.getFileURL(el.tsha.buffer, el.mimetype) - } - }) + return { + usedpict: + lecturedoc.usedpictures?.map?.((el) => { + return { + name: el.name, + mimetype: el.mimetype, + sha: el.sha.buffer.toString('hex'), + url: this.getFileURL(el.sha.buffer, el.mimetype), + urlthumb: this.getFileURL(el.tsha.buffer, el.mimetype) + } + }) || [], + bgpdf: + (lecturedoc.backgroundpdfuse && + lecturedoc.backgroundpdf && + lecturedoc.backgroundpdf.sha && + this.getFileURL(lecturedoc.backgroundpdf.sha, 'application/pdf')) || + null, + usedipynbs: + lecturedoc.usedipynbs?.map?.((el) => { + return { + name: el.name, + filename: el.filename, + /* note: el.note, */ + mimetype: el.mimetype, + id: el.id, + sha: el.sha.buffer.toString('hex'), + applets: el.applets?.map?.((applet) => ({ + appid: applet.appid, + appname: applet.appname + })), + url: this.getFileURL(el.sha.buffer, el.mimetype) + } + }) || [] + } + // ok now I have the picture, but I also have to generate the urls } catch (err) { console.log('error in getUsedPicts pictures', err) @@ -77,17 +84,25 @@ export class CommonConnection { async sendBoardsToSocket(lectureuuid, socket) { // we have to send first information about pictures - - const usedpict = await this.getUsedPicts({ lectureuuid: lectureuuid }) + // TODO use one mongo transaction + const { + usedpict = undefined, + bgpdf = null, + usedipynbs = undefined + } = await this.getUsedAssets({ + lectureuuid: lectureuuid + }) if (usedpict) { socket.emit('pictureinfo', usedpict) } - const bgpdf = await this.getBgpdf({ lectureuuid: lectureuuid }) if (bgpdf) { socket.emit('bgpdfinfo', { bgpdfurl: bgpdf }) } else { socket.emit('bgpdfinfo', { none: true }) } + if (usedipynbs) { + socket.emit('ipynbinfo', { ipynbs: usedipynbs }) + } try { const res = await this.redis.sMembers( diff --git a/src/notepadhandler.js b/src/notepadhandler.js index b03fe05..37bc72f 100644 --- a/src/notepadhandler.js +++ b/src/notepadhandler.js @@ -42,6 +42,7 @@ export class NoteScreenConnection extends CommonConnection { this.screenio = args.screenio this.notesio = args.notesio this.getFileURL = args.getFileURL + this.saveFile = args.saveFile this.signScreenJwt = args.signScreenJwt this.signNotepadJwt = args.signNotepadJwt @@ -442,6 +443,11 @@ export class NoteScreenConnection extends CommonConnection { callback(pictinfo) }) + socket.on('getAvailableIpynbs', async (callback) => { + const ipynbinfo = await this.getAvailableIpynbs(notepadscreenid) + callback(ipynbinfo) + }) + socket.on('getPolls', async (callback) => { const polls = await this.getPolls(notepadscreenid) callback(polls) @@ -469,6 +475,46 @@ export class NoteScreenConnection extends CommonConnection { } }) + socket.on('switchAppMaster', async (cmd) => { + const masterCommand = { appletMaster: socket.id } + this.notepadio + .to(notepadscreenid.roomname) + .emit('switchAppMaster', masterCommand) + this.screenio + .to(notepadscreenid.roomname) + .emit('switchAppMaster', masterCommand) + this.notesio + .to(notepadscreenid.roomname) + .emit('switchAppMaster', masterCommand) + }) + + socket.on( + 'uploadPicture', + async (name, type, picture, thumbnail, callback) => { + try { + if (typeof name !== 'string') + throw new Error('Type of name is not string') + if ( + typeof type !== 'string' || + !['image/jpeg', 'image/png'].includes(type) + ) + throw new Error('Type of type is not string') + if (!Buffer.isBuffer(picture) || !Buffer.isBuffer(thumbnail)) + throw new Error('Picture or thumbnail wrong type') + const { sha, tsha } = await this.uploadPicture(notepadscreenid, { + name, + type, + picture, + thumbnail + }) + callback({ sha, tsha, name }) + } catch (error) { + console.log('uploadPicture error', error) + callback({ error, name }) + } + } + ) + socket.on('drawcommand', async (cmd) => { await loadlectprom if (notepadscreenid) { @@ -491,6 +537,17 @@ export class NoteScreenConnection extends CommonConnection { } } } + if (cmd.task === 'startApp') { + const ipynbinfo = await this.getIpynb(notepadscreenid, cmd.id, cmd.sha) + if (ipynbinfo) { + const sendinfo = { ipynbs: ipynbinfo, appletMaster: socket.id } + this.notepadio + .to(notepadscreenid.roomname) + .emit('ipynbinfo', sendinfo) + this.screenio.to(notepadscreenid.roomname).emit('ipynbinfo', sendinfo) + this.notesio.to(notepadscreenid.roomname).emit('ipynbinfo', sendinfo) + } + } // generell distribution if (notepadscreenid.roomname) { this.notepadio.to(notepadscreenid.roomname).emit('drawcommand', cmd) @@ -901,6 +958,77 @@ export class NoteScreenConnection extends CommonConnection { } } + async getAvailableIpynbs(notepadscreenid) { + let lecturedoc = {} + try { + const lecturescol = this.mongo.collection('lectures') + lecturedoc = await lecturescol.findOne( + { uuid: notepadscreenid.lectureuuid }, + { + projection: { _id: 0, ipynbs: 1 } + } + ) + + if (!lecturedoc.ipynbs) return [] + + return lecturedoc.ipynbs.map((el) => { + return { + name: el.name, + filename: el.filename, + note: el.note, + id: el.id, + sha: el.sha.buffer.toString('hex'), + mimetype: el.mimetype, + /* sha: el.sha.buffer.toString('hex'), No download necessary ! */ + applets: el.applets?.map?.((applet) => ({ + appid: applet.appid, + appname: applet.appname + })), + url: this.getFileURL(el.sha.buffer, el.mimetype) + } + }) + // ok now I have the ipynb, but I also have to generate the urls + } catch (err) { + console.log('error in getAvailableIpynbs', err) + } + } + + async uploadPicture(notepadscreenid, { name, type, picture, thumbnail }) { + const picthash = createHash('sha256') + picthash.update(picture) + const thumbhash = createHash('sha256') + thumbhash.update(thumbnail) + + const sha = picthash.digest() + const tsha = thumbhash.digest() + + await Promise.all([ + this.saveFile(picture, sha, type, picture.length), + this.saveFile(thumbnail, tsha, type, thumbnail.length) + ]) + + const lecturescol = this.mongo.collection('lectures') + await lecturescol.updateOne( + { uuid: notepadscreenid.lectureuuid }, + { + $addToSet: { + usedpictures: { + name, + mimetype: type, + sha, + tsha + } + }, + $currentDate: { lastaccess: true } + } + ) + + return { + sha: sha.toString('hex'), + tsha: tsha.toString('hex') + } + } + async getPicture(notepadscreenid, id) { try { const lecturescol = this.mongo.collection('lectures') @@ -919,7 +1047,7 @@ export class NoteScreenConnection extends CommonConnection { ) if (findex === -1) { - if (!lecturedoc.pictures) throw new Error('No picture not found ' + id) + if (!lecturedoc.pictures) throw new Error('Pictures not found ' + id) // oh oh it is not found, but maybe it is available... const pindex = lecturedoc.pictures.findIndex( (el) => el.sha.buffer.toString('hex') === id @@ -955,6 +1083,64 @@ export class NoteScreenConnection extends CommonConnection { return null } + async getIpynb(notepadscreenid, id, sha) { + try { + const lecturescol = this.mongo.collection('lectures') + // first figure out if it already is assigned to the lecture, we use here mongo db instead of the redis cache + const lecturedoc = await lecturescol.findOne( + { uuid: notepadscreenid.lectureuuid }, + { + projection: { _id: 0, ipynbs: 1, usedipynbs: 1 } + } + ) + + if (!lecturedoc.usedipynbs) lecturedoc.usedipynbs = [] + + const findex = lecturedoc.usedipynbs.findIndex( + (el) => el.sha.buffer.toString('hex') === sha && el.id === id + ) + + if (findex === -1) { + if (!lecturedoc.ipynbs) throw new Error('No ipynbs found ' + id) + // oh oh it is not found, but maybe it is available... + const iindex = lecturedoc.ipynbs.findIndex( + (el) => el.sha.buffer.toString('hex') === sha && el.id === id + ) + if (iindex === -1) { + throw new Error('Ipynb not found ' + id) + } + const iinfo = lecturedoc.ipynbs[iindex] + // and now move it to the used inpnbs.... + lecturescol.updateOne( + { uuid: notepadscreenid.lectureuuid }, + { + $addToSet: { usedipynbs: iinfo }, + $currentDate: { lastaccess: true } + } + ) + lecturedoc.usedipynbs.push(iinfo) + } // else pinfo = lecturedoc.usedpictures[findex] + + return lecturedoc.usedipynbs.map((el) => { + return { + name: el.name, + note: el.note, + id: el.id, + mimetype: el.mimetype, + sha: el.sha.buffer.toString('hex'), + url: this.getFileURL(el.sha.buffer, el.mimetype), + applets: el.applets?.map?.((applet) => ({ + appid: applet.appid, + appname: applet.appname + })) + } + }, this) + } catch (err) { + console.log('error in getIpynb', err) + } + return null + } + async getPolls(notepadscreenid) { // TODO should be feed from mongodb diff --git a/src/server.js b/src/server.js index 9f9cd8d..b98e69b 100644 --- a/src/server.js +++ b/src/server.js @@ -76,6 +76,7 @@ const initServer = async () => { const assets = new FailsAssets({ datadir: cfg.getDataDir(), dataurl: cfg.getURL('data'), + savefile: cfg.getStatSaveType(), webservertype: cfg.getWSType(), privateKey: cfg.getStatSecret(), swift: cfg.getSwift(), @@ -150,6 +151,7 @@ const initServer = async () => { signScreenJwt: screensecurity.signToken, signNotepadJwt: lecturesecurity.signToken, signAvsJwt: avssecurity.signToken, + saveFile: assets.saveFile, getFileURL: assets.getFileURL, notepadhandlerURL: cfg.getURL('notepad'), screenUrl: targeturl,