├── index.js ├── .dockerignore ├── public ├── images │ ├── group.png │ ├── lamp.png │ ├── logo.png │ ├── add-icon.png │ ├── cursor.png │ ├── favicon.ico │ ├── favicon.png │ ├── question.png │ ├── tool-tip.png │ ├── brush-icon.png │ ├── clear-icon.png │ ├── endLecture.png │ ├── line-icon.png │ ├── pdf-image.png │ ├── undo-icon.png │ ├── aboutLecture.png │ ├── circle-icon.png │ ├── dowload-icon.png │ ├── eraser-icon.png │ ├── helpLecture.png │ ├── landingBanner.png │ ├── mobileBanner.png │ ├── pencil-icon.png │ ├── remove-icon.png │ ├── square-icon.png │ ├── test-webcam.png │ ├── triangle-icon.png │ ├── errorSession-en.png │ ├── errorSession-pt.png │ ├── liteboardCircle.png │ ├── liteboardSquare.png │ ├── createALecture-en.png │ ├── createALecture-pt.png │ ├── errorDontExist-en.png │ ├── errorDontExist-pt.png │ ├── errorInProgress-en.png │ ├── errorInProgress-pt.png │ ├── errorNotFound-en.png │ ├── errorNotFound-pt.png │ ├── github-readme-main.png │ ├── landingCluster-en.png │ ├── landingCluster-pt.png │ ├── liteboardTabOpen.png │ ├── liteboardTriangle.png │ ├── paint-bucket-icon.png │ ├── github-readme-main2.png │ ├── github-readme-stats.png │ ├── liteboardLandingBGdark.png │ ├── liteboardLandingBGlight.png │ ├── liteboardLandingCluster.png │ ├── whiteboardGraphicGradient.png │ ├── Guestlecturepage–background.png │ ├── thanksForUsingLiteboard-en.png │ ├── thanksForUsingLiteboard-pt.png │ ├── whiteboardGraphicGradientV2.png │ ├── liteboardLandingBGlightinverse.png │ ├── liteboardLandingBGlightinverse2.png │ ├── phone.svg │ ├── triangle.svg │ ├── dash.svg │ ├── width.svg │ ├── whiteboard.svg │ ├── shape.svg │ ├── uk.svg │ ├── portugal.svg │ ├── expand.svg │ ├── area.svg │ ├── SurveyMonkey_Logo.svg │ └── brazil.svg ├── audios │ └── notification.mp3 ├── js │ ├── classes │ │ ├── Point.js │ │ ├── Attachment.js │ │ ├── Board.js │ │ ├── Message.js │ │ ├── Fill.js │ │ ├── Chat.js │ │ └── Whiteboard.js │ ├── guest │ │ ├── guestBoards.js │ │ ├── guestChat.js │ │ ├── guestOptionsMenu.js │ │ ├── guest.js │ │ └── guestRTC.js │ ├── manager │ │ ├── managerChat.js │ │ ├── canvasTopMenu.js │ │ ├── manager.js │ │ ├── canvasActions.js │ │ ├── managerBoards.js │ │ ├── managerRTC.js │ │ └── paperTools.js │ ├── chatUtils.js │ ├── tools.js │ ├── create.js │ └── stats.js ├── css │ └── error404.css └── error.html ├── Dockerfile ├── server ├── models │ ├── manager.js │ ├── room.js │ └── stats.js ├── .eslintrc.js ├── services │ ├── logger │ │ ├── loggingMiddleware.js │ │ └── logger.js │ ├── sentry │ │ └── sentryConnection.js │ ├── i18n │ │ ├── i18n.js │ │ ├── i18n-en.json │ │ └── i18n-pt.json │ ├── credsGenerator.js │ └── emailer │ │ ├── emailer.js │ │ └── templates │ │ └── disconnectEmail.html ├── servers.js └── routes.js ├── .vscode └── launch.json ├── test ├── helpers │ └── apiHelpers.js ├── apiRequest.js └── apiTests.js ├── config ├── example.test.env ├── example.dev.env └── config.js ├── .github └── FUNDING.yml ├── docker └── docker-compose.yml ├── LICENSE ├── .circleci └── config.yml ├── nginx └── example_nginx.conf ├── package.json ├── .gitignore └── README.md /index.js: -------------------------------------------------------------------------------- 1 | require('./server/routes'); 2 | require('./server/subscriptions'); 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | 3 | node_modules 4 | 5 | test 6 | 7 | .circleci 8 | 9 | docker 10 | -------------------------------------------------------------------------------- /public/images/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/group.png -------------------------------------------------------------------------------- /public/images/lamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/lamp.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/images/add-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/add-icon.png -------------------------------------------------------------------------------- /public/images/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/cursor.png -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/question.png -------------------------------------------------------------------------------- /public/images/tool-tip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/tool-tip.png -------------------------------------------------------------------------------- /public/images/brush-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/brush-icon.png -------------------------------------------------------------------------------- /public/images/clear-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/clear-icon.png -------------------------------------------------------------------------------- /public/images/endLecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/endLecture.png -------------------------------------------------------------------------------- /public/images/line-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/line-icon.png -------------------------------------------------------------------------------- /public/images/pdf-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/pdf-image.png -------------------------------------------------------------------------------- /public/images/undo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/undo-icon.png -------------------------------------------------------------------------------- /public/audios/notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/audios/notification.mp3 -------------------------------------------------------------------------------- /public/images/aboutLecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/aboutLecture.png -------------------------------------------------------------------------------- /public/images/circle-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/circle-icon.png -------------------------------------------------------------------------------- /public/images/dowload-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/dowload-icon.png -------------------------------------------------------------------------------- /public/images/eraser-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/eraser-icon.png -------------------------------------------------------------------------------- /public/images/helpLecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/helpLecture.png -------------------------------------------------------------------------------- /public/images/landingBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/landingBanner.png -------------------------------------------------------------------------------- /public/images/mobileBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/mobileBanner.png -------------------------------------------------------------------------------- /public/images/pencil-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/pencil-icon.png -------------------------------------------------------------------------------- /public/images/remove-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/remove-icon.png -------------------------------------------------------------------------------- /public/images/square-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/square-icon.png -------------------------------------------------------------------------------- /public/images/test-webcam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/test-webcam.png -------------------------------------------------------------------------------- /public/images/triangle-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/triangle-icon.png -------------------------------------------------------------------------------- /public/images/errorSession-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/errorSession-en.png -------------------------------------------------------------------------------- /public/images/errorSession-pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/errorSession-pt.png -------------------------------------------------------------------------------- /public/images/liteboardCircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/liteboardCircle.png -------------------------------------------------------------------------------- /public/images/liteboardSquare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/liteboardSquare.png -------------------------------------------------------------------------------- /public/images/createALecture-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/createALecture-en.png -------------------------------------------------------------------------------- /public/images/createALecture-pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/createALecture-pt.png -------------------------------------------------------------------------------- /public/images/errorDontExist-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/errorDontExist-en.png -------------------------------------------------------------------------------- /public/images/errorDontExist-pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/errorDontExist-pt.png -------------------------------------------------------------------------------- /public/images/errorInProgress-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/errorInProgress-en.png -------------------------------------------------------------------------------- /public/images/errorInProgress-pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/errorInProgress-pt.png -------------------------------------------------------------------------------- /public/images/errorNotFound-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/errorNotFound-en.png -------------------------------------------------------------------------------- /public/images/errorNotFound-pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/errorNotFound-pt.png -------------------------------------------------------------------------------- /public/images/github-readme-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/github-readme-main.png -------------------------------------------------------------------------------- /public/images/landingCluster-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/landingCluster-en.png -------------------------------------------------------------------------------- /public/images/landingCluster-pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/landingCluster-pt.png -------------------------------------------------------------------------------- /public/images/liteboardTabOpen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/liteboardTabOpen.png -------------------------------------------------------------------------------- /public/images/liteboardTriangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/liteboardTriangle.png -------------------------------------------------------------------------------- /public/images/paint-bucket-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/paint-bucket-icon.png -------------------------------------------------------------------------------- /public/images/github-readme-main2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/github-readme-main2.png -------------------------------------------------------------------------------- /public/images/github-readme-stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/github-readme-stats.png -------------------------------------------------------------------------------- /public/images/liteboardLandingBGdark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/liteboardLandingBGdark.png -------------------------------------------------------------------------------- /public/images/liteboardLandingBGlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/liteboardLandingBGlight.png -------------------------------------------------------------------------------- /public/images/liteboardLandingCluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/liteboardLandingCluster.png -------------------------------------------------------------------------------- /public/images/whiteboardGraphicGradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/whiteboardGraphicGradient.png -------------------------------------------------------------------------------- /public/js/classes/Point.js: -------------------------------------------------------------------------------- 1 | export default class Point { 2 | constructor(x = 0, y = 0) { 3 | this.x = x; 4 | this.y = y; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /public/images/Guestlecturepage–background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/Guestlecturepage–background.png -------------------------------------------------------------------------------- /public/images/thanksForUsingLiteboard-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/thanksForUsingLiteboard-en.png -------------------------------------------------------------------------------- /public/images/thanksForUsingLiteboard-pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/thanksForUsingLiteboard-pt.png -------------------------------------------------------------------------------- /public/images/whiteboardGraphicGradientV2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/whiteboardGraphicGradientV2.png -------------------------------------------------------------------------------- /public/images/liteboardLandingBGlightinverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/liteboardLandingBGlightinverse.png -------------------------------------------------------------------------------- /public/images/liteboardLandingBGlightinverse2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeverd/lecture-experience/HEAD/public/images/liteboardLandingBGlightinverse2.png -------------------------------------------------------------------------------- /public/js/classes/Attachment.js: -------------------------------------------------------------------------------- 1 | export default function Attachment(fileContent, name, type) { 2 | this.file = fileContent; 3 | this.name = name; 4 | this.type = type; 5 | } 6 | -------------------------------------------------------------------------------- /public/js/classes/Board.js: -------------------------------------------------------------------------------- 1 | export default function Board(pathsData, image, zoom) { 2 | this.pathsData = pathsData; 3 | this.image = image; 4 | this.zoom = zoom; 5 | this.centerView = null; 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | 10 | COPY . . 11 | 12 | 13 | EXPOSE 8080 14 | 15 | 16 | CMD ["npm", "start"] 17 | -------------------------------------------------------------------------------- /public/js/classes/Message.js: -------------------------------------------------------------------------------- 1 | export default function Message(content, file, sender, color) { 2 | this.color = color; 3 | this.sender = sender; 4 | this.content = content; 5 | this.attachment = file; 6 | } 7 | -------------------------------------------------------------------------------- /server/models/manager.js: -------------------------------------------------------------------------------- 1 | class Manager { 2 | constructor(roomId, email, socketId = null) { 3 | this.roomId = roomId; 4 | this.email = email; 5 | this.socketId = socketId; 6 | } 7 | } 8 | 9 | module.exports = Manager; 10 | -------------------------------------------------------------------------------- /server/models/room.js: -------------------------------------------------------------------------------- 1 | class Room { 2 | constructor(name, managerId, lectureTools) { 3 | this.name = name; 4 | this.managerId = managerId; 5 | this.boards = []; 6 | this.boardActive = 0; 7 | this.lectureTools = lectureTools; 8 | } 9 | } 10 | 11 | module.exports = Room; 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Run Debug express", 8 | "skipFiles": [ 9 | "/**" 10 | ], 11 | "program": "${workspaceFolder}/index.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: [ 7 | 'airbnb-base', 8 | ], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly', 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 11, 15 | sourceType: 'module', 16 | }, 17 | rules: { 18 | "linebreak-style": ["error", (process.platform === "win32" ? "windows" : "unix")], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /test/helpers/apiHelpers.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | 3 | 4 | const createLecture = (cb) => { 5 | const options = { 6 | uri: 'http://localhost:8080/create', // note this is hardcoded in change this afterwards 7 | method: 'POST', 8 | json: { 9 | email: 'testing123@gmail.com', 10 | }, 11 | }; 12 | request(options, (err, response, body) => cb(err, response, body)); 13 | }; 14 | 15 | 16 | module.exports = { 17 | createLecture, 18 | }; 19 | -------------------------------------------------------------------------------- /server/services/logger/loggingMiddleware.js: -------------------------------------------------------------------------------- 1 | const morgan = require('morgan'); // http logging middleware 2 | const { logger } = require('./logger'); 3 | 4 | 5 | logger.stream = { 6 | write: (message) => logger.info(message.substring(0, message.lastIndexOf('\n'))), 7 | }; 8 | 9 | 10 | module.exports = { 11 | 12 | logMiddleWare: morgan( 13 | ':remote-addr - :remote-user [:date[clf]] ":method :url" :status :res[content-length]', 14 | { stream: logger.stream }, 15 | ), 16 | }; 17 | -------------------------------------------------------------------------------- /server/models/stats.js: -------------------------------------------------------------------------------- 1 | class Stats { 2 | constructor(lectureName, userTracker = [], maxNumOfUsers = 0, numOfBoards = 0) { 3 | this.userTracker = userTracker; 4 | this.maxNumOfUsers = maxNumOfUsers; 5 | this.numOfBoards = numOfBoards; 6 | this.lectureName = lectureName; 7 | } 8 | 9 | addUserTrack(time, numberOfUser) { 10 | const userTrack = { 11 | time, 12 | numberOfUser, 13 | }; 14 | this.userTracker.push(userTrack); 15 | if (numberOfUser > this.maxNumOfUsers) { 16 | this.maxNumOfUsers = numberOfUser; 17 | } 18 | } 19 | } 20 | 21 | module.exports = Stats; 22 | -------------------------------------------------------------------------------- /public/css/error404.css: -------------------------------------------------------------------------------- 1 | .centered { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | text-align: center; 7 | } 8 | 9 | #error404 { 10 | min-width: 250px; 11 | max-width: 850px; 12 | width: 80%; 13 | height: auto; 14 | } 15 | 16 | .view-container .footer { 17 | position: absolute; 18 | left: 50%; 19 | transform: translateX(-50%); 20 | } 21 | 22 | .buttons { 23 | margin-bottom: 50px; 24 | margin-top: 10px; 25 | width: 100%; 26 | background-color: transparent; 27 | display: flex; 28 | flex-direction: row; 29 | } 30 | 31 | @media only screen and (max-width: 1024px) { 32 | #error404 { 33 | width: 70%; 34 | } 35 | } -------------------------------------------------------------------------------- /config/example.test.env: -------------------------------------------------------------------------------- 1 | # environment 2 | NODE_ENV=DEVELOPMENT 3 | 4 | 5 | # redis configs 6 | 7 | #REDIS_HOST=127.0.0.1 add this below to REDIS_URL, instead. 8 | 9 | #if this is updated, update it in redis url as well!! 10 | REDIS_PORT=6379 11 | 12 | REDIS_URL=redis://127.0.0.1:6379 13 | 14 | # express configs 15 | 16 | EXPRESS_PORT=8080 17 | 18 | # express sessions 19 | 20 | SESSION_SECRET=secret 21 | 22 | SESSION_NAME=session 23 | 24 | 25 | # email configuration, please add your own creds. 26 | 27 | EMAIL=testing123@gmail.com 28 | 29 | EMAIL_PASSWORD=test123 30 | 31 | EMAIL_SERVICE=Gmail 32 | 33 | 34 | # logs 35 | 36 | LOGGER=true 37 | 38 | 39 | 40 | 41 | # turn server configs 42 | 43 | TURN_SERVER_ACTIVE=false 44 | 45 | DEBUG_CLIENT=false 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: [ 13 | "https://paypal.me/liteboard" 14 | ] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /public/js/guest/guestBoards.js: -------------------------------------------------------------------------------- 1 | 2 | import { displayImagePopUpOnClick } from '../utility.js'; 3 | 4 | export default function setNonActiveBoards(boards) { 5 | if ($('#boards-view').hasClass('active-menu-item') && boards.length === 0) { 6 | $('#toggle-boards-view').click(); 7 | } 8 | const boardsDiv = document.getElementById('non-active-boards'); 9 | boardsDiv.innerHTML = ''; 10 | boards.forEach((board, i) => { 11 | const outer = document.createElement('div'); 12 | outer.classList.add('non-active-board-wrapper'); 13 | const imgElem = document.createElement('img'); 14 | imgElem.src = board; 15 | $(imgElem).attr('data-name', `board${i}.png`); 16 | imgElem.onclick = displayImagePopUpOnClick; 17 | outer.appendChild(imgElem); 18 | boardsDiv.appendChild(outer); 19 | }); 20 | document.querySelector('#num-boards').innerHTML = boards.length; 21 | } 22 | -------------------------------------------------------------------------------- /public/images/phone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | redis_db: 4 | image: redis:4.0-alpine 5 | command: redis-server --requirepass REDIS_PASSWORD_IMPORTANT_KEEP_SAFE 6 | ports: 7 | - 6379:6379 8 | volumes: 9 | - ./docker-config/redis.conf:/redis.conf 10 | command: [ "redis-server", "/redis.conf" ] 11 | janus-gateway: 12 | image: canyan/janus-gateway:master_88df9449ac54f27afa29672cf092fac65a695c29 13 | command: ["/usr/local/bin/janus", "-F", "/usr/local/etc/janus"] 14 | ports: 15 | - "8088:8088" 16 | - "8089:8089" 17 | - "8889:8889" 18 | - "8000:8000" 19 | - "7088:7088" 20 | - "7089:7089" 21 | - "8188:8188" 22 | volumes: 23 | - ./docker-config/janus/janus.jcfg:/usr/local/etc/janus/janus.jcfg # might need to change this later, dk if this is best practice .... TODO 24 | restart: always 25 | -------------------------------------------------------------------------------- /public/images/triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /server/services/sentry/sentryConnection.js: -------------------------------------------------------------------------------- 1 | const Sentry = require('@sentry/node'); 2 | // or use es6 import statements 3 | // import * as Sentry from '@sentry/node'; 4 | 5 | // This is required since it patches functions on the hub 6 | const Apm = require('@sentry/apm'); 7 | // or use es6 import statements 8 | // import * as Apm from '@sentry/apm'; 9 | 10 | Sentry.init({ 11 | dsn: process.env.SENTRY_DSN || 'test', // make sure to source .env file, before running this, alternatively maunally add it in here, remove prior to commiting. 12 | tracesSampleRate: 1.0, // Be sure to lower this in production 13 | }); 14 | 15 | // Your test code to verify it works 16 | 17 | const transaction = Sentry.startTransaction({ 18 | op: 'test', 19 | name: 'My First Test Transaction', 20 | }); 21 | 22 | setTimeout(() => { 23 | try { 24 | foo(); 25 | } catch (e) { 26 | Sentry.captureException(e); 27 | } finally { 28 | transaction.finish(); 29 | } 30 | }, 99); 31 | -------------------------------------------------------------------------------- /public/images/dash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /server/services/i18n/i18n.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { supportedLanguages, defaultLanguage } = require('../../../config/config'); 5 | 6 | const portuguese = JSON.parse(fs.readFileSync(path.resolve(__dirname, './i18n-pt.json'), 'utf8')); 7 | const english = JSON.parse(fs.readFileSync(path.resolve(__dirname, './i18n-en.json'), 'utf8')); 8 | 9 | const langCodeMap = {}; 10 | supportedLanguages.forEach((langCode) => { 11 | langCodeMap[langCode] = langCode.includes('pt') ? portuguese : english; 12 | }); 13 | 14 | 15 | function setLanguage(session, language) { 16 | const langToSet = language in langCodeMap ? language : defaultLanguage; 17 | session.lang = langToSet; 18 | } 19 | 20 | function getLanguage(session, locale = 'en-US') { 21 | const langToUse = 'lang' in session ? session.lang : locale; 22 | return { lang: langCodeMap[langToUse] }; 23 | } 24 | 25 | module.exports = { setLanguage, getLanguage }; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Liteboard.io 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | build: 3 | docker: 4 | - image: circleci/node:10.15.0 5 | steps: 6 | - checkout 7 | - setup_remote_docker 8 | - run: | 9 | TAG=0.1.3$CIRCLE_BUILD_NUM_$CIRCLE_BRANCH 10 | docker build -t jeverd/liteboard-express:$TAG . 11 | echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin # (4) 12 | docker push jeverd/liteboard-express:$TAG 13 | tests: 14 | docker: 15 | - image: circleci/node:10.15.0 16 | - image: redis 17 | steps: 18 | - checkout 19 | - setup_remote_docker 20 | - run: 21 | name: 'Running tests' 22 | command: | 23 | echo 'running npm install' 24 | npm install 25 | cd config 26 | mv example.test.env .env 27 | echo 'now running tests' 28 | cd .. 29 | npm test 30 | 31 | workflows: 32 | version: 2 33 | main: 34 | jobs: 35 | - build: 36 | filters: 37 | branches: 38 | only: 39 | - staging 40 | - master 41 | - tests 42 | 43 | -------------------------------------------------------------------------------- /test/apiRequest.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | 3 | 4 | // method = "GET", "POST", "PATCH" ....etc 5 | // endpoint = refers to various endpoints in the api. 6 | // data, the data you want to send, usually for post/patch request, 7 | // cb = CALLBACK 8 | // query, used for get requests 9 | 10 | 11 | // TODO Add more capability for types of request, these are the most basics ones 12 | 13 | const jobSearchRequest = function (method, endpoint, data, query, cb) { // add capability for doing get requests. 14 | // /FOR POST REQUEST ONLY 15 | if (method == 'POST' || method == 'PATCH') { 16 | var options = { 17 | uri: `http://localhost:3000/api${endpoint}`, // note this is hardcoded in change this afterwards 18 | method, 19 | json: data, 20 | }; 21 | 22 | request(options, (err, response, body) => cb(err, response, body)); 23 | } 24 | // FOR GET REQUESTS 25 | if (method == 'GET') { 26 | var options = { 27 | uri: `http://localhost:3000/api${endpoint}`, // note this is hardcoded in change this afterwards 28 | qs: query, 29 | method, 30 | }; 31 | request(options, (err, response, body) => cb(err, response, body)); 32 | } 33 | }; 34 | 35 | 36 | module.exports = jobSearchRequest; 37 | -------------------------------------------------------------------------------- /server/services/credsGenerator.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | const turnCredsGenerator = (name, secret) => { // https://stackoverflow.com/questions/35766382/coturn-how-to-use-turn-rest-api 4 | const unixTimeStamp = parseInt(Date.now() / 1000, 10) + 24 * 3600; 5 | const username = [unixTimeStamp, name].join(':'); 6 | let password; 7 | const hmac = crypto.createHmac('sha1', secret); 8 | hmac.setEncoding('base64'); 9 | hmac.write(username); 10 | hmac.end(); 11 | // eslint-disable-next-line prefer-const 12 | password = hmac.read(); 13 | return { 14 | username, 15 | password, 16 | }; 17 | }; 18 | 19 | const janusCredsGenerator = (data = [], secret, timeout = 24 * 60 * 60) => { // https://janus.conf.meetecho.com/docs/auth.html 20 | const expiry = Math.floor(Date.now() / 1000) + timeout; 21 | const strdata = [expiry.toString(), 'janus', ...data].join(','); // relm is always janus 22 | const hmac = crypto.createHmac('sha1', secret); 23 | hmac.setEncoding('base64'); 24 | hmac.write(strdata); 25 | hmac.end(); 26 | 27 | return [strdata, hmac.read()].join(':'); // format ,janus,[,plugin2...]: 28 | }; 29 | 30 | 31 | module.exports = { 32 | turnCredsGenerator, 33 | janusCredsGenerator, 34 | }; 35 | -------------------------------------------------------------------------------- /public/images/width.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /nginx/example_nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | events { 3 | worker_connections 4096; ## Default: 1024 4 | } 5 | 6 | http { 7 | server { 8 | listen 80 default_server; 9 | 10 | server_name _; 11 | 12 | return 301 https://$host$request_uri; 13 | 14 | } 15 | 16 | server { 17 | server_name mydomain.com; 18 | listen 443 ssl; 19 | listen [::]:443 ssl; 20 | 21 | location / { 22 | proxy_pass http://app:8080/; #whatever port your app runs on, do proper env vars 23 | proxy_http_version 1.1; 24 | proxy_set_header X-Forwarded-Proto https; 25 | proxy_set_header Upgrade $http_upgrade; 26 | proxy_set_header Connection 'upgrade'; 27 | proxy_set_header Host $host; 28 | proxy_cache_bypass $http_upgrade; 29 | } 30 | 31 | location /janus { 32 | proxy_pass http://janus:8088/janus; #whatever port your app runs on, do proper env vars 33 | proxy_http_version 1.1; 34 | proxy_set_header X-Forwarded-Proto https; 35 | proxy_set_header Upgrade $http_upgrade; 36 | proxy_set_header Connection 'upgrade'; 37 | proxy_set_header Host $host; 38 | proxy_cache_bypass $http_upgrade; 39 | } 40 | 41 | #ssl_certificate /path/to/ssl_certificate; 42 | #ssl_certificate_key /path/to/ssl_certificate; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /config/example.dev.env: -------------------------------------------------------------------------------- 1 | # environment 2 | NODE_ENV=DEVELOPMENT 3 | 4 | 5 | # redis configs 6 | 7 | #REDIS_HOST=127.0.0.1 add this below to REDIS_URL, instead. 8 | 9 | #if this is updated, update it in redis url as well!! 10 | REDIS_PORT=6379 11 | 12 | REDIS_URL=redis://REDIS_PASSWORD_IMPORTANT_KEEP_SAFE@127.0.0.1:6379 13 | 14 | # express configs 15 | 16 | EXPRESS_PORT=8080 17 | 18 | # express sessions 19 | 20 | SESSION_SECRET=secret 21 | 22 | SESSION_NAME=session 23 | 24 | 25 | # email configuration, please add your own creds. 26 | 27 | EMAIL_USERNAME=testing123@gmail.com 28 | 29 | #when using gmail email_username and this are the same, but for other platforms it isn't hence the reason for 2 different vars 30 | EMAIL_SENDER=testing123@gmail.com 31 | 32 | 33 | EMAIL_PASSWORD=test123 34 | 35 | EMAIL_SERVICE=Gmail 36 | 37 | 38 | # logs 39 | 40 | LOGGER=true 41 | 42 | 43 | 44 | 45 | # turn server configs 46 | 47 | TURN_SERVER_ACTIVE=false 48 | 49 | 50 | 51 | #janus Configs 52 | 53 | #if you change this make sure in your janus config file you update it there as well. 54 | JANUS_SERVER_SECRET=janus 55 | 56 | 57 | 58 | # sentry configs - make a sentry account and then use the sentryConnection script to connect to sentry...they will give you DSN 59 | SENTRY_DSN=empty 60 | SENTRY_ENVIRONMENT=dev 61 | 62 | 63 | 64 | # lang Configs 65 | DEFAULT_LANGUAGE=en-US 66 | 67 | DEBUG_CLIENT=false 68 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | const path = require('path'); 3 | 4 | const SUPPORTED_LANGUAGES = [ 5 | 'pt', 'pt-BR', 'pt-PT', 'en-US', 6 | ]; 7 | 8 | dotenv.config({ path: path.resolve(__dirname, './.env') }); // requires providing full path, due to some issues with dotenv and node versions 9 | 10 | module.exports = { 11 | redisPort: process.env.REDIS_PORT, 12 | expressPort: process.env.EXPRESS_PORT, 13 | environment: process.env.NODE_ENV, 14 | ptHost: process.env.PT_HOST, 15 | ptPort: process.env.PT_PORT, 16 | redisUrl: process.env.REDIS_URL, 17 | emailUsername: process.env.EMAIL_USERNAME, 18 | emailSender: process.env.EMAIL_SENDER, 19 | emailPassword: process.env.EMAIL_PASSWORD, 20 | emailService: process.env.EMAIL_SERVICE, 21 | loggerFlag: (process.env.LOGGER === 'true'), 22 | sessionSecret: process.env.SESSION_SECRET, 23 | sessionName: process.env.SESSION_NAME, 24 | redisTurnDbNumber: process.env.REDIS_TURN_DB_NUMBER, 25 | turnServerSecret: process.env.TURN_SERVER_SECRET, 26 | turnServerPort: process.env.TURN_SERVER_PORT, 27 | turnServerActive: (process.env.TURN_SERVER_ACTIVE === 'true'), 28 | turnServerUrl: process.env.TURN_SERVER_URL, 29 | defaultLanguage: process.env.DEFAULT_LANGUAGE, 30 | supportedLanguages: SUPPORTED_LANGUAGES, 31 | sentryDSN: process.env.SENTRY_DSN, 32 | sentryEnvironment: process.env.SENTRY_ENVIRONMENT, 33 | janusServerSecret: process.env.JANUS_SERVER_SECRET, 34 | debugClient: process.env.DEBUG_CLIENT, 35 | }; 36 | -------------------------------------------------------------------------------- /public/images/whiteboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/js/guest/guestChat.js: -------------------------------------------------------------------------------- 1 | import Message from '../classes/Message.js'; 2 | import Chat from '../classes/Chat.js'; 3 | import { getRandomColor } from '../utility.js'; 4 | import initializeChat from '../chatUtils.js'; 5 | 6 | const sendContainer = document.getElementById('send-container'); 7 | const messageInput = document.getElementById('message-input'); 8 | const fileInput = document.getElementById('file-input'); 9 | 10 | 11 | export default function initializeGuestChat(socket, roomId, name) { 12 | const chat = new Chat('message-container'); 13 | socket.emit('send-to-room', roomId, { joined: name }); 14 | socket.on('send-to-room', (message) => { 15 | chat.appendMessage(message, true); 16 | if (!$('div.chat').hasClass('active-menu-item')) { 17 | const currNumOfUnread = parseInt(document.querySelector('#num-unread-messages').innerText); 18 | $('#num-unread-messages').html(currNumOfUnread + 1); 19 | if ($('.mute-unmute-chat').hasClass('fa-volume-up')) { 20 | playSound('/notification.mp3'); 21 | } 22 | } 23 | }); 24 | 25 | initializeChat(chat); 26 | 27 | const guestChatColor = getRandomColor(); 28 | sendContainer.addEventListener('submit', (e) => { 29 | e.preventDefault(); 30 | const messageContent = messageInput.value.trim(); 31 | if (messageContent !== '' || chat.preview !== null) { 32 | const message = new Message(messageContent, chat.preview, name, guestChatColor); 33 | socket.emit('send-to-room', roomId, message); 34 | chat.appendMessage(message, false); 35 | messageInput.value = ''; 36 | fileInput.value = ''; 37 | chat.preview = null; 38 | } 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /public/images/shape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 12 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/js/manager/managerChat.js: -------------------------------------------------------------------------------- 1 | import Message from '../classes/Message.js'; 2 | import Chat from '../classes/Chat.js'; 3 | import initializeChat from '../chatUtils.js'; 4 | import { playSound } from '../utility.js'; 5 | 6 | export default function initializeManagerChat(socket, roomId) { 7 | const chat = new Chat('message-container'); 8 | socket.emit('send-to-room', roomId, { joined: $('#host-name-chat').val() }); 9 | const sendContainer = document.getElementById('send-container'); 10 | const messageInput = document.getElementById('message-input'); 11 | const fileInput = document.getElementById('file-input'); 12 | socket.on('send-to-room', (message) => { 13 | chat.appendMessage(message, true); 14 | const messagesDiv = $('div.messages'); 15 | if (!messagesDiv.hasClass('active-chat')) { 16 | chat.unreadCount += 1; 17 | $('.new-messages-badge').html(chat.unreadCount); 18 | if ($('.mute-unmute-chat').hasClass('fa-volume-up')) { 19 | playSound('/notification.mp3'); 20 | } 21 | } 22 | }); 23 | 24 | initializeChat(chat); 25 | 26 | $('#toggle-messages').click((e) => { 27 | e.preventDefault(); 28 | const messagesDiv = $('div.messages'); 29 | messagesDiv.toggleClass('active-chat'); 30 | if (messagesDiv.hasClass('active-chat')) { 31 | chat.unreadCount = 0; 32 | $('.new-messages-badge').html(chat.unreadCount); 33 | } 34 | }); 35 | 36 | sendContainer.addEventListener('submit', (e) => { 37 | e.preventDefault(); 38 | const messageContent = messageInput.value.trim(); 39 | if (messageContent !== '' || chat.preview !== null) { 40 | const message = new Message(messageContent, chat.preview, $('#host-name-chat').val(), 'rgba(70, 194, 255, 1)'); 41 | socket.emit('send-to-room', roomId, message); 42 | chat.appendMessage(message, false); 43 | messageInput.value = ''; 44 | fileInput.value = ''; 45 | chat.preview = null; 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lecture-experience", 3 | "version": "1.0.0", 4 | "description": "Liteboard's website Repository", 5 | "main": "servers.js", 6 | "scripts": { 7 | "debug:client": "set DEBUG_CLIENT=true && nodemon -e html,json index.js", 8 | "debug:server": "nodemon -e html,json,js index.js", 9 | "start": "node index.js", 10 | "test": "set LOGGER=false && mocha --exit" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/jeverd/lecture-experience.git" 15 | }, 16 | "keywords": [], 17 | "author": "Liteboard", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/jeverd/lecture-experience/issues" 21 | }, 22 | "homepage": "https://github.com/jeverd/lecture-experience#readme", 23 | "dependencies": { 24 | "@sentry/apm": "^5.19.1", 25 | "@sentry/node": "^5.19.1", 26 | "connect-redis": "^4.0.4", 27 | "dotenv": "^8.2.0", 28 | "express": "^4.17.1", 29 | "express-session": "^1.17.1", 30 | "express-socket.io-session": "^1.3.5", 31 | "helmet": "^3.22.0", 32 | "locale": "^0.1.0", 33 | "morgan": "^1.10.0", 34 | "mustache": "^4.0.1", 35 | "mustache-express": "^1.3.0", 36 | "nodemailer": "^6.4.6", 37 | "nodemon": "^2.0.4", 38 | "redis": "^3.0.2", 39 | "socket.io": "^2.3.0", 40 | "url": "^0.11.0", 41 | "uuid": "^8.0.0", 42 | "winston": "^3.2.1", 43 | "winston-papertrail": "git+https://github.com/kenperkins/winston-papertrail.git#v2", 44 | "winston-sentry-log": "^1.0.16" 45 | }, 46 | "devDependencies": { 47 | "chai": "^4.2.0", 48 | "eslint": "^6.8.0", 49 | "eslint-config-airbnb": "^18.1.0", 50 | "eslint-config-airbnb-base": "^14.1.0", 51 | "eslint-plugin-import": "^2.20.2", 52 | "eslint-plugin-jsx-a11y": "^6.2.3", 53 | "eslint-plugin-react": "^7.19.0", 54 | "eslint-plugin-react-hooks": "^2.5.0", 55 | "mocha": "^7.2.0", 56 | "request": "^2.88.2", 57 | "socket.io-client": "^2.3.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/services/emailer/emailer.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | const Mustache = require('mustache'); 3 | const fs = require('fs'); 4 | const { 5 | emailUsername, 6 | emailSender, 7 | emailPassword, 8 | emailService, 9 | environment, 10 | } = require('../../../config/config'); 11 | const { logger } = require('../logger/logger'); 12 | 13 | 14 | const transporter = nodemailer.createTransport({ 15 | service: emailService, 16 | auth: { 17 | user: emailUsername, 18 | pass: emailPassword, 19 | }, 20 | }); 21 | 22 | 23 | const readHtmlFile = (path, cb) => { 24 | fs.readFile(path, { encoding: 'utf-8' }, (err, html) => { 25 | if (err) { 26 | cb(err); 27 | } else { 28 | cb(null, html); 29 | } 30 | }); 31 | }; 32 | 33 | 34 | const sendEmail = (toEmail, subject, htmlBody) => { 35 | const fromEmailFormat = `✏️ Liteboard.io <${emailSender}>`; 36 | const mailOption = { 37 | from: fromEmailFormat, 38 | to: toEmail, 39 | subject, 40 | html: htmlBody, 41 | }; 42 | transporter.sendMail(mailOption, (err) => { 43 | if (err) logger.info(`EMAIL: Failed to send email to: ${toEmail}, error: ${err}`); 44 | else logger.info(`EMAIL: Successfully sent email to ${toEmail}`); 45 | }); 46 | }; 47 | 48 | // eslint-disable-next-line max-len 49 | const sendManagerDisconnectEmail = (toEmail, id, lang) => { // doesn't matter because its async, since user disconnected 50 | const subject = 'You disconnected from your lecture, heres your link'; 51 | const host = environment === 'DEVELOPMENT' ? 'http://localhost:8080' : 'https://liteboard.io'; 52 | const link = `${host}/lecture/${id}`; 53 | // now we get the html file 54 | readHtmlFile(`${__dirname}/templates/disconnectEmail.html`, (err, html) => { 55 | if (err) { 56 | logger.error('EMAIL: issue reading html file '); 57 | } else { 58 | const renderedHtml = Mustache.render(html, { link, ...lang }); 59 | sendEmail(toEmail, subject, renderedHtml); 60 | } 61 | }); 62 | }; 63 | 64 | module.exports = { 65 | sendEmail, 66 | sendManagerDisconnectEmail, 67 | }; 68 | -------------------------------------------------------------------------------- /server/services/logger/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const { PapertrailTransport, PapertrailConnection } = require('winston-papertrail'); 3 | const Sentry = require('winston-sentry-log'); 4 | 5 | const { format } = winston; 6 | const { 7 | combine, timestamp, printf, colorize, 8 | } = format; 9 | 10 | const { 11 | environment, ptPort, ptHost, loggerFlag, sentryDSN, 12 | } = require('../../../config/config'); 13 | 14 | const consoleFormat = printf(({ 15 | // eslint-disable-next-line no-shadow 16 | level, message, timestamp, 17 | }) => `${timestamp}: ${level}: ${message}`); 18 | 19 | 20 | const consoleLogger = new winston.transports.Console({ 21 | level: 'debug', 22 | format: combine( 23 | colorize(), 24 | timestamp(), 25 | consoleFormat, 26 | ), 27 | silent: !loggerFlag, 28 | }); 29 | 30 | 31 | const loggerTransports = [consoleLogger]; 32 | 33 | if (environment === 'PRODUCTION' || environment === 'STAGING') { 34 | // sentry logs 35 | 36 | const options = { 37 | config: { 38 | dsn: sentryDSN, 39 | }, 40 | level: 'error', 41 | }; 42 | 43 | // eslint-disable-next-line new-cap 44 | const sentryLogger = new winston.createLogger({ 45 | transports: [new Sentry(options)], 46 | }); 47 | 48 | loggerTransports.push(sentryLogger); 49 | 50 | const papertrailConnection = new PapertrailConnection({ 51 | host: ptHost, 52 | port: ptPort, 53 | }); 54 | 55 | const winstonPapertrail = new PapertrailTransport(papertrailConnection, { 56 | inlineMeta: true, 57 | level: 'debug', 58 | logFormat(level, message) { 59 | // eslint-disable-next-line prefer-template 60 | return '[' + level + ']: ' + message; 61 | }, 62 | colorize: true, 63 | }); 64 | loggerTransports.push(winstonPapertrail); 65 | } 66 | 67 | const logger = winston.createLogger({ 68 | format: winston.format.combine( 69 | winston.format.timestamp({ 70 | format: 'YYYY-MM-DD HH:mm:ss', 71 | }), 72 | winston.format.simple(), 73 | ), 74 | transports: loggerTransports, 75 | }); 76 | 77 | module.exports = { 78 | logger, 79 | }; 80 | -------------------------------------------------------------------------------- /public/images/uk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # letsentrcpy 101 | docker/letsencrypt/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | /.history 109 | 110 | .DS_Store 111 | 112 | # ignoring .env file 113 | config/.env 114 | 115 | #janus config file 116 | /docker/docker-config/janus/janus.jcfg 117 | /docker/docker-config/janus.jcfg 118 | /docker/docker-config/turnserver.conf 119 | /docker/docker-config/example_turnserver.conf 120 | /docker/docker-config/janus/janus.transport.http.jcfg 121 | /docker/docker-config/nginx.conf 122 | config/.env 123 | /docker/production.yml; 124 | 125 | -------------------------------------------------------------------------------- /public/js/guest/guestOptionsMenu.js: -------------------------------------------------------------------------------- 1 | import { showInfoMessage, toggleSpeakers } from '../utility.js'; 2 | 3 | function handleOptionClick() { 4 | const targetId = this.getAttribute('target-id'); 5 | const targetElem = $(`#${targetId}`); 6 | if (targetId === 'boards-view' && $('#num-boards').html() === '0') { 7 | showInfoMessage($('#no-other-boards-text').val()); 8 | return; 9 | } 10 | const activeOptionClass = 'active-menu-item'; 11 | const activeOptionButtonClass = 'active-menu-item-button'; 12 | if (targetElem.hasClass(activeOptionClass)) { 13 | targetElem.hide(); 14 | targetElem.removeClass(activeOptionClass); 15 | $(this).removeClass(activeOptionButtonClass); 16 | } else { 17 | if (targetId === 'chat-view') { 18 | $('#num-unread-messages').html(0); 19 | $('.message-lecture-name').css('opacity', 0); 20 | $('.message-content').css('opacity', 0); 21 | $('.minimize-chat-view').css('opacity', 0); 22 | } else if (targetId === 'boards-view') { 23 | $('.non-active-boards-title').css('opacity', 0); 24 | } 25 | $(`.${activeOptionClass}`).hide(); 26 | $(`.${activeOptionClass}`).removeClass(activeOptionClass); 27 | $(`.${activeOptionButtonClass}`).removeClass(activeOptionButtonClass); 28 | targetElem.show(); 29 | setTimeout(() => { 30 | targetElem.addClass(activeOptionClass); 31 | $(this).addClass(activeOptionButtonClass); 32 | setTimeout(() => { 33 | $('.message-lecture-name').css('opacity', 0.4); 34 | $('.message-content').css('opacity', 1); 35 | $('.non-active-boards-title').css('opacity', 1); 36 | $('.minimize-chat-view').css('opacity', 1); 37 | }, 700); 38 | }, 0); 39 | } 40 | } 41 | 42 | export default function initializeOptionsMenu() { 43 | $('#toggle-boards-view').click(handleOptionClick); 44 | $('#toggle-chat-view').click(handleOptionClick); 45 | $('#close-non-active-boards').click(() => $('#toggle-boards-view').click()); 46 | $('#minimize-chat-view').click(() => $('#toggle-chat-view').click()); 47 | 48 | $('#toggle-speaker').click(function () { 49 | toggleSpeakers(); 50 | }); 51 | 52 | $('#fullscreen-video').click(() => { 53 | const elem = document.getElementById('whiteboard'); 54 | if (screenfull.isEnabled) { 55 | screenfull.request(elem); 56 | } 57 | }); 58 | 59 | // check this later to use it on the other modals. 60 | $('#connect-on-your-phone').click(() => $('#qr-code-modal').fadeIn()); 61 | $('.qrcode-modal-content').click((e) => e.stopPropagation()); 62 | $('#qr-code-modal').click(function (e) { e.stopPropagation(); $(this).fadeOut(); }); 63 | 64 | $('#toggle-chat-view').click(); 65 | } 66 | -------------------------------------------------------------------------------- /public/images/portugal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 29 | 30 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /server/servers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const express = require('express'); 3 | const helmet = require('helmet'); 4 | const redis = require('redis'); 5 | const socketio = require('socket.io'); 6 | const bodyParser = require('body-parser'); 7 | const session = require('express-session'); 8 | const sharedSession = require('express-socket.io-session'); 9 | const mustache = require('mustache-express'); 10 | const Sentry = require('@sentry/node'); 11 | const locale = require('locale'); 12 | 13 | const RedisStore = require('connect-redis')(session); 14 | const { 15 | redisPort, expressPort, environment, debugClient, 16 | redisUrl, loggerFlag, sessionSecret, sessionName, 17 | defaultLanguage, supportedLanguages, sentryDSN, 18 | } = require('../config/config'); 19 | 20 | const { logger } = require('./services/logger/logger'); 21 | const { logMiddleWare } = require('./services/logger/loggingMiddleware'); 22 | 23 | 24 | const app = express(); 25 | 26 | // sentry intergation 27 | app.use(Sentry.Handlers.requestHandler()); 28 | 29 | const expressServer = app.listen(expressPort); 30 | 31 | 32 | const io = socketio(expressServer, { cookie: false }); 33 | 34 | app.engine('html', mustache()); 35 | app.set('view engine', 'html'); 36 | app.set('views', 'public'); 37 | app.use(express.static('public/js')); 38 | app.use(express.static('public/css')); 39 | app.use(express.static('public/images')); 40 | app.use(express.static('public/audios')); 41 | app.use(express.json({ limit: '50mb' })); 42 | app.use(locale(supportedLanguages, defaultLanguage)); 43 | app.use(bodyParser.json()); 44 | app.use(helmet()); 45 | if (loggerFlag && environment === 'PRODUCTION') app.use(logMiddleWare); 46 | 47 | 48 | let client = null; 49 | if (environment === 'DEVELOPMENT') { 50 | client = redis.createClient(redisUrl); // use envir var TODO. 51 | } else { 52 | Sentry.init({ dsn: sentryDSN, environment }); 53 | app.set('trust proxy', 1); // trust first proxy, if not set, ngnix ip will be considered by same as clients 54 | client = redis.createClient(redisUrl); 55 | } 56 | 57 | const expressSession = session( 58 | { 59 | store: new RedisStore({ client }), 60 | name: sessionName, 61 | secret: sessionSecret, 62 | resave: false, 63 | saveUninitialized: true, 64 | cookie: { 65 | secure: (environment === 'PRODUCTION'), 66 | sameSite: true, 67 | domain: (environment === 'PRODUCTION') ? 'liteboard.io' : null, 68 | }, 69 | }, 70 | ); 71 | app.use(expressSession); 72 | 73 | io.use(sharedSession(expressSession, { 74 | autoSave: true, 75 | })); 76 | 77 | logger.info(`Express and socketio are listening on port: ${expressPort}`); 78 | 79 | if (debugClient === 'false') { 80 | client.flushall((err, succeeded) => { 81 | logger.info(`Redis cleared: ${succeeded}`); 82 | }); 83 | } 84 | 85 | 86 | client.on('connect', () => { 87 | logger.info(`Redis connected on port: ${redisPort}`); 88 | }); 89 | 90 | if (environment !== 'DEVELOPMENT') app.use(Sentry.Handlers.errorHandler()); 91 | 92 | 93 | module.exports = { 94 | app, 95 | io, 96 | client, 97 | }; 98 | -------------------------------------------------------------------------------- /public/js/manager/canvasTopMenu.js: -------------------------------------------------------------------------------- 1 | import { showInfoMessage, redirectToStats, copyTextToClipboard } from '../utility.js'; 2 | import { showConfigModal } from './streamConfigurations.js'; 3 | 4 | export default function initializeCanvasTopMenu(socket, roomId) { 5 | const hasAudio = $('#audioValidator').val() === 'true'; 6 | const hasWebcam = $('#webcamValidator').val() === 'true'; 7 | const hasWhiteboard = $('#whiteboardValidator').val() === 'true'; 8 | $('.hide-options-right').click(() => { 9 | $('.right-bar').fadeToggle(); 10 | }); 11 | 12 | let sharableUrl = window.location.href; 13 | sharableUrl = sharableUrl.substr(0, sharableUrl.lastIndexOf('/') + 1); 14 | sharableUrl += roomId; 15 | document.getElementById('copy-share-link').addEventListener('click', () => { 16 | copyTextToClipboard(sharableUrl); 17 | showInfoMessage(`${$('#link-copied-info').val()}!`); 18 | }); 19 | 20 | if (hasAudio || hasWebcam) { 21 | document.querySelector('#mic-config').addEventListener('click', () => { 22 | $('#welcome-lecture-modal').show(); 23 | $('#join-content').hide(); 24 | $('#go-back').hide(); 25 | showConfigModal(); 26 | document.querySelector('.modal-content').classList.add('lecture'); 27 | }); 28 | } else { 29 | $('#mic-config').hide(); 30 | $('#config-divider').hide(); 31 | } 32 | 33 | socket.on('updateNumOfStudents', (roomSizeObj) => { 34 | if (`${roomSizeObj.room}` === `${roomId}`) { 35 | document.getElementById('specs').innerHTML = roomSizeObj.size; 36 | } 37 | }); 38 | 39 | document.querySelector('#end-lecture').addEventListener('click', () => { 40 | socket.emit('lectureEnd', () => redirectToStats(roomId)); 41 | }); 42 | 43 | $('.hide-bar-button').click(() => { 44 | $('.classroom-info').fadeToggle(500); 45 | $('.show-bar-button-container').delay(500).fadeToggle(); 46 | $('.left-bar').removeClass('animate__fadeInLeft').addClass('animate__fadeOutLeft'); 47 | $('.right-bar').removeClass('animate__fadeInRight').addClass('animate__fadeOutRight'); 48 | $('div.messages').removeClass('animate__fadeInUp').addClass('animate__fadeOutDown'); 49 | $('.webcam-container').removeClass('animate__fadeIn').addClass('animate__fadeOut'); 50 | $('.toggle-canvas-and-audio-menu-wrap').removeClass('animate__fadeInLeft').addClass('animate__fadeOutDown'); 51 | }); 52 | 53 | $('.show-bar-button').click(() => { 54 | $('.show-bar-button-container').fadeToggle(1200); 55 | $('.classroom-info').delay().fadeToggle(); 56 | $('.left-bar').show().removeClass('animate__fadeOutLeft').addClass('animate__fadeInLeft'); 57 | $('.right-bar').show().removeClass('animate__fadeOutRight').addClass('animate__fadeInRight'); 58 | $('div.messages').show().removeClass('animate__fadeOutDown').addClass('animate__fadeInUp'); 59 | $('.webcam-container').show().removeClass('animate__fadeOut').addClass('animate__fadeIn'); 60 | $('.toggle-canvas-and-audio-menu-wrap').show().removeClass('animate__fadeOutDown').addClass('animate__fadeInLeft'); 61 | }); 62 | 63 | setTimeout(() => $('.show-bar-button').click(), 400); 64 | } 65 | -------------------------------------------------------------------------------- /public/js/chatUtils.js: -------------------------------------------------------------------------------- 1 | import { displayImagePopUpOnClick, downloadFile } from '../utility.js'; 2 | import Attachment from './classes/Attachment.js'; 3 | 4 | export default function initializeChat(chat) { 5 | const fileInput = document.getElementById('file-input'); 6 | 7 | fileInput.addEventListener('change', (e) => { 8 | const file = e.target.files[0]; 9 | const reader = new FileReader(); 10 | reader.onload = function (fileLoadedEvent) { 11 | if (file.type.includes('image')) { 12 | $('#image-preview').attr('src', fileLoadedEvent.target.result); 13 | } 14 | chat.setPreview(new Attachment(fileLoadedEvent.target.result, file.name, file.type)); 15 | }; 16 | reader.readAsDataURL(file); 17 | if (file.type.includes('image')) { 18 | $('#file-preview').hide(); 19 | $('#preview').show(); 20 | $('#close-preview').css('bottom', '55px'); 21 | // show image 22 | let imgWidth = 0; 23 | $('img').load(function () { 24 | imgWidth = $(this).width(); 25 | const left = (15 + imgWidth + 15); 26 | $('#close-preview').css('left', `${left}px`); 27 | chat.scrollToBottom(); 28 | }); 29 | } else { 30 | $('#file-preview').show(); 31 | $('#name-file').html(file.name); 32 | $('#preview').show(); 33 | $('#close-preview').css('bottom', '3px'); 34 | $('#close-preview').css('left', ''); 35 | $('#close-preview').css('right', '15px'); 36 | } 37 | chat.scrollToBottom(); 38 | }); 39 | 40 | $(document).on('click', '.name-file', (e) => { 41 | downloadFile($(e.target).attr('data-file'), e.target.innerHTML); 42 | }); 43 | 44 | $(document).on('click', '.download-container', (e) => { 45 | // e.stopPropagation(); 46 | let containerElem = $(e.target); 47 | while (!containerElem.hasClass('download-container')) containerElem = containerElem.parent(); 48 | downloadFile(containerElem.attr('data-file'), containerElem.attr('data-name')); 49 | }); 50 | 51 | $(document).on('click', '.message-image', displayImagePopUpOnClick); 52 | 53 | let click = 0; 54 | window.addEventListener('click', (e) => { 55 | const image = document.querySelector('.modal-message-image-vertical') || document.querySelector('.modal-message-image-horizontal'); 56 | const modal = document.getElementById('image-modal'); 57 | const download = document.querySelector('.download-container'); 58 | if (image !== null && $(modal).is(':visible')) { 59 | if (!image.contains(e.target) && !download.contains(e.target) && click > 0) { 60 | $(modal).hide(); 61 | $(image).remove(); 62 | $(download).remove(); 63 | click = 0; 64 | } else { 65 | click += 1; 66 | } 67 | } 68 | }); 69 | 70 | $('.mute-unmute-chat').click(function (e) { 71 | e.stopPropagation(); 72 | $(this).toggleClass('fa-volume-up'); 73 | $(this).toggleClass('fa-volume-mute'); 74 | }) 75 | 76 | $('#close-preview').click(() => { 77 | fileInput.value = ''; 78 | $('#file-preview').hide(); 79 | $('#name-file').html(''); 80 | $('#image-preview').attr('src', ''); 81 | $('#preview').hide(); 82 | $('#close-preview').css('right', '15px'); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /test/apiTests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const request = require('request'); 3 | const url = require('url'); 4 | const io = require('socket.io-client'); 5 | // const { should } = require('chai'); 6 | const assert = require('assert'); 7 | const { client: redisClient } = require('../server/servers'); 8 | const { createLecture } = require('./helpers/apiHelpers'); 9 | 10 | require('../index'); // used to start the server 11 | 12 | 13 | describe('Socket.io Tests', () => { 14 | it('/lecture/:id should return status 200 and path with error code 3 when lecture id is not found', (done) => { 15 | const options = { 16 | uri: 'http://localhost:8080/lecture/id', // note this is hardcoded in change this afterwards 17 | method: 'GET', 18 | }; 19 | request(options, (err, response) => { 20 | const responseJson = response.toJSON(); 21 | const urlPart = url.parse(responseJson.request.uri); 22 | assert.equal(urlPart.path, '/error?code=3'); 23 | assert.equal(response.statusCode, 200); 24 | done(); 25 | }); 26 | }); 27 | 28 | it('/lecture/stats/:id should return status 200 and path with error code 3 when lecture id is not found', (done) => { 29 | const options = { 30 | uri: 'http://localhost:8080/lecture/stats/id', // note this is hardcoded in change this afterwards 31 | method: 'GET', 32 | }; 33 | request(options, (err, response) => { 34 | const responseJson = response.toJSON(); 35 | const urlPart = url.parse(responseJson.request.uri); 36 | assert.equal(urlPart.path, '/error?code=3'); 37 | assert.equal(response.statusCode, 200); 38 | done(); 39 | }); 40 | }); 41 | 42 | it('/lecture/stats/:id should return status 200 and path with error code 4 when lecutre id is found but not deleted', (done) => { 43 | createLecture((err, response, body) => { 44 | const { redirectUrl } = body; 45 | const managerId = (redirectUrl.split('/'))[2]; 46 | redisClient.hmget('managers', managerId, (err, manager) => { 47 | const managerObj = JSON.parse(manager.pop()); 48 | const options = { 49 | uri: `http://localhost:8080/lecture/stats/${managerObj.roomId}`, // note this is hardcoded in change this afterwards 50 | method: 'GET', 51 | }; 52 | request(options, (getErr, getResponse) => { 53 | const responseJson = getResponse.toJSON(); 54 | const urlPart = url.parse(responseJson.request.uri); 55 | assert.equal(urlPart.path, '/error?code=4'); 56 | assert.equal(getErr, null); 57 | assert.equal(getResponse.statusCode, 200); 58 | done(); 59 | }); 60 | }); 61 | }); 62 | }); 63 | it('/create should return status 200', (done) => { 64 | createLecture((err, response, body) => { 65 | assert.equal(response.statusCode, 200); 66 | const { redirectUrl } = body; 67 | const managerId = (redirectUrl.split('/'))[2]; 68 | assert.equal(redirectUrl.includes('/lecture/'), true); 69 | redisClient.hmget('managers', managerId, (error, manager) => { 70 | assert.equal(error, null); 71 | const managerObj = JSON.parse(manager.pop()); 72 | assert.equal(managerObj.email, 'testing123@gmail.com'); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /public/js/tools.js: -------------------------------------------------------------------------------- 1 | export const TOOL_SQUARE = 'square'; 2 | export const TOOL_TRIANGLE = 'triangle'; 3 | export const TOOL_PAINT_BUCKET = 'pointer'; 4 | export const TOOL_PENCIL = 'pencil'; 5 | export const TOOL_ERASER = 'eraser'; 6 | export const TOOL_LINE = 'line'; 7 | export const TOOL_CIRCLE = 'circle'; 8 | export const TOOL_SELECTAREA = 'select-area'; 9 | 10 | export default function initializeToolsMenu(whiteboard) { 11 | let timeout; 12 | const onZoomDone = () => { 13 | $('#zoom-modal').hide(); 14 | }; 15 | document.getElementById('canvas').addEventListener('wheel', (e) => { 16 | whiteboard.onScroll(e.deltaY, e.clientX, e.clientY); 17 | const { zoom } = whiteboard.getZoom(); 18 | $('#zoom-modal').show(); 19 | $('#zoom-span').html(`${Math.ceil(zoom * 100)}%`); 20 | clearTimeout(timeout); 21 | timeout = setTimeout(onZoomDone, 300); 22 | }); 23 | document.querySelectorAll('.back-to-tool-menu').forEach( 24 | (backElem) => { 25 | backElem.addEventListener('click', () => { 26 | document.querySelectorAll('[data-main]').forEach( 27 | (item) => { 28 | const mainKey = item.getAttribute('data-main'); 29 | const prefix = '#right-bar'; 30 | document.querySelector(`${prefix}-${mainKey}`).style.display = 'none'; 31 | }, 32 | ); 33 | document.querySelector('#right-bar-main').style.display = 'block'; 34 | }); 35 | }, 36 | ); 37 | 38 | document.querySelectorAll('[data-main]').forEach( 39 | (item) => { 40 | item.addEventListener('click', () => { 41 | const mainKey = item.getAttribute('data-main'); 42 | const prefix = '#right-bar'; 43 | document.querySelector(`${prefix}-main`).style.display = 'none'; 44 | document.querySelector(`${prefix}-${mainKey}`).style.display = 'block'; 45 | }); 46 | }, 47 | ); 48 | document.querySelectorAll('[data-tool]').forEach( 49 | (item) => ( 50 | item.addEventListener('click', () => { 51 | $('[data-tool]').find('.tool-active-svg').removeClass('tool-active-svg'); 52 | $('[data-tool]').find('.tool-active').removeClass('tool-active'); 53 | 54 | $(item).find('.left-bar-link-svg').addClass('tool-active-svg'); 55 | $(item).find('.left-bar-link').addClass('tool-active'); 56 | const clickedTool = item.getAttribute('data-tool'); 57 | whiteboard.updateCursor(clickedTool); 58 | whiteboard.tools.switchTo(clickedTool); 59 | })), 60 | ); 61 | 62 | document.querySelectorAll('[data-line-width]').forEach( 63 | (item) => { 64 | item.addEventListener('click', () => { 65 | $('[data-line-width]').find('.tool-active').removeClass('tool-active'); 66 | $(item).find('.left-bar-link').addClass('tool-active'); 67 | 68 | const selectedWidth = item.getAttribute('data-line-width'); 69 | activeWidth = selectedWidth; 70 | }); 71 | }, 72 | ); 73 | 74 | document.querySelectorAll('[data-color]').forEach( 75 | (item) => { 76 | item.addEventListener('click', () => { 77 | $('[data-color]').find('.tool-active').removeClass('tool-active'); 78 | $(item).find('.right-bar-link').addClass('tool-active'); 79 | const selectedColor = item.getAttribute('data-color'); 80 | activeColor = selectedColor; 81 | }); 82 | }, 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /public/images/expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /public/images/area.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 63 | 64 | 65 | 66 | 67 | 69 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /public/js/classes/Fill.js: -------------------------------------------------------------------------------- 1 | import Point from './Point.js'; 2 | 3 | function colorsMatch(color1, color2) { 4 | return color1[0] === color2[0] && color1[1] === color2[1] 5 | && color1[2] === color2[2] && color1[3] === color2[3]; 6 | } 7 | 8 | function hexToRgba(hex) { 9 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 10 | return [ 11 | parseInt(result[1], 16), // convert hex value to a number 12 | parseInt(result[2], 16), 13 | parseInt(result[3], 16), 14 | 255, 15 | ]; 16 | } 17 | 18 | export default class Fill { 19 | // canvas ==> canvas board being affected 20 | // point ==> point at which the algorithm will start being executed 21 | // color ==> replacement color (the one that will fill the screen) 22 | 23 | constructor(canvas, point, color) { 24 | this.fillStack = []; 25 | this.context = canvas.getContext('2d'); 26 | 27 | // image data will be the RGB value of all the pixels on the screen in an array 28 | this.imageData = this.context.getImageData(0, 0, 29 | this.context.canvas.width, this.context.canvas.height); 30 | 31 | const targetColor = this.getPixel(point); 32 | 33 | const fillColor = hexToRgba(color); 34 | 35 | this.floodFill(point, targetColor, fillColor); // maybe this.targetColor, this.fillColor 36 | this.fillColor(); 37 | } 38 | 39 | // function that will fill the screen 40 | floodFill(point, targetColor, fillColor) { 41 | if (colorsMatch(targetColor, fillColor)) return; 42 | 43 | const currentColor = this.getPixel(point); 44 | 45 | // paint if colorsMatch(currentColor, targetColor) returns true 46 | if (colorsMatch(currentColor, targetColor)) { 47 | this.setPixel(point, fillColor); 48 | // color left, right, up, down pixels respectively 49 | this.fillStack.push([new Point(point.x + 1, point.y), targetColor, fillColor]); 50 | this.fillStack.push([new Point(point.x - 1, point.y), targetColor, fillColor]); 51 | this.fillStack.push([new Point(point.x, point.y + 1), targetColor, fillColor]); 52 | this.fillStack.push([new Point(point.x, point.y - 1), targetColor, fillColor]); 53 | } 54 | } 55 | 56 | fillColor() { 57 | if (this.fillStack.length !== 0) { 58 | const range = this.fillStack.length; 59 | 60 | for (let i = 0; i < range; i++) { 61 | this.floodFill(this.fillStack[i][0], this.fillStack[i][1], this.fillStack[i][2]); 62 | } 63 | 64 | this.fillStack.splice(0, range); 65 | 66 | this.fillColor(); 67 | } else { 68 | // if it is succesfull, color the image and empty the stack 69 | this.context.putImageData(this.imageData, 0, 0); 70 | this.fillStack = []; 71 | } 72 | } 73 | 74 | getPixel(point) { 75 | // check to see if the pixel selected is not outside of the canvas 76 | if (point.x < 0 || point.y < 0 77 | || point.x >= this.imageData.width, point.y >= this.imageData.height) { 78 | return [-1, -1, -1, -1]; 79 | } 80 | const offset = (point.y * this.imageData.width + point.x) * 4; 81 | 82 | // return the surrounding pixel values 83 | return [ 84 | this.imageData.data[offset + 0], // red portion 85 | this.imageData.data[offset + 1], // green portion 86 | this.imageData.data[offset + 2], // blue portion 87 | this.imageData.data[offset + 3], // alpha portion 88 | 89 | ]; 90 | } 91 | 92 | setPixel(point, fillColor) { 93 | const offset = (point.y * this.imageData.width + point.x) * 4; 94 | 95 | this.imageData.data[offset + 0] = fillColor[0]; // red portion 96 | this.imageData.data[offset + 1] = fillColor[1]; // green portion 97 | this.imageData.data[offset + 2] = fillColor[2]; // blue portion 98 | this.imageData.data[offset + 3] = fillColor[3]; // alpha portion 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /public/js/guest/guest.js: -------------------------------------------------------------------------------- 1 | import initializeGuestChat from './guestChat.js'; 2 | import { getUrlId, redirectToStats, getStatusColor } from '../utility.js'; 3 | import initializeGuestRTC, { changeStatus, disconnectMicrophone } from './guestRTC.js'; 4 | import setNonActiveBoards from './guestBoards.js'; 5 | import initializeOptionsMenu from './guestOptionsMenu.js'; 6 | 7 | const nameInput = document.querySelector('#studentName'); 8 | const invalidNameDiv = document.getElementById('invalid-student-name'); 9 | const hasWhiteboard = $('#whiteboardValidator').val() === 'true'; 10 | const roomId = getUrlId(); 11 | let studentName; 12 | let currentBoard; 13 | 14 | function joinLecture() { 15 | const socket = io('/', { 16 | query: `id=${roomId}`, 17 | }); 18 | 19 | window.onbeforeunload = () => { 20 | socket.emit('send-to-room', roomId, { left: studentName }); 21 | socket.disconnect(); 22 | }; 23 | 24 | socket.on('ready', (room) => { 25 | if (hasWhiteboard) { 26 | const { boards, boardActive } = room.lecture_details; 27 | setNonActiveBoards(boards.filter((e, i) => i !== boardActive)); 28 | } 29 | 30 | if (!room.lecture_details.isManagerLive) { 31 | changeStatus.host_disconnected(); 32 | } 33 | 34 | initializeOptionsMenu(); 35 | initializeGuestRTC(); 36 | initializeGuestChat(socket, room.lecture_details.id, studentName); 37 | }); 38 | 39 | socket.on('lectureEnd', () => { 40 | // Right now only redirect to stats 41 | // But later we will display a modal saying 42 | // That the lecture ended and then redirect them. 43 | redirectToStats(roomId); 44 | }); 45 | 46 | socket.on('disconnect', () => { 47 | changeStatus.connection_lost(); 48 | document.querySelector('#whiteboard').poster = currentBoard; 49 | }); 50 | 51 | socket.on('managerDisconnected', () => { 52 | document.querySelector('#whiteboard').poster = currentBoard; 53 | changeStatus.host_disconnected(); 54 | disconnectMicrophone(); 55 | $('#toggle-mic').removeClass('fa-microphone'); 56 | $('#toggle-mic').addClass('fa-microphone-slash'); 57 | }); 58 | 59 | socket.on('updateNumOfStudents', (roomSizeObj) => { 60 | if (`${roomSizeObj.room}` === `${roomId}`) { 61 | document.getElementById('specs').innerHTML = roomSizeObj.size; 62 | } 63 | }); 64 | 65 | socket.on('boards', setNonActiveBoards); 66 | 67 | socket.on('currentBoard', (boardImg) => { 68 | currentBoard = boardImg; 69 | if (document.querySelector('#whiteboard').poster) { 70 | // if poster is defined we don't want to do anything 71 | } else { 72 | document.querySelector('#whiteboard').poster = currentBoard; 73 | } 74 | }); 75 | } 76 | 77 | window.onload = async () => { 78 | $('#modal-select-button').click(() => { 79 | if (nameInput.value === '') { 80 | invalidNameDiv.style.opacity = 1; 81 | } else { 82 | studentName = nameInput.value; 83 | $('#lecture-status .status-dot').css('background', getStatusColor('starting')); 84 | $('#lecture-status .status-text').html($('#status-starting').val()); 85 | $('video#whiteboard').parent().addClass('running'); 86 | 87 | /* 88 | joinLecture(); 89 | $('#login-lecture-modal').hide(); 90 | */ 91 | 92 | fetch(`/validate/lecture?id=${roomId}`).then((req) => { 93 | switch (req.status) { 94 | case 200: 95 | joinLecture(); 96 | $('#login-lecture-modal').hide(); 97 | break; 98 | case 404: 99 | window.location.replace('/error?code=1'); 100 | break; 101 | case 401: 102 | window.location.replace('/error?code=2'); 103 | break; 104 | default: break; 105 | } 106 | }); 107 | } 108 | }); 109 | 110 | nameInput.addEventListener('input', () => { 111 | invalidNameDiv.style.opacity = 0; 112 | }); 113 | }; 114 | -------------------------------------------------------------------------------- /public/js/classes/Chat.js: -------------------------------------------------------------------------------- 1 | export default class Chat { 2 | constructor(containerId) { 3 | this.container = document.getElementById(containerId); 4 | this.unreadCount = 0; 5 | this.history = []; 6 | this.preview = null; 7 | } 8 | 9 | appendMessage(message, isIncoming) { 10 | const time = new Date(); 11 | this.history.push({ 12 | message, isIncoming, time, 13 | }); 14 | const messageElement = document.createElement('div'); 15 | const isLeavingRoomMsg = typeof message.left !== 'undefined'; 16 | const isJoiningRoomMsg = typeof message.joined !== 'undefined'; 17 | if (isLeavingRoomMsg || isJoiningRoomMsg) { 18 | messageElement.classList.add('message-lecture-name'); 19 | const from = isJoiningRoomMsg ? message.joined : message.left; 20 | const action = $(`#${isJoiningRoomMsg ? 'joined-chat-action' : 'left-chat-action'}`).val(); 21 | messageElement.innerHTML = `${from} ${action}`; 22 | } else { 23 | messageElement.classList.add(isIncoming ? 'incoming-message' : 'outgoing-message'); 24 | messageElement.classList.add('message-margin'); 25 | const tableData = document.createElement('div'); 26 | tableData.classList.add('message-content'); 27 | tableData.classList.add(isIncoming ? 'in' : 'out'); 28 | const nameDiv = document.createElement('div'); 29 | nameDiv.classList.add('bottom-padding'); 30 | const nameSpan = document.createElement('span'); 31 | nameSpan.classList.add('name-span'); 32 | if (tableData.classList.contains('out')) { 33 | nameSpan.innerHTML = $('#you-name-chat').val(); 34 | nameDiv.append(nameSpan); 35 | tableData.append(nameDiv); 36 | } 37 | if (tableData.classList.contains('in')) { 38 | tableData.style.background = message.color; 39 | nameSpan.innerHTML = message.sender; 40 | nameDiv.append(nameSpan); 41 | tableData.append(nameDiv); 42 | } 43 | const messageText = document.createElement('div'); 44 | messageText.classList.add('message-text'); 45 | messageText.innerText = message.content; 46 | let image; let file; let imageName; let fileImage; 47 | if (message.attachment !== null) { 48 | file = document.createElement('div'); 49 | if (messageText.innerHTML !== '') { 50 | file.classList.add('bottom-padding'); 51 | } 52 | 53 | if (message.attachment.type.includes('image')) { 54 | image = document.createElement('img'); 55 | image.classList.add('message-image'); 56 | image.src = message.attachment.file; 57 | image.setAttribute('data-name', message.attachment.name); 58 | file.append(image); 59 | tableData.append(file); 60 | } else { 61 | fileImage = document.createElement('span'); 62 | fileImage.innerHTML = ''; 63 | fileImage.classList.add('file-preview'); 64 | imageName = document.createElement('span'); 65 | imageName.classList.add('name-file'); 66 | imageName.innerHTML = message.attachment.name; 67 | imageName.setAttribute('data-file', message.attachment.file); 68 | imageName.setAttribute('href', message.attachment.file); 69 | 70 | file.append(fileImage); 71 | file.append(imageName); 72 | tableData.append(file); 73 | } 74 | if (!isIncoming) { 75 | $('#file-preview').hide(); 76 | $('#name-file').html(''); 77 | $('#image-preview').attr('src', ''); 78 | $('#close-preview').css('right', '15px'); 79 | $('#preview').hide(); 80 | } 81 | } 82 | tableData.append(messageText); 83 | messageElement.append(tableData); 84 | } 85 | this.container.append(messageElement); 86 | this.scrollToBottom(); 87 | } 88 | 89 | scrollToBottom() { 90 | this.container.scrollTop = this.container.scrollHeight; 91 | } 92 | 93 | setPreview(src) { 94 | this.preview = src; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /public/js/manager/manager.js: -------------------------------------------------------------------------------- 1 | import Whiteboard from '../classes/Whiteboard.js'; 2 | import initializeToolsMenu from '../tools.js'; 3 | import initializeCanvasTopMenu from './canvasTopMenu.js'; 4 | import initializeManagerChat from './managerChat.js'; 5 | import initializeBoards, { emitBoards } from './managerBoards.js'; 6 | import initializeActionsMenu from './canvasActions.js'; 7 | import { initializeManagerMedia, initializeManagerRTC, changeStatus } from './managerRTC.js'; 8 | import { 9 | getUrlId, reloadWindow, copyTextToClipboard, saveCurrentBoard, 10 | } from '../utility.js'; 11 | 12 | const managerId = getUrlId(); 13 | const hasAudio = $('#audioValidator').val() === 'true'; 14 | const hasWebcam = $('#webcamValidator').val() === 'true'; 15 | const hasWhiteboard = $('#whiteboardValidator').val() === 'true'; 16 | const roomId = $('#_id').val(); 17 | 18 | function beginLecture(stream) { 19 | const socket = io('/', { query: `id=${managerId}` }); 20 | const whiteboard = hasWhiteboard ? new Whiteboard('canvas') : null; 21 | const canvasStream = hasWhiteboard ? whiteboard.getStream() : null; 22 | 23 | if (hasWhiteboard) { 24 | socket.on('currentBoard', (studentSocketId) => { 25 | socket.emit('currentBoard', { 26 | boardImg: whiteboard.getImage(), 27 | studentSocket: studentSocketId, 28 | }); 29 | }); 30 | } 31 | 32 | socket.on('disconnect', changeStatus.connection_lost); 33 | 34 | socket.on('attemptToConnectMultipleManagers', () => { 35 | window.location.replace('/error?code=2'); 36 | }); 37 | 38 | $(window).on('beforeunload', () => { 39 | socket.emit('send-to-room', roomId, { left: $('#host-name-chat').val() }); 40 | if (hasWhiteboard) { 41 | saveCurrentBoard(whiteboard); 42 | emitBoards(socket, whiteboard); 43 | } 44 | socket.disconnect(); 45 | }); 46 | 47 | socket.on('invalidLecture', reloadWindow); 48 | 49 | socket.on('ready', (room) => { 50 | if (hasWhiteboard) { 51 | const { boards, boardActive } = room.lecture_details; 52 | whiteboard.initialize(); 53 | initializeToolsMenu(whiteboard); 54 | initializeActionsMenu(socket, whiteboard, canvasStream); 55 | initializeBoards(socket, whiteboard, boards, boardActive, canvasStream); 56 | } 57 | initializeCanvasTopMenu(socket, room.lecture_details.id); 58 | initializeManagerChat(socket, room.lecture_details.id); 59 | initializeManagerRTC(stream, canvasStream); 60 | }); 61 | } 62 | 63 | window.onload = () => { 64 | $('#modal-copy-link').click(function () { 65 | const copyText = document.querySelector('.modal-url-share'); 66 | copyTextToClipboard(copyText.innerText); 67 | const range = document.createRange(); 68 | range.selectNodeContents(copyText); 69 | const selection = window.getSelection(); 70 | selection.removeAllRanges(); 71 | selection.addRange(range); 72 | this.innerHTML = $('#copied-info').val(); 73 | this.style.opacity = 1; 74 | setTimeout(() => { 75 | this.style.opacity = 0.83; 76 | this.innerHTML = $('#copy-info').val(); 77 | selection.removeAllRanges(); 78 | }, 2000); 79 | }); 80 | 81 | if (!(hasAudio || hasWebcam)) $('#modal-select-button').css('margin-bottom', '30px'); 82 | $('#welcome-lecture-modal').show(); 83 | 84 | changeStatus.starting(); 85 | initializeManagerMedia((stream) => { 86 | $('#modal-select-button').removeClass('live-button-inactive').find('.ld').fadeOut(function () { 87 | $(this).parent().find('span').fadeIn(); 88 | }); 89 | 90 | $('#modal-select-button').click(() => { 91 | fetch(`/validate/lecture?id=${roomId}`).then((req) => { 92 | switch (req.status) { 93 | case 200: 94 | beginLecture(stream); 95 | $('#welcome-lecture-modal').hide(); 96 | break; 97 | case 404: 98 | window.location.replace('/error?code=1'); 99 | break; 100 | case 401: 101 | window.location.replace('/error?code=2'); 102 | break; 103 | default: break; 104 | } 105 | }); 106 | }); 107 | }); 108 | }; 109 | -------------------------------------------------------------------------------- /public/js/create.js: -------------------------------------------------------------------------------- 1 | import { buildPostRequestOpts, getJanusUrl, getJanusToken } from './utility.js'; 2 | 3 | const janusUrl = getJanusUrl(); 4 | const createBut = document.querySelector('#create-lecture'); 5 | const invalidEmailDiv = document.getElementById('invalid-email'); 6 | const invalidNameDiv = document.getElementById('invalid-lecturename'); 7 | const invalidToolsDiv = document.getElementById('invalid-tools'); 8 | const invalidAudioDiv = document.getElementById('select-video'); 9 | const emailInput = document.querySelector('#email'); 10 | const nameInput = document.querySelector('#lectureName'); 11 | const audioCheckbox = document.querySelector('#audio-check'); 12 | const whiteboardCheckbox = document.querySelector('#whiteboard-check'); 13 | const webcamCheckbox = document.querySelector('#webcam-check'); 14 | function isValidEmail(email) { 15 | return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email); 16 | } 17 | 18 | createBut.addEventListener('click', async (e) => { 19 | e.preventDefault(); 20 | const lectureName = nameInput.value; 21 | const lectureEmail = emailInput.value; 22 | if (!audioCheckbox.checked && !webcamCheckbox.checked && !whiteboardCheckbox.checked) { 23 | invalidToolsDiv.style.opacity = 1; 24 | invalidEmailDiv.style.opacity = 0; 25 | invalidNameDiv.style.opacity = 0; 26 | invalidAudioDiv.style.opacity = 0; 27 | } else if (audioCheckbox.checked && !webcamCheckbox.checked && !whiteboardCheckbox.checked) { 28 | invalidToolsDiv.style.opacity = 0; 29 | invalidEmailDiv.style.opacity = 0; 30 | invalidNameDiv.style.opacity = 0; 31 | invalidAudioDiv.style.opacity = 1; 32 | } else if (lectureName === '') { 33 | invalidToolsDiv.style.opacity = 0; 34 | invalidEmailDiv.style.opacity = 0; 35 | invalidNameDiv.style.opacity = 1; 36 | invalidAudioDiv.style.opacity = 0; 37 | } else if (lectureEmail === '' || isValidEmail(lectureEmail)) { 38 | const janusToken = await getJanusToken(); 39 | Janus.init({ 40 | debug: 'all', 41 | callback() { 42 | const janus = new Janus({ 43 | server: janusUrl, 44 | token: janusToken, 45 | success() { 46 | // Attach to VideoRoom plugin 47 | janus.attach( 48 | { 49 | plugin: 'janus.plugin.videoroom', 50 | success(pluginHandle) { 51 | pluginHandle.send({ 52 | message: { 53 | request: 'create', 54 | publishers: 8 55 | }, 56 | success(res) { 57 | if (res.videoroom === 'created') { 58 | const body = JSON.stringify({ 59 | name: lectureName, 60 | email: lectureEmail, 61 | roomId: res.room, 62 | lectureTools: { 63 | audio: audioCheckbox.checked, 64 | webcam: webcamCheckbox.checked, 65 | whiteboard: whiteboardCheckbox.checked, 66 | }, 67 | }); 68 | fetch('/create', buildPostRequestOpts(body)) 69 | .then((response) => { 70 | if (response.status === 200) { 71 | response.json().then((json) => { 72 | window.location = json.redirectUrl; 73 | }); 74 | } 75 | }); 76 | } 77 | }, 78 | }); 79 | }, 80 | }, 81 | ); 82 | }, 83 | }); 84 | }, 85 | }); 86 | } else { 87 | invalidNameDiv.style.opacity = 0; 88 | invalidEmailDiv.style.opacity = 1; 89 | invalidToolsDiv.style.opacity = 0; 90 | invalidAudioDiv.style.opacity = 0; 91 | } 92 | }); 93 | 94 | emailInput.addEventListener('input', () => { 95 | invalidEmailDiv.style.opacity = 0; 96 | }); 97 | 98 | nameInput.addEventListener('input', () => { 99 | invalidNameDiv.style.opacity = 0; 100 | }); 101 | -------------------------------------------------------------------------------- /public/js/stats.js: -------------------------------------------------------------------------------- 1 | import { getUrlId, buildPostRequestOpts } from './utility.js'; 2 | 3 | // getSecondsBetweenTwoTimes receives two Date objects 4 | function getSecondsBetweenTwoTimes(start, end) { 5 | return (end.getTime() - start.getTime()) / 1000; 6 | } 7 | 8 | function buildGraph(statsObj) { 9 | const timeTracks = statsObj.userTracker.map((obj) => obj.time); 10 | const watchersTracks = statsObj.userTracker.map((obj) => obj.numberOfUser); 11 | const lectureDurationInSeconds = getSecondsBetweenTwoTimes(new Date(timeTracks[0]), 12 | new Date(timeTracks[timeTracks.length - 1])); 13 | 14 | const graphSize = 25; 15 | 16 | const timeIntervalInSeconds = lectureDurationInSeconds / graphSize; 17 | const watchers = Array(graphSize).fill(0); 18 | const numberOfInputs = Array(graphSize).fill(0); 19 | const time = Array(timeTracks.length).fill(0); 20 | 21 | for (let i = 0; i < timeTracks.length; i++) { 22 | time[i] = getSecondsBetweenTwoTimes(new Date(timeTracks[0]), 23 | new Date(timeTracks[i])); 24 | } 25 | 26 | for (let i = 1; i < watchersTracks.length - 1; i++) { 27 | const index = Math.floor(time[i] / timeIntervalInSeconds); 28 | if (watchersTracks[i] === 0) { 29 | watchers[index] += watchersTracks[i]; 30 | } else { 31 | watchers[index] += watchersTracks[i] - 1; 32 | } 33 | // watchers[index] += watchersTracks[i]; 34 | numberOfInputs[index] += 1; 35 | } 36 | 37 | const averageWatchers = []; 38 | 39 | for (let i = 0; i < graphSize; i++) { 40 | if (watchers[i] === 0) { 41 | if (i === 0) { 42 | averageWatchers[i] = 0; 43 | } else if (numberOfInputs[i] === 0) { 44 | averageWatchers[i] = averageWatchers[i - 1]; 45 | } else { 46 | averageWatchers[i] = 0; 47 | } 48 | } else { 49 | averageWatchers[i] = Math.ceil(watchers[i] / numberOfInputs[i]); 50 | } 51 | } 52 | 53 | for (let i = 0; i < graphSize; i++) { 54 | const progressBar = document.createElement('div'); 55 | progressBar.classList.add('myProgress'); 56 | // setting the class to item and active 57 | const inner = document.createElement('div'); 58 | inner.classList.add('myBar'); 59 | inner.classList.add('tooltip'); 60 | 61 | const popup = document.createElement('div'); 62 | popup.classList.add('tooltiptext'); 63 | 64 | const timer = timeIntervalInSeconds * i; 65 | 66 | const hours = Math.floor(timer / 60 / 60); 67 | const minutes = Math.floor(timer / 60) - (hours * 60); 68 | const seconds = Number((timer % 60).toPrecision(2)); 69 | 70 | const formatted = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; 71 | 72 | popup.innerHTML = `${$('#spectators-popup').val()}: ${averageWatchers[i]}
${$('#time-popup').val()}: ${formatted}`; 73 | 74 | inner.appendChild(popup); 75 | 76 | progressBar.appendChild(inner); 77 | 78 | document.getElementById('graph').appendChild(progressBar); 79 | } 80 | 81 | 82 | const elem = document.getElementsByClassName('myBar'); 83 | 84 | const maxStudents = Math.max.apply(null, averageWatchers); 85 | 86 | for (let i = 0; i < graphSize; i++) { 87 | if (averageWatchers[i] === 0) { 88 | elem[i].style.height = `${1}%`; 89 | } else { 90 | elem[i].style.height = `${(averageWatchers[i] / maxStudents) * 100}%`; 91 | } 92 | } 93 | 94 | let averageNumOfUsers = 0; 95 | for (let i = 0; i < graphSize; i++) { 96 | averageNumOfUsers += averageWatchers[i]; 97 | } 98 | 99 | document.getElementById('lecture-stats-title').innerHTML = `${statsObj.lectureName.charAt(0).toUpperCase()}${statsObj.lectureName.slice(1)}`; 100 | document.getElementById('max-specs').innerHTML = maxStudents; 101 | document.getElementById('max-specss').innerHTML = maxStudents; 102 | document.getElementById('avg-specs').innerHTML = Math.ceil(averageNumOfUsers / graphSize); 103 | document.getElementById('boards-used').innerHTML = statsObj.numOfBoards; 104 | 105 | const hours = Math.floor(lectureDurationInSeconds / 60 / 60); 106 | const minutes = Math.floor(lectureDurationInSeconds / 60) - (hours * 60); 107 | const seconds = Number((lectureDurationInSeconds % 60).toPrecision(2)); 108 | 109 | const formatted = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; 110 | document.getElementById('lecture-duration').innerHTML = formatted; 111 | document.getElementById('lecture-durationn').innerHTML = formatted; 112 | } 113 | 114 | fetch(`/lecture/stats/${getUrlId()}`, buildPostRequestOpts('')) 115 | .then((response) => { 116 | if (response.status === 200) { 117 | response.json().then((jsonResponse) => { 118 | buildGraph(jsonResponse); 119 | }); 120 | } 121 | // else display error loading stats 122 | }); 123 | -------------------------------------------------------------------------------- /public/images/SurveyMonkey_Logo.svg: -------------------------------------------------------------------------------- 1 | Horizontal_Sabaeus_RGB -------------------------------------------------------------------------------- /public/js/classes/Whiteboard.js: -------------------------------------------------------------------------------- 1 | import { showInfoMessage } from '../utility.js'; 2 | import { handleBoardsViewButtonsDisplay } from '../manager/managerBoards.js'; 3 | import Tools, { ToolsImages } from './Tools.js'; 4 | import Board from './Board.js'; 5 | 6 | export default class Whiteboard { 7 | constructor(canvasId) { 8 | this.canvas = document.getElementById(canvasId); 9 | this.canvas.height = window.innerHeight; 10 | this.canvas.width = window.innerWidth; 11 | this.context = this.canvas.getContext('2d'); 12 | this.updateCursor('pencil'); 13 | this.currentBoard = 0; 14 | this.paintWhite(); 15 | this.boards = []; 16 | this.centerCoords = []; 17 | this.undoStack = []; 18 | this.redoStack = []; 19 | } 20 | 21 | set activeTool(tool) { 22 | this.tool = tool; 23 | } 24 | 25 | set lineWidth(lineWidth) { 26 | this._lineWidth = lineWidth; 27 | this.context.lineWidth = this._lineWidth; 28 | } 29 | 30 | set selectedColor(color) { 31 | this._color = color; 32 | this.context.strokeStyle = this._color; 33 | } 34 | 35 | updateCursor(tool) { 36 | this.canvas.style.cursor = tool in ToolsImages ? `url(${ToolsImages[tool]}), auto` : 'crosshair'; 37 | } 38 | 39 | // returns a MediaStream of canvas 40 | getStream() { 41 | return this.canvas.captureStream(); 42 | } 43 | 44 | // returns an image of the current state of canvas 45 | getImage() { 46 | return this.canvas.toDataURL('image/png', 1.0); 47 | } 48 | 49 | initialize() { 50 | this.canvas.onmousedown = this.onMouseDown.bind(this); 51 | this.canvas.ontouchstart = this.onMouseDown.bind(this); 52 | this.canvas.onmouseup = this.onMouseUp.bind(this); 53 | this.canvas.ontouchend = this.onMouseUp.bind(this); 54 | window.onkeydown = this.handleShortcutKeys.bind(this); 55 | this.updateFrameInterval = window.app.updateCanvasFrame(); 56 | this.tools = new Tools(); 57 | this.onScroll(); 58 | this.handleResize(); 59 | } 60 | 61 | paintWhite() { 62 | window.app.paintBackgroundWhite(); 63 | } 64 | 65 | setZoom(point) { 66 | window.app.setZoom(point); 67 | } 68 | 69 | onMouseDown() { 70 | clearInterval(this.updateFrameInterval); 71 | this.pushToUndoStack(); 72 | this.clearRedoStack(); 73 | } 74 | 75 | onMouseUp() { 76 | this.updateFrameInterval = window.app.updateCanvasFrame(); 77 | document.onmouseup = null; 78 | document.ontouchend = null; 79 | } 80 | 81 | onScroll(e, offsetX, offsetY) { 82 | window.app.zoom(e, offsetX, offsetY); 83 | } 84 | 85 | getZoom() { 86 | return window.app.getZoomData(); 87 | } 88 | 89 | cloneItem() { 90 | window.app.copyItem(); 91 | } 92 | 93 | deleteItem() { 94 | window.app.deleteItem(); 95 | } 96 | 97 | handleResize() { 98 | let timeout; 99 | const onResizeDone = () => { 100 | handleBoardsViewButtonsDisplay(); 101 | }; 102 | $(window).on('resize', () => { 103 | clearTimeout(timeout); 104 | timeout = setTimeout(onResizeDone, 20); 105 | }); 106 | } 107 | 108 | handleShortcutKeys(e) { 109 | if (e.key === 'c' && e.ctrlKey) { 110 | this.cloneItem(); 111 | } else if (e.key === 'Backspace' || e.key === 'Delete') { 112 | this.pushToUndoStack(); 113 | this.deleteItem(); 114 | } else if (e.key === 'z' && e.ctrlKey) { 115 | this.undoPaint(); 116 | } else if (e.key === 'y' && e.ctrlKey) { 117 | this.redoPaint(); 118 | } 119 | } 120 | 121 | undoPaint() { 122 | if (this.undoStack.length > 0) { 123 | // this.context.putImageData(this.undoStack.pop(), 0, 0); 124 | this.pushToRedoStack(); 125 | const draws = this.undoStack.pop(); 126 | window.app.drawProject(draws); 127 | } else { 128 | showInfoMessage($('#nothing-to-undo').val()); 129 | } 130 | } 131 | 132 | setPaths(array) { 133 | window.app.drawProject(array); 134 | } 135 | 136 | redoPaint() { 137 | if (this.redoStack.length > 0) { 138 | this.pushToUndoStack(); 139 | const draws = this.redoStack.pop(); 140 | window.app.drawProject(draws); 141 | } else { 142 | showInfoMessage($('#nothing-to-redo').val()); 143 | } 144 | } 145 | 146 | clearRedoStack() { 147 | this.redoStack = []; 148 | } 149 | 150 | clearCanvas() { 151 | // make the canvass a blank page 152 | window.app.clear(); 153 | } 154 | 155 | getSvgImage() { 156 | return window.app.saveSVG(); 157 | } 158 | 159 | setCurrentBoard(img) { 160 | window.app.clear(); 161 | this.context.drawImage(img, 0, 0); 162 | window.app.setBackground(img.src); 163 | } 164 | 165 | makeNewBoard(zoom = null) { 166 | return new Board(window.app.saveProject(), this.getImage(), zoom); 167 | } 168 | 169 | pushToUndoStack() { 170 | var undoLimit = 40; 171 | this.saveData = window.app.saveProject(); 172 | if (this.undoStack.length >= undoLimit) this.undoStack.shift(); 173 | this.undoStack.push(this.saveData); 174 | } 175 | 176 | pushToRedoStack() { 177 | var redoLimit = 40; 178 | this.saveData = window.app.saveProject(); 179 | if (this.undoStack.length >= redoLimit) this.redoStack.shift(); 180 | this.redoStack.push(this.saveData); 181 | } 182 | 183 | addImg(imgSrc){ 184 | this.pushToUndoStack(); 185 | this.clearRedoStack(); 186 | window.app.addImg(imgSrc) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /public/images/brazil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 10 | 11 | 12 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 38 | 41 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /public/js/manager/canvasActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | createNonActiveBoardElem, addBoard, 3 | removeBoard, emitBoards, 4 | deactivateCurrentBoard, 5 | activateCurrentBoard, 6 | handleBoardsViewButtonsDisplay 7 | } from './managerBoards.js'; 8 | 9 | import { 10 | showInfoMessage, 11 | downloadFile, 12 | saveCurrentBoard, 13 | dataURItoBlob, 14 | startSpinningPage, 15 | stopSpinningPage 16 | } from '../utility.js'; 17 | 18 | export default function initializeActionsMenu(socket, whiteboard, stream) { 19 | var currPage = 1 20 | var numPages = 0 21 | var thePDF = null 22 | function handlePages(page) { 23 | var scale = 1.5 24 | var viewport = page.getViewport({scale: scale}) 25 | var canvas = document.createElement('canvas') 26 | var context = canvas.getContext('2d') 27 | canvas.height = viewport.height 28 | canvas.width = viewport.width 29 | 30 | var task = page.render({ 31 | canvasContext: context, 32 | viewport: viewport 33 | }); 34 | 35 | task.promise.then(function() { 36 | var img_b64 = canvas.toDataURL('image/png') 37 | deactivateCurrentBoard(whiteboard) 38 | whiteboard.clearCanvas(); 39 | whiteboard.addImg(img_b64) 40 | // must defer to give enough time for pdfs to load on board and be saved 41 | setTimeout(() => { 42 | createNonActiveBoardElem(socket, whiteboard, whiteboard.makeNewBoard(), true, stream) 43 | currPage++ 44 | if (thePDF !== null && currPage <= numPages) { 45 | thePDF.getPage(currPage).then(handlePages) 46 | } else { 47 | deactivateCurrentBoard(whiteboard) 48 | activateCurrentBoard(socket, whiteboard, stream, whiteboard.currentBoard - numPages + 1) 49 | emitBoards(socket, whiteboard) 50 | handleBoardsViewButtonsDisplay() 51 | stopSpinningPage() 52 | } 53 | }, 0) 54 | }); 55 | } 56 | 57 | const fileInput = document.getElementById('image-input'); 58 | 59 | fileInput.addEventListener('change', (e) => { 60 | const file = e.target.files[0]; 61 | const reader = new FileReader(); 62 | reader.onload = function (fileLoadedEvent) { 63 | if (file.type.includes('image')) { 64 | whiteboard.addImg(fileLoadedEvent.target.result) 65 | } else if (file.type.includes('pdf')) { 66 | var typedarray = new Uint8Array(fileLoadedEvent.target.result) 67 | var loadingTask = pdfjsLib.getDocument(typedarray) 68 | loadingTask.promise.then(function (pdf) { 69 | if (pdf.numPages > 30) { 70 | showInfoMessage(`${$('#pdf-too-big-msg').val()}`) 71 | } else { 72 | thePDF = pdf 73 | numPages = pdf.numPages 74 | startSpinningPage() 75 | pdf.getPage(1).then(handlePages) 76 | } 77 | }); 78 | currPage = 1 79 | numPages = 0 80 | thePDF = null 81 | } else { 82 | showInfoMessage(`${$('#invalid-file-type-msg').val()}`) 83 | } 84 | }; 85 | if (file.type.includes('image')) { 86 | reader.readAsDataURL(file) 87 | } else { 88 | reader.readAsArrayBuffer(file) 89 | } 90 | fileInput.value = null; 91 | }); 92 | 93 | window.addEventListener('paste', (event) => { 94 | // use event.originalEvent.clipboard for newer chrome versions 95 | var items = (event.clipboardData || event.originalEvent.clipboardData).items; 96 | console.log(JSON.stringify(items)); // will give you the mime types 97 | // find pasted image among pasted items 98 | var blob = null; 99 | for (var i = 0; i < items.length; i++) { 100 | if (items[i].type.indexOf("image") === 0) { 101 | blob = items[i].getAsFile(); 102 | } 103 | } 104 | // load image if there is a pasted image 105 | if (blob !== null) { 106 | var reader = new FileReader(); 107 | reader.onload = function(event) { 108 | whiteboard.addImg(event.target.result) // data url! 109 | }; 110 | reader.readAsDataURL(blob); 111 | } 112 | }); 113 | 114 | document.addEventListener("dragover", (event) => { 115 | event.preventDefault(); 116 | }); 117 | 118 | document.addEventListener('drop', (event) => { 119 | event.preventDefault(); 120 | 121 | var file = event.dataTransfer.files[0]; 122 | var reader = new FileReader(); 123 | 124 | reader.onload = function(event) { 125 | whiteboard.addImg(event.target.result) 126 | }; 127 | try { reader.readAsDataURL(file) } catch{} 128 | }) 129 | 130 | document.querySelectorAll('[data-command]').forEach((item) => { 131 | item.addEventListener('click', () => { 132 | const command = item.getAttribute('data-command'); // not doing shit here still 133 | switch (command) { 134 | case 'redo': 135 | whiteboard.redoPaint(); 136 | break; 137 | case 'undo': 138 | whiteboard.undoPaint(); 139 | break; 140 | case 'save': 141 | saveCurrentBoard(whiteboard); 142 | const zip = new JSZip(); 143 | const boardsFolder = zip.folder('boards') 144 | whiteboard.boards.forEach((board, i) => { 145 | boardsFolder.file(`board_${i+1}.png`, dataURItoBlob(board.image)) 146 | }); 147 | zip.generateAsync({type:"blob"}) 148 | .then((content) => { 149 | downloadFile(URL.createObjectURL(content), "boards.zip") 150 | showInfoMessage(`${$('#boards-saved-info').val()}: ${whiteboard.boards.length}`); 151 | }); 152 | break; 153 | case 'add-page': 154 | addBoard(socket, whiteboard, stream); 155 | break; 156 | case 'add-image': 157 | const imageInput = document.getElementById('image-input'); 158 | break; 159 | case 'remove-page': 160 | removeBoard(socket, whiteboard, stream); 161 | break; 162 | case 'clear-page': 163 | whiteboard.clearCanvas(); 164 | break; 165 | default: break; 166 | } 167 | }); 168 | }); 169 | 170 | } 171 | -------------------------------------------------------------------------------- /public/js/manager/managerBoards.js: -------------------------------------------------------------------------------- 1 | export function emitBoards(socket, whiteboard) { 2 | socket.emit('updateBoards', { 3 | boards: whiteboard.boards, 4 | activeBoardIndex: whiteboard.currentBoard, 5 | }); 6 | socket.emit('currentBoardToAll', whiteboard.boards[whiteboard.currentBoard].image); 7 | } 8 | 9 | export function handleBoardsViewButtonsDisplay() { 10 | const boardView = document.querySelector('.canvas-toggle-nav'); 11 | if (boardView.querySelectorAll('li').length >= 3 && boardView.offsetWidth === 0 && boardView.scrollWidth === 0) { 12 | $('.scroll-boards-view-right').show(); 13 | } else if (boardView.offsetWidth < boardView.scrollWidth) { 14 | if ($(boardView).scrollLeft() > 0) { 15 | $('.scroll-boards-view-left').show(); 16 | } else { 17 | $('.scroll-boards-view-left').hide(); 18 | } 19 | $('.scroll-boards-view-right').show(); 20 | if ($(boardView).scrollLeft() + boardView.offsetWidth >= boardView.scrollWidth - 20) { 21 | $('.scroll-boards-view-right').hide(); 22 | } 23 | } else { 24 | $('.scroll-boards-view-left').hide(); 25 | } 26 | } 27 | 28 | export function updateBoardsBadge() { 29 | document.querySelectorAll('.board-badge').forEach((badge, i) => { 30 | badge.innerHTML = i + 1; 31 | }); 32 | } 33 | 34 | export function deactivateCurrentBoard(whiteboard, zoom = null) { 35 | // console.log(whiteboard.getSvgImage()); 36 | whiteboard.boards[whiteboard.currentBoard] = whiteboard.makeNewBoard(zoom); 37 | const currentBoardDiv = $('[data-page=page]').eq(`${whiteboard.currentBoard}`); 38 | currentBoardDiv.find('img').attr('src', whiteboard.boards[whiteboard.currentBoard].image); 39 | currentBoardDiv.find('img').show(); 40 | currentBoardDiv.find('video')[0].srcObject = null; 41 | currentBoardDiv.find('video').hide(); 42 | } 43 | 44 | export function activateCurrentBoard(socket, whiteboard, stream, clickedBoardIndex) { 45 | whiteboard.currentBoard = clickedBoardIndex; 46 | emitBoards(socket, whiteboard); 47 | const clickedBoardDiv = $('[data-page=page]').eq(`${clickedBoardIndex}`); 48 | clickedBoardDiv.find('img').hide(); 49 | clickedBoardDiv.find('video')[0].srcObject = stream; 50 | clickedBoardDiv.find('video').show(); 51 | const newBoardImg = document.createElement('img'); 52 | newBoardImg.setAttribute('src', whiteboard.boards[clickedBoardIndex].image); 53 | const newBoardPath = whiteboard.boards[clickedBoardIndex].pathsData; 54 | setTimeout(() => { 55 | if (whiteboard.boards[whiteboard.currentBoard].zoom) { 56 | whiteboard.setZoom(whiteboard.boards[whiteboard.currentBoard].zoom) 57 | } 58 | whiteboard.setPaths(newBoardPath); 59 | socket.emit('currentBoardToAll', newBoardImg.getAttribute('src')); 60 | }, 0); 61 | } 62 | 63 | export function createNonActiveBoardElem(socket, whiteboard, board, isActive, stream) { 64 | function onClickNonActiveBoardElem() { 65 | const zoom = whiteboard.getZoom(); 66 | whiteboard.boards[whiteboard.currentBoard].zoom = zoom; 67 | deactivateCurrentBoard(whiteboard, zoom); 68 | const clickedBoardIndex = $(this).index(); 69 | activateCurrentBoard(socket, whiteboard, stream, clickedBoardIndex); 70 | } 71 | 72 | // making the new page image 73 | const newBoardImg = document.createElement('img'); 74 | 75 | newBoardImg.setAttribute('src', board.image); 76 | // setting the class to item and active 77 | const outer = document.createElement('li'); 78 | outer.classList.add('canvas-toggle-item'); 79 | 80 | outer.setAttribute('data-page', 'page'); 81 | 82 | const inner = document.createElement('a'); 83 | inner.classList.add('canvas-toggle-link'); 84 | inner.appendChild(newBoardImg); 85 | const canvasStream = document.createElement('video'); 86 | canvasStream.autoplay = true; 87 | $(canvasStream).hide(); 88 | inner.appendChild(canvasStream); 89 | outer.appendChild(inner); 90 | const pageList = document.getElementById('pagelist'); 91 | pageList.appendChild(outer); 92 | const boardBadge = document.createElement('div'); 93 | boardBadge.classList.add('board-badge'); 94 | inner.appendChild(boardBadge); 95 | 96 | whiteboard.boards[whiteboard.boards.length] = board; 97 | newBoardImg.addEventListener('click', onClickNonActiveBoardElem.bind(outer)); 98 | if (isActive) { 99 | activateCurrentBoard(socket, whiteboard, stream, whiteboard.boards.length - 1); 100 | } 101 | // must defer this for DOM to have time to update 102 | setTimeout(() => { 103 | updateBoardsBadge(); 104 | handleBoardsViewButtonsDisplay(); 105 | }, 0); 106 | } 107 | 108 | export function addBoard(socket, whiteboard, stream) { 109 | deactivateCurrentBoard(whiteboard); 110 | whiteboard.clearCanvas(); 111 | createNonActiveBoardElem(socket, whiteboard, whiteboard.makeNewBoard(), true, stream); 112 | emitBoards(socket, whiteboard); 113 | $('.canvas-toggle-nav').animate({ scrollLeft: '+=100000px' }, 150, () => { 114 | handleBoardsViewButtonsDisplay(); 115 | }); 116 | } 117 | 118 | export function removeBoard(socket, whiteboard, stream) { 119 | whiteboard.clearCanvas(); 120 | if (whiteboard.boards.length > 1) { 121 | whiteboard.boards.splice(whiteboard.currentBoard, 1); 122 | $('[data-page=page]').eq(`${whiteboard.currentBoard}`).remove(); 123 | activateCurrentBoard(socket, whiteboard, stream, whiteboard.boards.length - 1); 124 | } 125 | 126 | setTimeout(() => { 127 | handleBoardsViewButtonsDisplay(); 128 | updateBoardsBadge(); 129 | }, 0); 130 | } 131 | 132 | export default function initializeBoards(socket, whiteboard, boards, boardActive, stream) { 133 | if (boards.length > 0) { 134 | boards.forEach((board, i) => { 135 | createNonActiveBoardElem(socket, whiteboard, board, i === boardActive, stream); 136 | }); 137 | } else { 138 | createNonActiveBoardElem(socket, whiteboard, whiteboard.makeNewBoard(), true, stream); 139 | } 140 | 141 | document.querySelector('.scroll-boards-view-left').addEventListener('click', () => { 142 | $('.canvas-toggle-nav').animate({ scrollLeft: '-=120px' }, 150, () => { 143 | handleBoardsViewButtonsDisplay(); 144 | }); 145 | }); 146 | 147 | document.querySelector('.scroll-boards-view-right').addEventListener('click', () => { 148 | $('.canvas-toggle-nav').animate({ scrollLeft: '+=120px' }, 150, () => { 149 | handleBoardsViewButtonsDisplay(); 150 | }); 151 | }); 152 | } 153 | -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | const { v4: uuidv4 } = require('uuid'); 3 | const Sentry = require('@sentry/node'); 4 | const { app } = require('./servers'); 5 | const redisClient = require('./servers').client; 6 | const { logger } = require('./services/logger/logger'); 7 | const { turnCredsGenerator, janusCredsGenerator } = require('./services/credsGenerator'); 8 | const Stats = require('./models/stats'); 9 | const Manager = require('./models/manager'); 10 | const Room = require('./models/room'); 11 | const { 12 | expressPort, environment, turnServerSecret, redisTurnDbNumber, 13 | turnServerActive, turnServerPort, turnServerUrl, sentryDSN, sentryEnvironment, janusServerSecret, 14 | } = require('../config/config'); 15 | 16 | 17 | const { getLanguage, setLanguage } = require('./services/i18n/i18n'); 18 | 19 | 20 | app.get('/', (req, res) => { 21 | res.render('index.html', { sentryDSN, sentryEnvironment, ...getLanguage(req.session, req.locale) }); 22 | }); 23 | 24 | app.get('/create', (req, res) => { 25 | res.render('create.html', { sentryDSN, sentryEnvironment, ...getLanguage(req.session, req.locale) }); 26 | }); 27 | 28 | app.post('/create', (req, res) => { 29 | logger.info('POST request received: /create'); 30 | const managerId = uuidv4(); 31 | const { 32 | name, email, roomId, lectureTools, 33 | } = req.body; 34 | const newLectureStats = new Stats(name); 35 | newLectureStats.addUserTrack(new Date(), 0); 36 | redisClient.hmset('stats', { [roomId]: JSON.stringify(newLectureStats) }); 37 | redisClient.hmset('rooms', { [roomId]: JSON.stringify(new Room(name, managerId, lectureTools)) }); 38 | redisClient.hmset('managers', { [managerId]: JSON.stringify(new Manager(roomId, email)) }); 39 | 40 | logger.info('POST /create successfully added room and manager id to redis'); 41 | const redirectUrl = `/lecture/${managerId}`; 42 | res.status(200); 43 | res.send({ redirectUrl }); 44 | }); 45 | 46 | app.get('/validate/lecture', (req, res) => { 47 | logger.info(`GET request received: /validate/lecture for sessionId ${req.session.id}`); 48 | redisClient.hexists('rooms', req.query.id, (err, roomExist) => { 49 | if (roomExist) { 50 | if (req.session.inRoom) { 51 | res.status(401); 52 | res.json({ error: 'User already connected on different tab' }); 53 | } else { 54 | res.status(200); 55 | res.json({ success: 'User is ready to be connected' }); 56 | } 57 | } else { 58 | res.status(404); 59 | res.json({ error: 'Lecture does not exist' }); 60 | } 61 | }); 62 | }); 63 | 64 | app.get('/lecture/:id', (req, res) => { 65 | const urlId = req.params.id; 66 | logger.info(`GET request received: /lecture for lecture id: ${urlId}`); 67 | redisClient.hmget('managers', urlId, (err, object) => { 68 | const isGuest = object[0] === null; 69 | const roomId = !isGuest && JSON.parse(object[0]).roomId; 70 | redisClient.hmget('rooms', isGuest ? urlId : roomId, (err, room) => { 71 | let roomJson = room.pop(); 72 | if (err === null && roomJson !== null) { 73 | roomJson = JSON.parse(roomJson); 74 | const host = environment === 'DEVELOPMENT' ? `http://localhost:${expressPort}` : 'https://liteboard.io'; 75 | const sharableUrl = `${host}/lecture/${roomId}`; 76 | roomJson.id = roomId; 77 | roomJson.sharableUrl = sharableUrl; 78 | const objToRender = { 79 | sentryDSN, sentryEnvironment, ...roomJson, ...getLanguage(req.session, req.locale), 80 | }; 81 | 82 | if (isGuest) { 83 | delete roomJson.managerId; 84 | res.render('lecture.html', objToRender); 85 | } else if (roomJson.lectureTools.whiteboard) { 86 | res.render('whiteboard.html', objToRender); 87 | } else { 88 | res.render('webcamboard.html', objToRender); 89 | } 90 | } else { 91 | res.status(404); 92 | res.redirect('/error?code=3'); 93 | } 94 | }); 95 | }); 96 | }); 97 | 98 | app.get('/lecture/stats/:id', (req, res) => { 99 | const urlId = req.params.id; 100 | logger.info(`GET request received: /lecture/stats for lecture id: ${urlId}`); 101 | redisClient.hexists('rooms', urlId, (er, roomExist) => { 102 | if (roomExist) { 103 | res.status(404).redirect('/error?code=4'); 104 | } else { 105 | redisClient.hexists('stats', urlId, (er, statsExist) => { 106 | if (statsExist) { 107 | res.render('stats.html', { sentryDSN, sentryEnvironment, ...getLanguage(req.session, req.locale) }); 108 | } else { 109 | res.status(404).redirect('/error?code=3'); 110 | } 111 | }); 112 | } 113 | }); 114 | }); 115 | 116 | app.post('/lecture/stats/:id', (req, res) => { 117 | const urlId = req.params.id; 118 | logger.info(`POST request received: /lecture/stats for lecture id: ${urlId}`); 119 | redisClient.hmget('stats', urlId, (err, statsJson) => { 120 | if (err === null) { 121 | res.send(statsJson.pop()); 122 | } else { 123 | res.status(404); 124 | } 125 | }); 126 | }); 127 | 128 | app.get('/error', (req, res) => { 129 | let errType; 130 | switch (req.query.code) { 131 | case '0': errType = null; break; 132 | case '1': errType = 'PageNotFound'; break; 133 | case '2': errType = 'InvalidSession'; break; 134 | case '3': errType = 'LectureNotFound'; break; 135 | case '4': errType = 'LectureInProgress'; break; 136 | default: break; 137 | } 138 | if (errType) { 139 | res.render('error.html', { 140 | [errType]: true, sentryDSN, sentryEnvironment, ...getLanguage(req.session, req.locale), 141 | }); 142 | } else { 143 | res.redirect('/'); 144 | } 145 | }); 146 | 147 | // auths 148 | app.get('/turnCreds', (req, res) => { 149 | if (!turnServerActive) { 150 | // it was a success, but server is not active, so notifying client to not use turn servers. 151 | res.json({ active: false }); 152 | } else { 153 | redisClient.select(redisTurnDbNumber, (err) => { 154 | const name = uuidv4(); 155 | const uri = environment === 'DEVELOPMENT' ? `turn:localhost:${turnServerPort}` : `turn:${turnServerUrl}:${turnServerPort}`; 156 | 157 | if (err) res.status(500).json({ error: `Could not select correct redis db: ${err}` }); 158 | // !!lets not expose the secret!!! 159 | const { username, password } = turnCredsGenerator(name, turnServerSecret); 160 | redisClient.set(username, password, (err) => { 161 | if (err) res.status(500).json({ error: `Couldnot add turn creds to redis: ${err}` }); 162 | res.json({ 163 | username, password, ttl: 86400, uri, active: true, 164 | }); // 86400 refers to one day, recommended here https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00#section-2 165 | }); 166 | }); 167 | } 168 | }); 169 | 170 | app.get('/rtcToken', (req, res) => { 171 | const janusToken = janusCredsGenerator(['janus.plugin.videoroom'], janusServerSecret); 172 | res.json({ janusToken, ttl: 86400 }); 173 | }); 174 | 175 | app.get('/setLanguage', (req, res) => { 176 | setLanguage(req.session, req.query.langCode); 177 | res.redirect(req.query.pageRef || '/'); 178 | }); 179 | 180 | app.get('*', (req, res) => { 181 | res.redirect('/error?code=1'); 182 | }); 183 | 184 | // error handling middleware, have to specify here, refer to docs https://docs.sentry.io/platforms/node/express/, error handlers should always be defined last 185 | if (environment !== 'DEVELOPMENT') app.use(Sentry.Handlers.errorHandler()); // will capture any statusCode of 500 186 | -------------------------------------------------------------------------------- /server/services/emailer/templates/disconnectEmail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Liteboard | Lecture Disconnected 7 | 96 | 97 | 98 | 99 | 100 | 101 | 146 | 147 | 148 |
  102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 | 139 | 140 | 141 | 142 |
110 | 111 | 112 | 136 | 137 |
113 |

{{#lang}}{{emailHiThere}}{{/lang}},

114 |

{{#lang}}{{emailDisconnectedNotice}}{{/lang}}.

115 | 116 | 117 | 118 | 131 | 132 | 133 |
119 | 120 | 121 | 122 | 127 | 128 | 129 |
123 | 124 | {{#lang}}{{emailReconnect}}{{/lang}}Reconnect 125 | 126 |
130 |
134 |

{{#lang}}{{emailSlogan}}{{/lang}}

135 |
138 |
143 | 144 |
145 |
 
149 | 150 | -------------------------------------------------------------------------------- /public/js/manager/managerRTC.js: -------------------------------------------------------------------------------- 1 | import initializeStreamConfigurations from './streamConfigurations.js'; 2 | import { 3 | getJanusUrl, getTurnServers, getStunServers, displayMediaError, 4 | getJanusToken, getStatusColor, addNewSpeaker, addStream 5 | } from '../utility.js'; 6 | 7 | let janus; 8 | let isRtcEstablished = false; 9 | let canvasStream; 10 | 11 | const hasAudio = $('#audioValidator').val() === 'true'; 12 | const hasWebcam = $('#webcamValidator').val() === 'true'; 13 | const hasWhiteboard = $('#whiteboardValidator').val() === 'true'; 14 | 15 | function changeLectureStatus(status) { 16 | $('.status-dot').css('color', getStatusColor(status)); 17 | $('.lecture-running-text').html($(`#status-${status}`).val()); 18 | } 19 | 20 | export const changeStatus = { 21 | starting: () => changeLectureStatus('starting'), 22 | live: () => { changeLectureStatus('live'); isRtcEstablished = true; }, 23 | connection_lost: () => { changeLectureStatus('connection_lost'); isRtcEstablished = false; }, 24 | }; 25 | 26 | export async function initializeManagerRTC(stream, initCanvasStream) { 27 | canvasStream = initCanvasStream; 28 | const turnServers = await getTurnServers(); 29 | const janusToken = await getJanusToken(); 30 | const stunServers = getStunServers(); 31 | const roomId = $('#_id').val(); 32 | const janusUrl = getJanusUrl(); 33 | 34 | function joinFeed(publishers){ 35 | publishers.forEach((publisher) => { 36 | //if display is defined it means it's a stream from manager and we don't want to subscribe 37 | if (typeof publisher.display === 'undefined') { 38 | let remoteHandle; 39 | janus.attach({ 40 | plugin: 'janus.plugin.videoroom', 41 | success(remHandle) { 42 | remoteHandle = remHandle; 43 | remoteHandle.send({ 44 | message: { 45 | request: 'join', 46 | ptype: 'subscriber', 47 | room: parseInt(roomId), 48 | feed: publisher.id, 49 | }, 50 | }); 51 | }, 52 | onmessage(msg, offerJsep) { 53 | const event = msg.videoroom; 54 | if (event === 'attached') { 55 | remoteHandle.currentPublisherId = msg.id; 56 | } 57 | if (offerJsep) { 58 | remoteHandle.createAnswer({ 59 | jsep: offerJsep, 60 | media: { 61 | audioSend: false, 62 | videoSend: false, 63 | }, 64 | success(answerJsep) { 65 | remoteHandle.send({ 66 | message: { 67 | request: 'start', 68 | room: roomId 69 | }, 70 | jsep: answerJsep 71 | }); 72 | }, 73 | }); 74 | } 75 | }, 76 | onremotestream(stream) { 77 | const audioTrack = stream.getAudioTracks()[0]; 78 | addNewSpeaker(audioTrack, remoteHandle.currentPublisherId); 79 | } 80 | }); 81 | } 82 | }); 83 | } 84 | 85 | function publishFeed(feedStream, label) { 86 | let feedHandle; 87 | janus.attach({ 88 | plugin: 'janus.plugin.videoroom', 89 | success(handle) { 90 | feedHandle = handle; 91 | feedHandle.send({ 92 | message: { 93 | request: 'join', ptype: 'publisher', room: parseInt(roomId), display: label, 94 | }, 95 | }); 96 | }, 97 | onmessage(feedMsg, feedJsep) { 98 | if (feedJsep && feedJsep.type === 'answer') { 99 | feedHandle.handleRemoteJsep({ jsep: feedJsep }); 100 | } 101 | 102 | const status = feedMsg.videoroom; 103 | switch (status) { 104 | case 'joined': 105 | joinFeed(feedMsg.publishers); 106 | const feedRequest = { 107 | request: 'configure' 108 | }; 109 | feedRequest.video = feedStream.getVideoTracks().length > 0; 110 | feedRequest.audio = feedStream.getAudioTracks().length > 0; 111 | feedHandle.createOffer({ 112 | stream: feedStream, 113 | success(offerJsep) { 114 | feedHandle.send({ 115 | message: feedRequest, 116 | jsep: offerJsep, 117 | }); 118 | }, 119 | }); 120 | break; 121 | case 'event': 122 | if (typeof feedMsg.publishers !== 'undefined') { 123 | joinFeed(feedMsg.publishers); 124 | } 125 | break; 126 | default: 127 | break; 128 | } 129 | }, 130 | onlocalstream(localStream) { 131 | if (hasWebcam) { 132 | const webcam = document.getElementById('webcam'); 133 | 134 | const videoTracks = localStream.getTracks().filter((track) => track.kind === 'video'); 135 | videoTracks.forEach((video) => { 136 | if (typeof video.canvas === 'undefined' && video.label !== '') { 137 | addStream(webcam, video); 138 | } 139 | }); 140 | } 141 | }, 142 | webrtcState(isConnected) { 143 | setTimeout(changeStatus[isConnected ? 'live' : 'connection_lost'], 700); 144 | }, 145 | }); 146 | } 147 | 148 | Janus.init({ 149 | debug: 'all', 150 | callback() { 151 | janus = new Janus({ 152 | server: janusUrl, 153 | iceServers: [...turnServers, ...stunServers], 154 | token: janusToken, 155 | // iceTransportPolicy: 'relay', enable to force turn server 156 | success() { 157 | if (hasWhiteboard && hasWebcam) { 158 | publishFeed(stream, 'stream'); 159 | publishFeed(canvasStream, 'canvasStream'); 160 | } else if (hasWhiteboard && hasAudio) { 161 | stream.addTrack(canvasStream.getTracks()[0]); 162 | publishFeed(stream, 'canvasStream'); 163 | } else if (!hasWhiteboard) { 164 | publishFeed(stream, 'stream'); 165 | } else { 166 | publishFeed(canvasStream, 'canvasStream'); 167 | } 168 | }, 169 | }); 170 | }, 171 | }); 172 | } 173 | 174 | function reconnectStream(stream) { 175 | if (typeof janus !== 'undefined') { 176 | janus.destroy(); 177 | } 178 | changeStatus.starting(); 179 | initializeManagerRTC(stream, canvasStream); 180 | } 181 | 182 | export function initializeManagerMedia(beginLectureCb) { 183 | function getUserMedia(successCb, devices = {}) { 184 | const mediaConstraints = { 185 | audio: typeof devices.audio !== 'undefined' ? { deviceId: { exact: devices.audio } } : hasAudio, 186 | video: typeof devices.video !== 'undefined' ? { deviceId: { exact: devices.video } } : hasWebcam, 187 | }; 188 | navigator.mediaDevices.getUserMedia(mediaConstraints) 189 | .then((stream) => { 190 | successCb(stream); 191 | }) 192 | .catch(displayMediaError); 193 | } 194 | 195 | function changeDevice(stream, device) { 196 | stream.getTracks().forEach((track) => { 197 | track.stop(); 198 | stream.removeTrack(track); 199 | }); 200 | getUserMedia((updatedStream) => { 201 | updatedStream.getTracks().forEach((updatedTrack) => { 202 | stream.addTrack(updatedTrack); 203 | }); 204 | if (isRtcEstablished) { 205 | reconnectStream(stream); 206 | } 207 | }, device); 208 | } 209 | 210 | if (hasAudio || hasWebcam) { 211 | getUserMedia((stream) => { 212 | beginLectureCb(stream); 213 | initializeStreamConfigurations(stream, changeDevice); 214 | }); 215 | } else { 216 | beginLectureCb(); 217 | } 218 | 219 | $('#minimize-webcam-view').click(() => { 220 | $('#active-webcam-view').fadeOut(() => $('#inactive-webcam-view').fadeIn()); 221 | }); 222 | 223 | $('#inactive-webcam-view img').click(function () { 224 | $(this).parent().fadeOut(() => $('#active-webcam-view').fadeIn()); 225 | }); 226 | } 227 | -------------------------------------------------------------------------------- /server/services/i18n/i18n-en.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexLandingCluster": "landingCluster-en.png", 3 | "indexCreateLecture": "Create Lecture", 4 | "indexLearnMore": "Learn More", 5 | "indexLearn": "Learn", 6 | "indexTeach": "Teach", 7 | "indexFromAnywhere": "from anywhere", 8 | "indexProvidesA": "provides a", 9 | "indexDecentralizedExp": "decentralized lightweight online lecture experience", 10 | "indexWeOffer": "This means we offer, free of charge, a service that doesn't rely on accounts or downloads, and that ", 11 | "indexLowLatency": "supports an entire classroom ensuring low and stable latency.", 12 | "indexLiveStreamed": "Live Streamed Lectures", 13 | "indexLiveStreamedContent": "We use WebRTC technology to provide the best and smoothest live experience available for browsers", 14 | "indexRecordLecture": "Record Your Lessons", 15 | "indexRecordLectureContent": "You have the choice to record your lessons and make it available for students in our platform", 16 | "indexIntegratedBoard": "Integrated Whiteboard", 17 | "indexIntegratedBoardContent": "Use state-of-the-art drawing tools to create as many boards as you want with great quality and consistency", 18 | "indexAnalytics": "In-depth Analytics", 19 | "indexAnalyticsContent": "You can keep track of your class statistics by looking at the generated graph at the end of your lecture", 20 | "indexChatWithYourClass": "Chat with your Students", 21 | "indexChatWithYourClassContent": "Communicate sending messages, photos and documents to your classroom in the live lecture chat", 22 | "indexFromStudentToProf": "From students to teachers", 23 | "indexTryLiteboard": "Try Liteboard Now", 24 | "indexCreateALecture": "Create a Lecture", 25 | "createLectureDetails": "Lecture Details", 26 | "createLectureName": "Lecture Name", 27 | "createYourEmail": "Your Email", 28 | "createWhatWillYouUse": "What will you use?", 29 | "createAudio": "Audio", 30 | "createWebcam": "Webcam", 31 | "createWhiteboard": "Whiteboard", 32 | "createStartLecture": "Start Lecture", 33 | "createInvalidEmail": "Email provided is invalid", 34 | "createInvalidLectureName": "The lecture must have a name", 35 | "createInvalidToolSelection": "Select at least one teaching tool", 36 | "createLectureTopImg": "createALecture-en.png", 37 | "createBackToHome": "Back To Home", 38 | "createOptional": "Optional", 39 | "createOptionalContent": "In case you disconnect, we'll send you steps to join back on.", 40 | "deviceNotSupported":"Sorry, Liteboard isn't supported on this device yet", 41 | "footerLanguage": "Languages", 42 | "footerReportBug": "Report Bug", 43 | "reportBugTitle": "Did you find a bug?", 44 | "reportBugSubtitle": "Tell us what happened and help improve Liteboard.", 45 | "reportBugLabelName": "Your name:", 46 | "reportBugLabelEmail": "Your email:", 47 | "reportBugLabelIssue": "What Happened?", 48 | "reportBugFormError": "Some fields were invalid. Please correct the errors and try again.", 49 | "reportBugSuccess": "Your feedback has been sent. Thank you!", 50 | "reportBugLabelClose": "Close", 51 | "reportBugLabelSubmit": "Submit", 52 | "actualBugTitle": "It looks like we are having internal issues", 53 | "actualBugSubtitle": "Our team has been notified. If you would like to help, tell us what happened below.", 54 | "errorInProgress": "errorInProgress-en.png", 55 | "errorDontExist": "errorDontExist-en.png", 56 | "errorSession": "errorSession-en.png", 57 | "errorNotFound": "errorNotFound-en.png", 58 | "statsThanks": "thanksForUsingLiteboard-en.png", 59 | "statsTitleStats": "Analytics of", 60 | "statsTitle": "Lecture Analytics", 61 | "statsMaxNum": "Max of students", 62 | "statsAvgNum": "Average of students", 63 | "statsBoardsUsed": "Boards Used", 64 | "statsDuration": "Lecture duration", 65 | "statsRateExperience": "Please, rate your experience", 66 | "statusStarting": "Starting", 67 | "statusHostDisconnected": "Host Disconnected", 68 | "statusConnectionLost": "Connection Lost", 69 | "statusLive": "Live", 70 | "guestLectureRoom": "Lecture Room", 71 | "endLecture": "End Lecture", 72 | "shareLink": "Share Link", 73 | "Configure": "Configure", 74 | "lectureChat": "Lecture Chat", 75 | "readyToJoin": "Ready to Join?", 76 | "goLive": "Go Live", 77 | "testMic": "Test Microphone", 78 | "testWebcam": "Test Webcam", 79 | "shareURL": "Share Your Lecture URL", 80 | "Copy": "Copy", 81 | "voiceSettings": "Voice Settings", 82 | "inputDevice": "Input Device", 83 | "micTest": "Test your Microphone", 84 | "startMicTest": "Start a test to check up your mic!", 85 | "check": "Check", 86 | "stop": "Stop", 87 | "pencilTool": "Pencil Tool", 88 | "eraserTool": "Eraser Tool", 89 | "paintBucketTool": "Paint Bucket Tool", 90 | "selectObjectTool": "Select an Object", 91 | "Colors": "Colors", 92 | "strokeThickness": "Stroke thickness", 93 | "Shapes": "Shapes", 94 | "lineTool": "Line Tool", 95 | "circleTool": "Circle Tool", 96 | "squareTool": "Square Tool", 97 | "triangleTool": "Triangle Tool", 98 | "Redo": "Redo", 99 | "Undo": "Undo", 100 | "saveBoards": "Download Boards", 101 | "insertImageOrPDF": "Insert image or PDF", 102 | "addNewBoard": "Add a new boards", 103 | "deleteCurrentBoard": "Delete current board", 104 | "clearBoard": "Clear Board", 105 | "Mute": "Mute Microphone", 106 | "Unmute": "Unmute Microphone", 107 | "clickEndLecture": "Click to end lecture", 108 | "shareLectureLink":"Share lecture link", 109 | "lectureSettings": "Lecture Settings", 110 | "spectatorsNumber": "Number of Spectators", 111 | "openWebcamView": "Open Webcam View", 112 | "minimizeWebcamView": "Minimize Webcam View", 113 | "webcamInfo": "Webcam Info", 114 | "expandMenu": "Expand Menu", 115 | "guestScanQRCode": "Scan QR Code", 116 | "guestNoOtherBoards": "No other boards to display", 117 | "lectureMinimizeView": "Minimize View", 118 | "lectureViewFullscreen": "View Fullscreen", 119 | "lectureConnectOnPhone": "Connect on your phone", 120 | "lectureToggleBoards": "Toggle boards view", 121 | "lectureToggleChat": "Toggle lecture chat", 122 | "open": "Open", 123 | "expand": "Expand", 124 | "minimize": "Minimize", 125 | "addAttachment": "Add attachment", 126 | "send": "Send", 127 | "lectureNoteThat": "Note that your classmates will see your name", 128 | "lectureInvalidName": "Please, provide your name", 129 | "joinLecture": "Join Lecture", 130 | "devicesErrorTitle": "Failed to load input devices", 131 | "devicesErrorText": "Please, make sure the devices you have selected are connected to your computer, are not being used in another program and allow us to use them.", 132 | "devicesErrorHelp": "Here's how to fix", 133 | "liteboardDescription": "Liteboard is a decentralized lightweight platform for online real-like lecture experiences. Liteboard provides free State-of-the-Art drawing tools and unlimited time for lecture hosting. Create a lecture now!", 134 | "liteboardDescriptionLecture": "Invitation to join", 135 | "liteboardDescriptionCreate": "Create a Liteboard lecture", 136 | "liteboardDescriptionStats": "View analytics of the lecture", 137 | "errTitle": "Error", 138 | "statsPopupSpecs": "Spectators", 139 | "statsPopupTime": "Time", 140 | "waitingDevices": "Waiting for devices...", 141 | "lectureChatRoom": "Chat Room", 142 | "hostNameChat": "Room Host", 143 | "youNameChat": "You", 144 | "selectOneVideoSource": "Select one video source", 145 | "audioSettingsConfigure": "Audio Settings", 146 | "videoSettingsConfigure": "Video Settings", 147 | "nothingToUndo": "Nothing to undo.", 148 | "nothingToRedo": "Nothing to redo.", 149 | "boardsSavedInfo":"Boards Saved", 150 | "linkCopiedInfo": "Link Copied", 151 | "copiedInfo": "Copied", 152 | "copyInfo": "Copy", 153 | "emailHiThere": "Hi there", 154 | "emailDisconnectedNotice": "We noticed that you disconnected from your lecture. If this was by accident, click the button below to reconnect", 155 | "emailReconnect": "Reconnect", 156 | "emailSlogan": "Liteboard.io | Browser-based Lecture Experiences", 157 | "micConnected": "Microphone Connected", 158 | "micDisconnected": "Microphone Disconnected", 159 | "maxNumberOfPublishersTitle": "Seems there are too many students speaking already", 160 | "maxNumberOfPublishersText": "There's already other 6 students with the microphone connected and that is the maximum allowed per lecture. If you wish to speak, please ask others to mute themselves.", 161 | "invalidFileType": "Sorry, you tried to insert an invalid file to the board.\nValid types are images and PDFs.", 162 | "pdfIsTooBig": "Sorry, the PDF you tried to insert is too big. Please, try again with one containing no more than 30 pages.", 163 | "joinedChatAction": "joined", 164 | "leftChatAction": "left" 165 | } -------------------------------------------------------------------------------- /server/services/i18n/i18n-pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexLandingCluster": "landingCluster-pt.png", 3 | "indexCreateLecture": "Criar Aula", 4 | "indexLearnMore": "Leia Mais", 5 | "indexLearn": "Estude", 6 | "indexTeach": "Ensine", 7 | "indexFromAnywhere": "de qualquer lugar", 8 | "indexProvidesA": "promove uma", 9 | "indexDecentralizedExp": "experiência de aula online descentralizada e leve", 10 | "indexWeOffer": "Isso quer dizer que oferecemos, de maneira gratuita, um serviço que não requer downloads ou criação de conta, e que", 11 | "indexLowLatency": "garante latência baixa e estável para uma turma inteira.", 12 | "indexLiveStreamed": "Aulas ao Vivo", 13 | "indexLiveStreamedContent": "Utilizamos a tecnologia do WebRTC para garantir o melhor desempenho ao vivo disponivel para browsers", 14 | "indexRecordLecture": "Grave suas Aulas", 15 | "indexRecordLectureContent": "Você tem a opção de gravar suas aulas e deixá-las disponível aos alunos em nossa plataforma", 16 | "indexIntegratedBoard": "Quadro Integrado", 17 | "indexIntegratedBoardContent": "Use modernas ferramentas de desenho para criar quantos quadros você precisar com qualidade e consistência", 18 | "indexAnalytics": "Análise Detalhada", 19 | "indexAnalyticsContent": "Você tem acesso às estatísticas de sua aula com o gráfico gerado ao final da apresentação", 20 | "indexChatWithYourClass": "Fale com seus Estudantes", 21 | "indexChatWithYourClassContent": "Comunique-se mandando mensagens, fotos e documentos para sua turma no chat da aula", 22 | "indexFromStudentToProf": "De estudantes a professores", 23 | "indexTryLiteboard": "Experimente Liteboard", 24 | "indexCreateALecture": "Crie uma Aula", 25 | "createLectureDetails": "Detalhes da Aula", 26 | "createLectureName": "Nome da Aula", 27 | "createYourEmail": "Seu Email", 28 | "createWhatWillYouUse": "O que você irá usar?", 29 | "createAudio": "Audio", 30 | "createWebcam": "Webcam", 31 | "createWhiteboard": "Quadro", 32 | "createStartLecture": "Iniciar Aula", 33 | "createInvalidEmail": "Email dado não é valido", 34 | "createInvalidLectureName": "A aula precisa ter um nome", 35 | "createInvalidToolSelection": "Escolha uma ferramenta de ensino", 36 | "createLectureTopImg": "createALecture-pt.png", 37 | "createBackToHome": "Página Inicial", 38 | "createOptional": "Opcional", 39 | "createOptionalContent": "Caso você se desconecte, lhe informaremos como voltar à aula.", 40 | "deviceNotSupported":"Desculpe, Liteboard não está disponivel em seu dispositivo", 41 | "footerLanguage": "Idiomas", 42 | "footerReportBug": "Reportar Bug", 43 | "reportBugTitle": "Encontrou um bug?", 44 | "reportBugSubtitle": "Ajude a melhorar Liteboard e nos diga o que houve.", 45 | "reportBugLabelName": "Seu nome:", 46 | "reportBugLabelEmail": "Seu email:", 47 | "reportBugLabelIssue": "O que houve?", 48 | "reportBugFormError": "Por favor, preencha os campos corretamente e tente de novamente.", 49 | "reportBugSuccess": "Seu feedback foi enviado. Obrigado!", 50 | "reportBugLabelClose": "Fechar", 51 | "reportBugLabelSubmit": "Enviar", 52 | "actualBugTitle": "Parece que tivemos um erro interno", 53 | "actualBugSubtitle": "Nosso time já foi notificado. Se puder nos ajudar, diga o que aconteceu abaixo.", 54 | "errorInProgress": "errorInProgress-pt.png", 55 | "errorDontExist": "errorDontExist-pt.png", 56 | "errorSession": "errorSession-pt.png", 57 | "errorNotFound": "errorNotFound-pt.png", 58 | "statsThanks": "thanksForUsingLiteboard-pt.png", 59 | "statsTitleStats": "Estatísticas de", 60 | "statsTitle": "Estatísticas da aula", 61 | "statsMaxNum": "Máximo de alunos", 62 | "statsAvgNum": "Média de alunos", 63 | "statsBoardsUsed": "Quadros Usados", 64 | "statsDuration": "Duração da Aula", 65 | "statsRateExperience": "Por favor, nos dê seu feedback", 66 | "statusStarting": "Iniciando", 67 | "statusHostDisconnected": "Host Desconectado", 68 | "statusConnectionLost": "Conexão Perdida", 69 | "statusLive": "Ao Vivo", 70 | "guestLectureRoom": "Sala de Aula", 71 | "endLecture":"Terminar Aula", 72 | "shareLink": "Compartilhar Link", 73 | "Configure": "Configurações", 74 | "lectureChat": "Chat da Aula", 75 | "goLive": "Iniciar Aula", 76 | "testMic": "Teste o Microfone", 77 | "testWebcam": "Teste a Webcam", 78 | "shareURL": "Compartilhe a URL da sua Aula", 79 | "Copy": "Copiar", 80 | "voiceSettings": "Ajuste de Voz", 81 | "inputDevice": "Dispositivos de Entrada", 82 | "micTest": "Teste seu Microfone", 83 | "startMicTest": "Comece um teste para verificar seu microfone!", 84 | "check": "Testar", 85 | "stop": "Parar", 86 | "pencilTool": "Lápis", 87 | "eraserTool": "Borracha", 88 | "paintBucketTool": "Preencher com cor", 89 | "selectObjectTool": "Selecione um objeto", 90 | "Colors": "Cores", 91 | "strokeThickness": "Espessura do pincel", 92 | "Shapes": "Formas", 93 | "lineTool": "Segmento de reta", 94 | "circleTool": "Círculo", 95 | "squareTool": "Retângulo", 96 | "triangleTool": "Triângulo", 97 | "Redo": "Refazer", 98 | "Undo": "Desfazer", 99 | "saveBoards": "Salvar Quadros", 100 | "insertImageOrPDF": "Inserir imagem ou PDF", 101 | "addNewBoard": "Adicionar novo quadro", 102 | "deleteCurrentBoard": "Deletar quadro atual", 103 | "clearBoard": "Limpar quadro", 104 | "Mute": "Mutar Microfone", 105 | "Unmute": "Desmutar Microfone", 106 | "clickEndLecture": "Clique para terminar a aula", 107 | "shareLectureLink":"Copiar o link da aula", 108 | "lectureSettings": "Ajustes da Aula", 109 | "spectatorsNumber": "Número de Espectadores", 110 | "openWebcamView": "Abrir a janela da Webcam", 111 | "minimizeWebcamView": "Minimizar a janela da Webcam", 112 | "webcamInfo": "Informações da Webcam", 113 | "expandMenu": "Expandir Menu", 114 | "guestScanQRCode": "Scanei código QR", 115 | "guestNoOtherBoards": "Nenhum outro quadro para exibição", 116 | "lectureChatRoom": "Bate Papo", 117 | "lectureMinimizeView": "Minimizar", 118 | "lectureViewFullscreen": "Assistir em tela cheia", 119 | "lectureConnectOnPhone": "Conecte-se no seu celular", 120 | "lectureToggleBoards": "Alternar visão de quadros", 121 | "lectureToggleChat": "Alternar visão de chat", 122 | "open": "Abrir", 123 | "expand": "Expandir", 124 | "minimize": "Minimizar", 125 | "addAttachment": "Adicionar anexo", 126 | "send": "Enviar", 127 | "lectureNoteThat": "Note que outros nessa aula verão seu nome", 128 | "lectureInvalidName": "Por favor, preencha seu nome", 129 | "readyToJoin": "Pronto para entrar?", 130 | "joinLecture": "Entrar na Aula", 131 | "devicesErrorTitle": "Falha em carregar dispositivos de entrada", 132 | "devicesErrorText": "Por favor, certifique-se de que os dispositivos selecionados estão conectados no seu compudator, não estão sendo usados em outro programa e nos dê permissão de usá-los.", 133 | "devicesErrorHelp": "Como consertar?", 134 | "liteboardDescription": "Liteboard é uma plataforma descentralizada e leve para experiências reais de aula. Liteboard fornece ferramentas de desenho de última geração gratuitas e tempo ilimitado para apresentação de palestras. Crie uma aula já!", 135 | "liteboardDescriptionLecture": "Convite para participar de", 136 | "liteboardDescriptionCreate": "Crie uma aula com Liteboard", 137 | "liteboardDescriptionStats": "Veja as estatísticas da aula", 138 | "errTitle": "Erro", 139 | "statsPopupSpecs": "Espectadores", 140 | "statsPopupTime": "Tempo", 141 | "waitingDevices": "Carregando dispositivos...", 142 | "hostNameChat": "Moderador da sala", 143 | "youNameChat": "Você", 144 | "selectOneVideoSource": "Selecione uma fonte de vídeo", 145 | "audioSettingsConfigure": "Audio", 146 | "videoSettingsConfigure": "Video", 147 | "nothingToUndo": "Nada para desfazer.", 148 | "nothingToRedo": "Nada para refazer.", 149 | "boardsSavedInfo": "Quadros Salvos", 150 | "linkCopiedInfo": "Link Copiado", 151 | "copiedInfo": "Copiado", 152 | "copyInfo": "Copiar", 153 | "emailHiThere": "Olá", 154 | "emailDisconnectedNotice": "Vimos que você se desconectou de sua aula. Se isso não foi proposital, clique no botão abaixo para se reconectar", 155 | "emailReconnect": "Reconectar", 156 | "emailSlogan": "Liteboard.io | Experiências de Aula no Browser", 157 | "micConnected": "Microfone Conectado", 158 | "micDisconnected": "Microfone Desconectado", 159 | "maxNumberOfPublishersTitle": "Parece que já tem muitos outros estudantes falando", 160 | "maxNumberOfPublishersText": "Esta aula já tem outros 6 estudantes com o microfone conectado e esse é o número máximo permitido por aula. Se você deseja falar, por favor peça aos outros para se mutarem.", 161 | "invalidFileType": "Desculpe, você tentou inserir um tipo de arquivo inválido no quadro. Tipos válidos são imagens e PDFs.", 162 | "pdfIsTooBig": "Desculpe, o PDF que você tentou inserir é grande demais. Por favor, tente novamente com um que contenha não mais que 30 páginas.", 163 | "joinedChatAction": "entrou", 164 | "leftChatAction": "saiu" 165 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Liteboard.io 3 |

4 |

5 | 6 | 7 | 8 | Liteboard is released under the MIT license. 9 | 10 | 11 | Current CircleCI build status. 13 | 14 | 15 | 16 | 17 | chat on Discord 19 | 20 |

21 | 22 | Liteboard is a free, browser-based lecturing platform for anyone who wants to quickly setup a real-like classroom with State-of-the-Art drawing tools and webcam/audio broadcasts. We don't support cumbersome setups; no downloads or accounts required! Just create a lecture, and share the link. It's really that simple. 23 | 24 |
25 | 26 |
27 | 28 | Liteboard is powered by WebRTC and uses the [Janus](https://github.com/meetecho/janus-gateway) implementation of a Selective Forwarding Unit (SFU) to allow multiple participants per lecture while ensuring the lowest latency available on browsers. We host our own TURN server to guarantee support for users in any kind of network. Read about us in [this university article](https://falauniversidades.com.br/projeto-gratuito-simplifica-o-acesso-as-aulas-on-line/). 29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 |
37 | 🙌  Used to love liteboard? Please consider donating to keep the project alive 🙌 38 |
39 |
40 | 41 | ## Contents 42 | 43 | 44 | 45 | - [💡 Features](#-features) 46 | - [📝 Requirements](#-requirements) 47 | - [🏃 Getting Started](#-getting-started) 48 | - [Clone](#clone) 49 | - [Setup](#setup) 50 | - [🌲 Environment Variables](#-environment-variables) 51 | - [🔊 Contributing](#-contributing) 52 | 53 | 54 | ## 💡 Features 55 | 56 | 57 | 58 | - Live audio/video transmissions 59 | - High Quality live drawing boards 60 | - Chat rooms supporting text and attachments 61 | - Quick-to-setup lectures - no download or accounts 62 | - SFU infrastucture allowing multiple attendees 63 | - Lecture metrics with graphical interface 64 | - i18n - Portuguese | English 65 | 66 | ## 📝 Requirements 67 | 68 | To run liteboard locally, you will need the following: 69 | - [Node](https://nodejs.org/en/download/) 70 | - [Docker and Docker compose](https://docs.docker.com/get-docker/) 71 | 72 | 73 | ## 🏃 Getting Started 74 | ##### Clone 75 | - Clone this repo by running the following command `git clone https://github.com/jeverd/lecture-experience.git` 76 | 77 | #### Setup 78 | - Starting up docker containers 79 | > generating janus configuration file 80 | ```shell 81 | cd docker/docker-config 82 | cp janus/example_janus.jcfg janus/janus.jcfg # if you want play with janus configs, do it in janus.jcfg 83 | ``` 84 | > now we can start up our `janus` and `redis` containers 85 | ```shell 86 | cd .. # assuming you are in the docker-config directory 87 | docker-compose up -d # this will start up redis and janus containers 88 | ``` 89 | - Installing dependencies 90 | > now navigate to the root directory and install npm packages 91 | ```shell 92 | npm install 93 | ``` 94 | - Creating `.env` file 95 | > navigate to the config directory, create `.env` file, and then copy contents of `example.dev.env` into the file 96 | ```shell 97 | cd config # assuming you are in root directory 98 | cp example.dev.env .env #more info on env var below 99 | ``` 100 | - Running the App 101 | > now we can start the app 102 | ```shell 103 | npm start 104 | ``` 105 | 106 | 107 | ## 🌲 Environment Variables 108 | 109 | | Variable Name | Type | Description | Allowed | 110 | |---------------------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------| 111 | | NODE_ENV | string | Your environment, just keep DEVELOPMENT to work locally | DEVELOPMENT , PRODUCTION , STAGING | 112 | | REDIS_PORT | number | Sets your redis port. Make sure to only change this if you change the port in your docker-compose file | Any | 113 | | REDIS_URL | string | This is used to connect to your redis instance | Follow format as specified in example.dev.env | 114 | | EXPRESS_PORT | number | Sets the port your app is running on | Any | 115 | | SESSION_SECRET | string | Sets your express session secret. You usually don't need to touch this unless in PROD environment | Any | 116 | | SESSION_NAME | string | Sets your express session name. You usually don't need to touch this unless in PROD environment | Any | 117 | | EMAIL_USERNAME | string | This is used to send emails. If you are using gmail this is your email, if you are using something like sendGrid, they provide you with username | Any | 118 | | EMAIL_SENDER | string | This used for the "from" value when sending emails. For gmail this is just your email, for something like sendGrid this is your email that is connected to you domain. | Any | 119 | | EMAIL_SERVICE | string | This specifies the emailing service you use. For gmail it is "GMAIL" | Any | 120 | | LOGGER | bool | | true or false | 121 | | JANUS_SERVER_SECRET | string | This is used for janus webrtc gateway authentication. If you change this, please change it in janus config file as well. | Any | 122 | | TURN_SERVER_ACTIVE | bool | This specifies if you will be using a turn server. Please keep this false for dev environment | true or false | 123 | | DEFAULT_LANGUAGE | string | Sets the Liteboards default language. | en-US, pt-BR | 124 | 125 | 126 | 127 | ## 🔊 Contributing 128 | We encourge anyone interested in contributing to our project to open Pull Requests and Issues about bugs or cool features to implement. We use discord to communicate. Feel free to join the [Liteboard server](https://discord.gg/BH4akDY)! 129 | 130 | 131 | [1]: https://github.com/jeverd/lecture-experience/blob/master/LICENSE 132 | -------------------------------------------------------------------------------- /public/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 31 | 32 | 33 | Liteboard | {{#lang}}{{errTitle}}{{/lang}} 34 | 35 | 36 | 37 | 43 |
44 |
45 | 46 | 47 | 48 |
49 |
50 |
51 |
52 | {{#PageNotFound}} 53 | 54 | {{/PageNotFound}} 55 | {{#InvalidSession}} 56 | 57 | {{/InvalidSession}} 58 | {{#LectureNotFound}} 59 | 60 | {{/LectureNotFound}} 61 | {{#LectureInProgress}} 62 | 63 | {{/LectureInProgress}} 64 |
65 | 70 |
71 |
72 | 73 | 74 | 122 |
123 | 124 | 125 | 130 | 134 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /public/js/guest/guestRTC.js: -------------------------------------------------------------------------------- 1 | import { 2 | getUrlId, getJanusUrl, addStream, getTurnServers, addNewSpeaker, 3 | getStunServers, getStatusColor, getImageFromVideo, getJanusToken, 4 | showInfoMessage, displayMediaError, displayMaxPublishersReachedWarning 5 | } from '../utility.js'; 6 | 7 | const hasWebcam = $('#webcamValidator').val() === 'true'; 8 | const hasWhiteboard = $('#whiteboardValidator').val() === 'true'; 9 | const webcam = document.getElementById('webcam'); 10 | const whiteboard = document.getElementById('whiteboard'); 11 | const janusUrl = getJanusUrl(); 12 | let isCameraSwapped = false; 13 | let janus; 14 | let handle; 15 | 16 | export const changeStatus = { 17 | starting: () => { 18 | $('#lecture-status .status-dot').css('background', getStatusColor('starting')); 19 | $('#lecture-status .status-text').html($('#status-starting').val()); 20 | $('video#whiteboard').parent().addClass('running'); 21 | }, 22 | host_disconnected: () => { 23 | $('video#whiteboard').parent().addClass('running'); 24 | $('video#whiteboard').attr('srcObject', null); 25 | $('#lecture-status .status-dot').css('background', getStatusColor('host_disconnected')); 26 | $('#lecture-status .status-text').html($('#status-host-disconnected').val()); 27 | }, 28 | live: () => { 29 | $('#lecture-status .status-dot').css('background', getStatusColor('live')); 30 | $('#lecture-status .status-text').html($('#status-live').val()); 31 | $('video#whiteboard').parent().removeClass('running'); 32 | }, 33 | connection_lost: () => { 34 | $('video#whiteboard').parent().addClass('running'); 35 | $('video#whiteboard').attr('srcObject', null); 36 | $('#lecture-status .status-dot').css('background', getStatusColor('connection_lost')); 37 | $('#lecture-status .status-text').html($('#status-connection-lost').val()); 38 | }, 39 | }; 40 | 41 | export function disconnectMicrophone() { 42 | if (typeof handle !== 'undefined'){ 43 | handle.send({ 44 | message: { 45 | request: 'unpublish' 46 | } 47 | }); 48 | } 49 | } 50 | 51 | async function initializeJanus() { 52 | const roomId = parseInt(getUrlId()); 53 | function joinFeed(publishers) { 54 | publishers.forEach((publisher) => { 55 | const streamType = publisher.display; 56 | let remoteHandle; 57 | janus.attach({ 58 | plugin: 'janus.plugin.videoroom', 59 | success(remHandle) { 60 | remoteHandle = remHandle; 61 | remoteHandle.send({ 62 | message: { 63 | request: 'join', ptype: 'subscriber', room: roomId, feed: publisher.id, 64 | }, 65 | }); 66 | }, 67 | onmessage(msg, offerJsep) { 68 | const event = msg.videoroom; 69 | if (event === 'attached') { 70 | remoteHandle.currentPublisherId = msg.id; 71 | } 72 | if (offerJsep) { 73 | remoteHandle.createAnswer({ 74 | jsep: offerJsep, 75 | media: { 76 | audioSend: false, videoSend: false, 77 | }, 78 | success(answerJsep) { 79 | remoteHandle.send({ message: { request: 'start', room: roomId }, jsep: answerJsep }); 80 | }, 81 | }); 82 | } 83 | }, 84 | onremotestream(stream) { 85 | const videoTrack = stream.getVideoTracks()[0]; 86 | const audioTrack = stream.getAudioTracks()[0]; 87 | addNewSpeaker(audioTrack, remoteHandle.currentPublisherId); 88 | if (streamType === 'stream') { 89 | if (!isCameraSwapped) { 90 | addStream(hasWhiteboard ? webcam : whiteboard, videoTrack); 91 | } else { 92 | addStream(hasWhiteboard ? whiteboard : webcam, videoTrack); 93 | } 94 | } else if (streamType === 'canvasStream') { 95 | if (hasWebcam) { 96 | addStream(!isCameraSwapped ? whiteboard : webcam, videoTrack); 97 | } else { 98 | addStream(whiteboard, videoTrack); 99 | } 100 | } 101 | }, 102 | iceState(state) { 103 | switch(state) { 104 | case 'checking': 105 | changeStatus.starting(); 106 | break; 107 | case 'disconnected': 108 | changeStatus.connection_lost(); 109 | break; 110 | case 'connected': 111 | changeStatus.live(); 112 | break; 113 | default: break; 114 | } 115 | }, 116 | }); 117 | }); 118 | } 119 | 120 | const turnServers = await getTurnServers(); 121 | const janusToken = await getJanusToken(); 122 | const stunServers = getStunServers(); 123 | 124 | Janus.init({ 125 | callback() { 126 | janus = new Janus( 127 | { 128 | debug: 'all', 129 | server: janusUrl, 130 | iceServers: [...turnServers, ...stunServers], 131 | token: janusToken, 132 | // iceTransportPolicy: 'relay', enable to force turn server 133 | success() { 134 | janus.attach( 135 | { 136 | plugin: 'janus.plugin.videoroom', 137 | success(pluginHandle) { 138 | handle = pluginHandle; 139 | handle.send({ 140 | message: { 141 | request: 'join', ptype: 'publisher', room: roomId, 142 | }, 143 | }); 144 | }, 145 | onmessage(msg, feedJsep) { 146 | if (msg.configured === 'ok') { 147 | showInfoMessage($('#mic-connected-msg').val()); 148 | $('#mic-spin').hide(); 149 | $('#toggle-mic').show(); 150 | } 151 | 152 | if (msg.unpublished === 'ok') { 153 | showInfoMessage($('#mic-disconnected-msg').val()); 154 | $('#mic-spin').hide(); 155 | $('#toggle-mic').show(); 156 | } 157 | 158 | if (typeof msg.error !== 'undefined') { 159 | switch (msg.error_code) { 160 | case 432: 161 | displayMaxPublishersReachedWarning(); 162 | disconnectMicrophone(); 163 | $('#toggle-mic').removeClass('fa-microphone'); 164 | $('#toggle-mic').addClass('fa-microphone-slash'); 165 | $('#toggle-mic').show(); 166 | $('#mic-spin').hide(); 167 | default: break; 168 | } 169 | } 170 | 171 | if (feedJsep && feedJsep.type === 'answer') { 172 | handle.handleRemoteJsep({ 173 | jsep: feedJsep 174 | }); 175 | } 176 | const status = msg.videoroom; 177 | switch (status) { 178 | case 'joined': 179 | joinFeed(msg.publishers); 180 | break; 181 | case 'event': 182 | if (typeof msg.publishers !== 'undefined') { 183 | joinFeed(msg.publishers); 184 | } 185 | break; 186 | default: break; 187 | } 188 | }, 189 | }, 190 | ); 191 | }, 192 | }, 193 | ); 194 | }, 195 | }); 196 | } 197 | 198 | export default function initializeGuestRTC() { 199 | initializeJanus(); 200 | $('#expand-webcam-view').click(() => { 201 | const newPoster = getImageFromVideo(!isCameraSwapped ? whiteboard : webcam); 202 | const tmpStream = whiteboard.srcObject; 203 | whiteboard.srcObject = webcam.srcObject; 204 | webcam.srcObject = tmpStream; 205 | // picked 300 just to make sure its not an empty poster 206 | if (!isCameraSwapped) { 207 | webcam.poster = newPoster.length > 300 ? newPoster : whiteboard.poster; 208 | } else { 209 | whiteboard.poster = newPoster.length > 300 ? newPoster : webcam.poster; 210 | } 211 | isCameraSwapped = !isCameraSwapped; 212 | }); 213 | 214 | $('#minimize-webcam-view').click(() => { 215 | $('.options-webcam').fadeOut(); 216 | $('#webcam').fadeOut(() => $('#open-webcam-view').fadeIn()); 217 | }); 218 | 219 | $('#open-webcam-view').click(function () { 220 | $(this).fadeOut(() => { 221 | $('#webcam').fadeIn(); 222 | $('.options-webcam').fadeIn(); 223 | }); 224 | }); 225 | 226 | $('#toggle-mic').click(function () { 227 | $(this).toggleClass('fa-microphone-slash'); 228 | $(this).toggleClass('fa-microphone'); 229 | if (!$(this).hasClass('fa-microphone')) { 230 | disconnectMicrophone(); 231 | } else { 232 | handle.createOffer({ 233 | media: { audio: true, video: false }, 234 | success(offerJsep) { 235 | handle.send({ 236 | message: { request: 'configure', audio: true }, 237 | jsep: offerJsep, 238 | }); 239 | }, 240 | error: (err) => { 241 | $(this).removeClass('fa-microphone'); 242 | $(this).addClass('fa-microphone-slash'); 243 | $(this).show(); 244 | $('#mic-spin').hide(); 245 | displayMediaError(); 246 | } 247 | }); 248 | } 249 | $(this).hide(); 250 | $('#mic-spin').show(); 251 | }) 252 | } 253 | -------------------------------------------------------------------------------- /public/js/manager/paperTools.js: -------------------------------------------------------------------------------- 1 | var path; 2 | var items = new Group(); 3 | var imageLayer = new Layer(); 4 | var drawsLayer = new Layer(); 5 | var selectedItem = ''; 6 | var boardZoomData = []; 7 | var onDragItem; 8 | var centerPoint; 9 | var lastMousePoint = 0; 10 | drawsLayer.addChild(items); 11 | drawsLayer.activate(); 12 | imageLayer.insertBelow(drawsLayer); 13 | 14 | var displayImage = function (imgSrc, pos) { 15 | var position = pos; 16 | if (!pos) { 17 | position = view.center; 18 | } 19 | var raster = new Raster({ 20 | source: imgSrc, 21 | crossOrigin: 'anonymous', 22 | position: position, 23 | onLoad: function() { 24 | this.blendMode = 'normal'; 25 | this.position = position; 26 | } 27 | }); 28 | raster.scale(1/view.zoom); 29 | } 30 | 31 | var erase = function (event) { 32 | var hitResult = drawsLayer.hitTest(event.point); 33 | if (hitResult) { 34 | hitResult.item.opacity = 0.625; 35 | eraserTimeout = setTimeout(function () { 36 | if (hitResult) { 37 | hitResult.item.remove(); 38 | hitResult = null; 39 | } 40 | }, 100); 41 | } 42 | }; 43 | 44 | var desItem = function () { 45 | selectedItem.fullySelected = false; 46 | selectedItem = ''; 47 | }; 48 | 49 | var onChangeTool = function () { 50 | if (selectedItem) { 51 | selectedItem.fullySelected = false; 52 | selectedItem = ''; 53 | } 54 | }; 55 | 56 | var cloneItem = function () { 57 | if (selectedItem) { 58 | var clone = selectedItem.clone(); 59 | clone.position = selectedItem.position + (100, 100); 60 | selectedItem.fullySelected = false; 61 | selectedItem = clone; 62 | drawsLayer.addChild(clone) 63 | } 64 | }; 65 | 66 | var paint = function (event) { 67 | for (var i in drawsLayer.children) { 68 | var currentPath = drawsLayer.children[i]; 69 | if (currentPath.contains(event.point)) { 70 | currentPath.fillColor = activeColor; 71 | } 72 | } 73 | }; 74 | 75 | var selectItem = function (event) { 76 | var hitResult = drawsLayer.hitTest(event.point); 77 | /* COMEBACK TO THIS LATER 78 | for (var i in drawsLayer.children) { 79 | var currentPath = drawsLayer.children[i]; 80 | if (currentPath.contains(event.point)) { 81 | selectedItem = currentPath; 82 | onDragItem = currentPath; 83 | currentPath.fullySelected = true; 84 | } 85 | } 86 | */ 87 | if (hitResult) { 88 | if (selectedItem) { 89 | selectedItem.fullySelected = false; 90 | } 91 | selectedItem = hitResult.item; 92 | onDragItem = hitResult.item; 93 | selectedItem.fullySelected = true; 94 | selectedItem.bringToFront(); 95 | } 96 | if (!hitResult) { 97 | if (selectedItem) { 98 | selectedItem.fullySelected = false; 99 | selectedItem = ''; 100 | } 101 | centerPoint = { 102 | currentX: view.center.x, 103 | currentY: view.center.y 104 | }; 105 | } 106 | }; 107 | 108 | var drag = function (event) { 109 | if (onDragItem) { 110 | onDragItem.position = event.point; 111 | } else { 112 | var lastMousePoint = event.downPoint; 113 | lastViewCenter = view.center; 114 | view.center = view.center.add( 115 | lastMousePoint.subtract(event.point) 116 | ); 117 | lastMousePoint = event.point.add(view.center.subtract(lastViewCenter)); 118 | } 119 | }; 120 | 121 | var deselectItem = function (event) { 122 | onDragItem = ''; 123 | }; 124 | 125 | var changeZoomCenter = function (delta, mousePosition) { 126 | if (!delta) { 127 | return; 128 | } 129 | if (delta > 0) { 130 | var oldZoom = view.zoom; 131 | var oldCenter = view.center; 132 | var viewPos = view.viewToProject(mousePosition); 133 | var factor = 1.10; 134 | var newZoom = delta > 0 ? view.zoom * factor : view.zoom / factor; 135 | if (!newZoom) { 136 | return; 137 | } 138 | var zoomScale = oldZoom / newZoom; 139 | var centerAdjust = viewPos.subtract(oldCenter); 140 | var offset = viewPos.subtract(centerAdjust.multiply(zoomScale)).subtract(oldCenter); 141 | view.center = view.center.add(offset); 142 | } 143 | Zoom(delta); 144 | }; 145 | 146 | var Zoom = function (zoomDirection) { 147 | var zoomAmount = zoomDirection; 148 | var zoomFactor = 1.05; 149 | if (zoomDirection < 0) { 150 | newZoom = view.zoom * zoomFactor; 151 | if (view.zoom + zoomAmount <= 0.2) { 152 | view.zoom = 0.23; 153 | } else { 154 | view.zoom += zoomAmount; 155 | } 156 | } if (zoomDirection > 0) { 157 | newZoom = view.zoom / zoomFactor; 158 | } 159 | view.zoom += zoomDirection; 160 | }; 161 | 162 | var delItem = function () { 163 | if (selectedItem) { 164 | selectedItem.remove(); 165 | } 166 | }; 167 | 168 | var setPathProperties = function () { 169 | path.fillColor = 'transparent'; 170 | path.strokeColor = activeColor; 171 | path.strokeWidth = activeWidth / view.zoom; 172 | path.parent = items; 173 | }; 174 | 175 | 176 | window.app = { 177 | tools: { 178 | pencil: new Tool({ 179 | onMouseDown: function (event) { 180 | path = new Path({ 181 | index: 1000, 182 | segments: [event.point], 183 | strokeCap: 'round', 184 | sendToBack: false, 185 | }); 186 | setPathProperties(); 187 | drawsLayer.addChild(path); 188 | }, 189 | onMouseDrag: function (event) { 190 | path.add(event.point); 191 | }, 192 | onMouseUp: function (event) { 193 | path.simplify(1); 194 | }, 195 | }), 196 | pointer: new Tool({ 197 | onMouseDown: selectItem, 198 | onMouseDrag: drag, 199 | onMouseUp: deselectItem, 200 | }), 201 | bucket: new Tool({ 202 | onMouseDown: paint, 203 | }), 204 | eraser: new Tool({ 205 | onMouseDown: erase, 206 | onMouseDrag: erase, 207 | }), 208 | circle: new Tool({ 209 | onMouseDrag: function (event) { 210 | path = new Path.Circle({ 211 | center: event.downPoint, 212 | radius: (event.downPoint - event.point).length, 213 | }); 214 | setPathProperties(); 215 | drawsLayer.addChild(path); 216 | path.removeOnDrag(); 217 | }, 218 | }), 219 | square: new Tool({ 220 | onMouseDrag: function (event) { 221 | var rectangle = new Rectangle(event.downPoint, event.lastPoint); 222 | path = new Path.Rectangle(rectangle); 223 | setPathProperties(); 224 | drawsLayer.addChild(path); 225 | path.removeOnDrag(); 226 | }, 227 | }), 228 | line: new Tool({ 229 | onMouseDrag: function (event) { 230 | path = new Path.Line(event.downPoint, event.lastPoint); 231 | setPathProperties(); 232 | drawsLayer.addChild(path); 233 | path.removeOnDrag(); 234 | }, 235 | }), 236 | triangle: new Tool({ 237 | onMouseDrag: function (event) { 238 | path = new Path.RegularPolygon(event.downPoint, 3, (event.lastPoint - event.downPoint).y); 239 | setPathProperties(); 240 | drawsLayer.addChild(path); 241 | path.removeOnDrag(); 242 | }, 243 | }), 244 | }, 245 | paintBackgroundWhite: function () { 246 | var rect = new Path.Rectangle({ 247 | point: [0, 0], 248 | size: [14000, 14000], 249 | strokeColor: 'white', 250 | }); 251 | rect.fillColor = 'white'; 252 | rect.sendToBack(); 253 | imageLayer.addChild(rect); 254 | view.center = view.center.add( 255 | view.center.subtract({x: -3000, y: -3000}) 256 | ); 257 | }, 258 | updateCanvasFrame: function () { 259 | return setInterval(function() { 260 | var text = new PointText(new Point(200, 50)); 261 | text.justification = 'center'; 262 | text.content = ''; 263 | }, 500); 264 | }, 265 | clear: function () { 266 | var circle = new Path.Rectangle(new Point(0, 0), view.size.width, view.size.height); 267 | 268 | project.activeLayer.lastChild.fillColor = 'white'; 269 | 270 | drawsLayer.removeChildren(); 271 | }, 272 | setBackground: function (src) { 273 | var raster = new Raster({ 274 | source: src, 275 | position: view.center, 276 | }); 277 | }, 278 | putImage: function (src, x, y) { 279 | new Raster({ 280 | source: src, 281 | position: new Point(x, y), 282 | parent: items, 283 | }); 284 | }, 285 | zoom: function (scale, x, y) { 286 | changeZoomCenter(this.zoomDirection(scale), new Point(x, y)); 287 | }, 288 | getZoomData: function () { 289 | return { 290 | zoom: view.zoom, 291 | centerX: view.center.x, 292 | centerY: view.center.y 293 | }; 294 | }, 295 | zoomDirection: function (scale) { 296 | if (scale < 0) { 297 | // inward movement 298 | return 0.03; 299 | } else { 300 | // outward movement 301 | return -0.03; 302 | } 303 | }, 304 | saveSVG: function () { 305 | return project.exportSVG(); 306 | }, 307 | saveProject: function () { 308 | return project.activeLayer.exportJSON(); 309 | }, 310 | drawProject : function (json) { 311 | this.clear(); 312 | project.activeLayer.importJSON(json); 313 | project.view.update(); 314 | }, 315 | deselect: function () { 316 | desItem(); 317 | }, 318 | deselectOnToolChange: function () { 319 | onChangeTool(); 320 | }, 321 | copyItem: function () { 322 | cloneItem(); 323 | }, 324 | deleteItem: function () { 325 | delItem(); 326 | }, 327 | addImg: function (imgSrc) { 328 | displayImage(imgSrc); 329 | }, 330 | setZoom: function (point) { 331 | view.center = { 332 | x: point.centerX, 333 | y: point.centerY 334 | } 335 | view.zoom = point.zoom 336 | } 337 | }; 338 | --------------------------------------------------------------------------------