diff --git a/CHANGELOG.md b/CHANGELOG.md index dae1e1e..e8f64c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- socket.io for real time communication + +## Changed + +- frontend to use socket.io instead of graphql for refreshing + +### Removed + +- graphql frontend functions to send messages and refresh + ## [0.1.0] - 2019-05-19 ### Added diff --git a/app.js b/app.js index cebbece..6d8fcb4 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,6 @@ const createError = require('http-errors'), - express = require('express'), path = require('path'), + express = require('express'), cookieParser = require('cookie-parser'), logger = require('morgan'), compileSass = require('express-compile-sass'), @@ -20,6 +20,9 @@ const createError = require('http-errors'), changelogRouter = require('./routes/changelog'), bingoRouter = require('./routes/bingo'); +let app = require('express')(), + server = require('http').Server(app), + io = require('socket.io')(server); async function init() { // grapql default resolver @@ -36,9 +39,9 @@ async function init() { // database setup let pgPool = globals.pgPool; await pgPool.query(fsx.readFileSync('./sql/init.sql', 'utf-8')); - await bingoRouter.init(); - let app = express(); + let bingoIo = io.of('/bingo'); + await bingoRouter.init(bingoIo, io); // view engine setup app.set('views', path.join(__dirname, 'views')); @@ -98,7 +101,7 @@ async function init() { res.status(err.status || 500); res.render('error'); }); - return app; + return [app, server]; } module.exports = init; diff --git a/bin/www b/bin/www index f10421a..53f34d5 100644 --- a/bin/www +++ b/bin/www @@ -27,15 +27,9 @@ try { let port = normalizePort(process.env.PORT || settings.port || '3000'); -appInit().then((app) => { +appInit().then(([app, server]) => { app.set('port', port); - /** - * Create HTTP server. - */ - - let server = http.createServer(app); - /** * Listen on provided port, on all network interfaces. */ diff --git a/package-lock.json b/package-lock.json index 3522a8c..df255e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,11 @@ "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", "dev": true }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", @@ -219,6 +224,11 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -258,6 +268,11 @@ "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -347,6 +362,11 @@ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -402,6 +422,16 @@ } } }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -418,11 +448,24 @@ "tweetnacl": "^0.14.3" } }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -566,6 +609,11 @@ } } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -752,11 +800,21 @@ "delayed-stream": "~1.0.0" } }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1043,6 +1101,74 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "engine.io": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.3.2.tgz", + "integrity": "sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w==", + "requires": { + "accepts": "~1.3.4", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.0", + "ws": "~6.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "engine.io-client": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.2.tgz", + "integrity": "sha512-y0CPINnhMvPuwtqXfsGuWE8BB66+B6wTtCofQDRecMQPYX3MYUZXFNKDhdrSe3EVjgOu4V3rxdeqN/Tr91IgbQ==", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.1", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", + "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, "entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", @@ -2579,6 +2705,26 @@ "ansi-regex": "^2.0.0" } }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2706,6 +2852,11 @@ "repeating": "^2.0.0" } }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3639,6 +3790,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -3814,6 +3970,22 @@ "error-ex": "^1.2.0" } }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4833,6 +5005,90 @@ } } }, + "socket.io": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz", + "integrity": "sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w==", + "requires": { + "debug": "~4.1.0", + "engine.io": "~3.3.1", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.2.0", + "socket.io-parser": "~3.3.0" + } + }, + "socket.io-adapter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", + "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" + }, + "socket.io-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz", + "integrity": "sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "engine.io-client": "~3.3.1", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "socket.io-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", + "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, "source-map": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", @@ -5402,6 +5658,11 @@ "os-tmpdir": "~1.0.2" } }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", @@ -5777,6 +6038,19 @@ "mkdirp": "^0.5.1" } }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", @@ -5817,6 +6091,11 @@ "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" } } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" } } } diff --git a/package.json b/package.json index 1df0a93..e8716eb 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "morgan": "1.9.1", "node-sass": "4.12.0", "pg": "7.11.0", - "pug": "2.0.3" + "pug": "2.0.3", + "socket.io": "^2.2.0" }, "devDependencies": { "eslint": "^5.16.0", diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 3f95969..de97fec 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -1,5 +1,210 @@ /* eslint-disable no-unused-vars, no-undef */ +class BingoGraphqlHelper { + + /** + * Sets the username for a user + * @param username {String} - the username + * @returns {Promise} + */ + static async setUsername(username) { + username = username.replace(/^\s+|\s+$/g, ''); + let uname = username.replace(/[\n\tšŸ‘‘šŸŒŸ]|^\s+|\s+$/gu, ''); + if (uname.length === username.length) { + let response = await postGraphqlQuery(` + mutation($username:String!) { + bingo { + setUsername(username: $username) { + id + username + } + } + }`, {username: username}, '/graphql?g=' + getLobbyParam()); + if (response.status === 200) { + return response.data.bingo.setUsername.username; + } else { + if (response.errors) + showError(response.errors[0].message); + else + showError(`Failed to submit username.`); + + console.error(response); + return false; + } + } else { + showError(`Your username contains illegal characters (${username.replace(uname, '')}).`); + } + } + + /** + * Creates a lobby via the graphql endpoint + * @returns {Promise} + */ + static async createLobby() { + let response = await postGraphqlQuery(` + mutation { + bingo { + createLobby { + id + } + } + } + `); + if (response.status === 200 && response.data.bingo.createLobby) { + insertParam('g', response.data.bingo.createLobby.id); + return true; + } else { + showError('Failed to create Lobby. HTTP ERROR: ' + response.status); + console.error(response); + return false; + } + } + + /** + * Leaves a lobby via the graphql endpoint + * @returns {Promise} + */ + static async leaveLobby() { + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!){ + bingo { + mutateLobby(id:$lobbyId) { + leave + } + } + } + `, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + insertParam('g', ''); + } else { + showError('Failed to leave lobby'); + console.error(response); + } + } + + /** + * Kicks a player + * @param pid + * @returns {Promise} + */ + static async kickPlayer(pid) { + let response = await postGraphqlQuery(` + mutation ($lobbyId: ID!, $playerId:ID!) { + bingo { + mutateLobby(id: $lobbyId) { + kickPlayer(pid: $playerId) { + id + } + } + } + } + `, {lobbyId: getLobbyParam(), playerId: pid}); + if (response.status === 200) { + let kickId = response.data.bingo.mutateLobby.kickPlayer.id; + document.querySelector(`.playerEntryContainer[b-pid='${kickId}'`).remove(); + } else { + showError('Failed to kick player!'); + console.error(response); + } + } + + /** + * Loads information about the rounds winner and the round stats. + * @returns {Promise} + */ + static async loadWinnerInfo() { + let response = await postGraphqlQuery(` + query($lobbyId:ID!) { + bingo { + lobby(id:$lobbyId) { + currentRound { + status + winner { + id + username + } + start + finish + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let roundInfo = response.data.bingo.lobby.currentRound; + if (roundInfo.winner) + displayWinner(roundInfo); + else + window.location.reload(); + + } else { + console.error(response); + showError('Failed to get round information'); + } + } + + /** + * Loads the lobby wors in the words element via graphql. + * @returns {Promise} + */ + static async loadLobbyWords() { + let response = await postGraphqlQuery(` + query($lobbyId:ID!){ + bingo { + lobby(id:$lobbyId) { + words { + content + } + } + } + }`, {lobbyId: getLobbyParam()}); + + if (response.status === 200) { + let wordContainer = document.querySelector('#bingo-words'); + + if (wordContainer) + wordContainer.innerHTML = ` + ${response.data.bingo.lobby.words.map(x => x.content).join('')}`; + } else { + showError('Failed to load words.'); + } + } + + /** + * Sets the settings of the lobby + * @param words + * @param gridSize + * @returns {Promise} + */ + static async setLobbySettings(words, gridSize) { + gridSize = Number(gridSize); + let response = await postGraphqlQuery(` + mutation ($lobbyId: ID!, $words: [String!]!, $gridSize:Int!) { + bingo { + mutateLobby(id: $lobbyId) { + setWords(words: $words) { + words { + content + } + } + setGridSize(gridSize: $gridSize) { + gridSize + } + } + } + } + `, {lobbyId: getLobbyParam(), words: words, gridSize: gridSize}); + if (response.status === 200) { + return response.data.bingo.mutateLobby.setWords.words; + } else { + console.error(response); + if (response.errors) + showError(response.errors[0].message); + else + showError('Error when submitting lobby settings.'); + } + } +} + /** * Returns the value of the url-param 'g' * @returns {string} @@ -8,19 +213,7 @@ function getLobbyParam() { let matches = window.location.href.match(/\??&?g=(\d+)/); if (matches) return matches[1]; - else - return ''; -} - -/** - * REturns the value of the r url param - * @returns {string} - */ -function getRoundParam() { - let matches = window.location.href.match(/\??&?r=(\d+)/); - if (matches) - return matches[1]; - else + else return ''; } @@ -48,46 +241,13 @@ async function submitUsername() { let username = unameInput.value; if (username.length > 1 && username.length <= 30) { - return await setUsername(username); + return await BingoGraphqlHelper.setUsername(username); } else { showError('You need to provide a username (min. 2 characters, max. 30)!'); return false; } } -/** - * Sets the username for a user - * @param username {String} - the username - * @returns {Promise} - */ -async function setUsername(username) { - username = username.replace(/^\s+|\s+$/g, ''); - let uname = username.replace(/[\n\tšŸ‘‘šŸŒŸ]|^\s+|\s+$/gu, ''); - if (uname.length === username.length) { - let response = await postGraphqlQuery(` - mutation($username:String!) { - bingo { - setUsername(username: $username) { - id - username - } - } - }`, {username: username}, '/graphql?g='+getLobbyParam()); - if (response.status === 200) { - return response.data.bingo.setUsername.username; - } else { - if (response.errors) - showError(response.errors[0].message); - else - showError(`Failed to submit username.`); - console.error(response); - return false; - } - } else { - showError(`Your username contains illegal characters (${username.replace(uname, '')}).`); - } -} - /** * Function that displays the ping in the console. * @returns {Promise} @@ -120,25 +280,8 @@ async function joinLobby() { * @returns {Promise} */ async function createLobby() { - if (await submitUsername()) { - let response = await postGraphqlQuery(` - mutation { - bingo { - createLobby { - id - } - } - } - `); - if (response.status === 200 && response.data.bingo.createLobby) { - insertParam('g', response.data.bingo.createLobby.id); - return true; - } else { - showError('Failed to create Lobby. HTTP ERROR: ' + response.status); - console.error(response); - return false; - } - } + if (await submitUsername()) + await BingoGraphqlHelper.createLobby(); } /** @@ -146,21 +289,7 @@ async function createLobby() { * @returns {Promise} */ async function leaveLobby() { - let response = await postGraphqlQuery(` - mutation($lobbyId:ID!){ - bingo { - mutateLobby(id:$lobbyId) { - leave - } - } - } - `, {lobbyId: getLobbyParam()}); - if (response.status === 200) { - insertParam('g', ''); - } else { - showError('Failed to leave lobby'); - console.error(response); - } + await BingoGraphqlHelper.leaveLobby(); } /** @@ -169,24 +298,7 @@ async function leaveLobby() { * @returns {Promise} */ async function kickPlayer(pid) { - let response = await postGraphqlQuery(` - mutation ($lobbyId: ID!, $playerId:ID!) { - bingo { - mutateLobby(id: $lobbyId) { - kickPlayer(pid: $playerId) { - id - } - } - } - } - `, {lobbyId: getLobbyParam(), playerId: pid}); - if (response.status === 200) { - let kickId = response.data.bingo.mutateLobby.kickPlayer.id; - document.querySelector(`.playerEntryContainer[b-pid='${kickId}'`).remove(); - } else { - showError('Failed to kick player!'); - console.error(response); - } + await BingoGraphqlHelper.kickPlayer(pid); } /** @@ -197,11 +309,12 @@ async function executeCommand(message) { function reply(content) { addChatMessage({content: content, htmlContent: content, type: 'INFO'}); } + let jsStyle = document.querySelector('#js-style'); message = message.replace(/\s+$/g, ''); let command = /(\/\w+) ?(.*)?/g.exec(message); if (command && command.length >= 2) { - switch(command[1]) { + switch (command[1]) { case '/help': reply(`
Commands:
@@ -228,7 +341,7 @@ async function executeCommand(message) { break; case '/username': if (command[2]) { - let uname = await setUsername(command[2]); + let uname = await BingoGraphqlHelper.setUsername(command[2]); reply(`Your username is ${uname} now.`); } else { reply('You need to provide a username'); @@ -252,67 +365,11 @@ async function sendChatMessage() { if (messageInput.value && messageInput.value.length > 0) { let message = messageInput.value; messageInput.value = ''; - if (/^\/\.*/g.test(message)) { - await executeCommand(message); - } else { - let response = await postGraphqlQuery(` - mutation($lobbyId:ID!, $message:String!){ - bingo { - mutateLobby(id:$lobbyId) { - sendMessage(message:$message) { - id - htmlContent - type - author { - username - } - } - } - } - }`, {message: message, lobbyId: getLobbyParam()}); - if (response.status === 200) { - addChatMessage(response.data.bingo.mutateLobby.sendMessage); - } else { - messageInput.value = message; - console.error(response); - showError('Error when sending message.'); - } - } - } -} -/** - * Sets the words for the lobby - * @param words - * @param gridSize - * @returns {Promise} - */ -async function setLobbySettings(words, gridSize) { - gridSize = Number(gridSize); - let response = await postGraphqlQuery(` - mutation ($lobbyId: ID!, $words: [String!]!, $gridSize:Int!) { - bingo { - mutateLobby(id: $lobbyId) { - setWords(words: $words) { - words { - content - } - } - setGridSize(gridSize: $gridSize) { - gridSize - } - } - } - } - `, {lobbyId: getLobbyParam(), words: words, gridSize: gridSize}); - if (response.status === 200) { - return response.data.bingo.mutateLobby.setWords.words; - } else { - console.error(response); - if (response.errors) - showError(response.errors[0].message); + if (/^\/\.*/g.test(message)) + await executeCommand(message); else - showError('Error when submitting lobby settings.'); + socket.emit('message', message); } } @@ -325,7 +382,7 @@ async function startRound() { let words = getLobbyWords(); if (words.length > 0) { let gridSize = document.querySelector('#input-grid-size').value || 3; - let resultWords = await setLobbySettings(words, gridSize); + let resultWords = await BingoGraphqlHelper.setLobbySettings(words, gridSize); if (resultWords) { textinput.value = resultWords.map(x => x.content).join('\n'); let response = await postGraphqlQuery(` @@ -391,8 +448,9 @@ async function submitFieldToggle(wordPanel) { wordPanel.setAttribute('b-sub', response.data.bingo.mutateLobby.toggleGridField.submitted); if (response.data.bingo.mutateLobby.toggleGridField.grid.bingo) document.querySelector('#container-bingo-button').setAttribute('class', ''); - else + else document.querySelector('#container-bingo-button').setAttribute('class', 'hidden'); + } else { console.error(response); showError('Error when submitting field toggle'); @@ -502,47 +560,15 @@ async function statusWrap(func) { indicator.setAttribute('status', 'idle'); }, 1000); } catch (err) { - showError(err? err.message : 'Unknown error'); + showError(err ? err.message : 'Unknown error'); } } -/** - * Loads information about the rounds winner and the round stats. - * @returns {Promise} - */ -async function loadWinnerInfo() { - let response = await postGraphqlQuery(` - query($lobbyId:ID!) { - bingo { - lobby(id:$lobbyId) { - currentRound { - status - winner { - id - username - } - start - finish - } - } - } - }`, {lobbyId: getLobbyParam()}); - if (response.status === 200) { - let roundInfo = response.data.bingo.lobby.currentRound; - if (roundInfo.winner) - displayWinner(roundInfo); - else - window.location.reload(); - } else { - console.error(response); - showError('Failed to get round information'); - } -} /** * Adds a message to the chat * @param messageObject {Object} - the message object returned by graphql - * @param player {Number} - the id of the player + * @param [player] {Number} - the id of the player */ function addChatMessage(messageObject, player) { let msgSpan = document.createElement('span'); @@ -554,12 +580,14 @@ function addChatMessage(messageObject, player) { msgSpan.innerHTML = ` ${messageObject.author.username}: ${messageObject.htmlContent}`; - else + else msgSpan.innerHTML = ` ${messageObject.htmlContent}`; + if (messageObject.author && messageObject.author.id !== player) spawnNotification(messageObject.content, messageObject.author.username); + let chatContent = document.querySelector('#chat-content'); chatContent.appendChild(msgSpan); chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom @@ -577,231 +605,121 @@ function addPlayer(player, options) { if (options.isAdmin && player.id !== options.admin) playerContainer.innerHTML = ``; + playerContainer.innerHTML += `${player.username}`; if (player.id === options.admin) playerContainer.innerHTML += " šŸ‘‘"; + document.querySelector('#player-list').appendChild(playerContainer); } /** - * Refreshes the bingo chat - * @returns {Promise} + * Returns the current player id + * @returns {Promise<*>} */ -async function refreshChat() { - try { - let response = await postGraphqlQuery(` - query($lobbyId:ID!){ - bingo { - player { - id - } - lobby(id:$lobbyId) { - messages { - id - type - htmlContent - content - author { - id - username - } - } - } - } - }`, {lobbyId: getLobbyParam()}); - if (response.status === 200) { - let messages = response.data.bingo.lobby.messages; - for (let message of messages) - if (!document.querySelector(`.chatMessage[msg-id="${message.id}"]`)) - addChatMessage(message, response.data.bingo.player.id); - } else { - showError('Failed to refresh messages'); - console.error(response); +async function getPlayerInfo() { + let result = await postGraphqlQuery(` + query ($lobbyId:ID!) { + bingo { + player { + id + username } - } catch (err) { - showError('Failed to refresh messages'); - console.error(err); - } -} - -/** - * Refreshes the player list - * @returns {Promise} - */ -async function refreshPlayers() { - try { - let response = await postGraphqlQuery(` - query ($lobbyId: ID!) { - bingo { - player { - id - } - lobby(id: $lobbyId) { - players { - id - username - wins(lobbyId: $lobbyId) - } - admin { - id - } - } + lobby(id:$lobbyId) { + id + admin { + id } } - `, {lobbyId: getLobbyParam()}); - if (response.status === 200) { - let players = response.data.bingo.lobby.players; - let adminId = response.data.bingo.lobby.admin.id; - let isAdmin = response.data.bingo.player.id === adminId; - for (let player of players) - if (!document.querySelector(`.playerEntryContainer[b-pid="${player.id}"]`)) - addPlayer(player, {admin: adminId, isAdmin: isAdmin}); - } else { - showError('Failed to refresh players'); - console.error(response); - } - } catch (err) { - showError('Failed to refresh players'); - console.error(err); + } + }`, {lobbyId: getLobbyParam()}); + if (result.status === 200) { + let bingoData = result.data.bingo; + return { + id: bingoData.player.id, + username: bingoData.player.username, + isAdmin: bingoData.lobby.admin.id === bingoData.player.id + }; + } else { + showError('Failed to fetch player Id'); + console.error(result); } } /** - * Removes players that are not existent in the player array - * @param players {Array} - player id response of graphql + * Initializes all socket events + * @param data */ -function removeLeftPlayers(players) { - for (let playerEntry of document.querySelectorAll('.playerEntryContainer')) - if (!players.find(x => (x.id === playerEntry.getAttribute('b-pid')))) - playerEntry.remove(); -} +function initSocketEvents(data) { + let playerId = data.id; + let indicator = document.querySelector('#status-indicator'); + indicator.setAttribute('status', 'error'); -/** - * Refreshes if a player-refresh is needed. - * Removes players that are not in the lobby anyomre. - * @param players - */ -function checkPlayerRefresh(players) { - let playerRefresh = false; - removeLeftPlayers(players); - for (let player of players) - if (!document.querySelector(`.playerEntryContainer[b-pid="${player.id}"]`)) - playerRefresh = true; - if (playerRefresh) - statusWrap(refreshPlayers); -} + socket.on('connect', () => { + indicator.setAttribute('socket-status', 'connected'); + }); -/** - * Checks if messages need to be refreshed and does it if it needs to. - * @param messages - */ -function checkMessageRefresh(messages) { - let messageRefresh = false; - for (let message of messages) - if (!document.querySelector(`.chatMessage[msg-id="${message.id}"]`)) - messageRefresh = true; - if (messageRefresh) - statusWrap(refreshChat); -} + socket.on('reconnect', () => { + indicator.setAttribute('socket-status', 'connected'); + }); -/** - * refreshes the lobby and calls itself with a timeout - * @returns {Promise} - */ -async function refreshLobby() { - try { - let response = await postGraphqlQuery(` - query($lobbyId:ID!){ - bingo { - lobby(id:$lobbyId) { - players { - id - } - messages { - id - } - currentRound { - id - status - } - words { - content - } - } - } - }`, {lobbyId: getLobbyParam()}); - if (response.status === 200) { - let {players, messages, currentRound} = response.data.bingo.lobby; - checkPlayerRefresh(players); - checkMessageRefresh(messages); - let wordContainer = document.querySelector('#bingo-words'); + socket.on('disconnect', () => { + indicator.setAttribute('socket-status', 'disconnected'); + showError('Disconnected from socket!'); + }); - if (wordContainer) - wordContainer.innerHTML = ` - ${response.data.bingo.lobby.words.map(x => x.content).join('')}`; + socket.on('reconnecting', () => { + indicator.setAttribute('socket-status', 'reconnecting'); + }); - if (currentRound && currentRound.status === 'ACTIVE' && Number(currentRound.id) !== Number(getRoundParam())) { - insertParam('r', currentRound.id); - spawnNotification('The round started!', 'Bingo'); - } + socket.on('error', (error) => { + showError(`Socket Error: ${JSON.stringify(error)}`); + }); - } else { - showError('Failed to refresh lobby'); - console.error(response); + socket.on('message', (msg) => { + console.log(msg); + addChatMessage(msg, playerId); + }); + + socket.on('statusChange', (status) => { + console.log(`Status changed to ${status}`); + if (status === 'FINISHED') + BingoGraphqlHelper.loadWinnerInfo(); + else + window.location.reload(); + }); + + socket.on('playerJoin', (playerObject) => { + addPlayer(playerObject, data); + }); + + socket.on('playerLeave', (playerId) => { + document.querySelector(`.playerEntryContainer[b-pid='${playerId}']`).remove(); + }); + + socket.on('usernameChange', (playerObject) => { + console.log(playerObject); + document.querySelector(`.playerEntryContainer[b-pid='${playerObject.id}'] .playerNameSpan`).innerText = playerObject.username; + }); + + socket.on('wordsChange', async () => { + try { + await BingoGraphqlHelper.loadLobbyWords(); + } catch (err) { + showError('Failed to load new lobby words.'); } - } catch (err) { - showError('Failed to refresh lobby'); - console.error(err); - } finally { - setTimeout(refreshLobby, 1000); - } + }); } /** - * Checks the status of the lobby and the current round. - * @returns {Promise} + * Initializes the lobby refresh with sockets or graphql */ -async function refreshRound() { - let roundOver = false; - try { - let response = await postGraphqlQuery(` - query($lobbyId:ID!) { - bingo { - lobby(id:$lobbyId) { - players { - id - } - messages { - id - } - currentRound { - id - status - } - } - } - }`, {lobbyId: getLobbyParam()}); - if (response.status === 200) { - let {players, messages, currentRound} = response.data.bingo.lobby; - - checkPlayerRefresh(players); - checkMessageRefresh(messages); - if (!currentRound || currentRound.status === "FINISHED") { - roundOver = true; - await loadWinnerInfo(); - } - } else { - showError('Failed to refresh round'); - console.error(response); - } - } catch (err) { - showError('Failed to refresh round'); - console.error(err); - } finally { - if (!roundOver) - setTimeout(refreshRound, 1000); - } +function initRefresh() { + getPlayerInfo().then((data) => { + socket = new SimpleSocket(`/bingo/${getLobbyParam()}`, {playerId: data.id}); + initSocketEvents(data); + }); } window.addEventListener("unhandledrejection", function (promiseRejectionEvent) { @@ -815,7 +733,7 @@ window.addEventListener("keydown", async (e) => { e.preventDefault(); if (document.querySelector('#input-bingo-words')) { let gridSize = document.querySelector('#input-grid-size').value || 3; - await statusWrap(async () => await setLobbySettings(getLobbyWords(), gridSize)); + await statusWrap(async () => await BingoGraphqlHelper.setLobbySettings(getLobbyWords(), gridSize)); } } }, false); @@ -830,3 +748,5 @@ window.onload = async () => { } } }; + +let socket = null; diff --git a/public/javascripts/common.js b/public/javascripts/common.js index c74fd23..43769b7 100644 --- a/public/javascripts/common.js +++ b/public/javascripts/common.js @@ -1,5 +1,47 @@ /* eslint-disable no-unused-vars, no-undef */ +/** + * A simple WebSocket + */ +class SimpleSocket { + /** + * Constructor + * @param url + * @param emitContext + */ + constructor(url, emitContext) { + this.socket = io.connect(url); + this.context = emitContext; + } + + /** + * Wrapper for the emit function + * @param event + * @param data + * @param callback + */ + emit(event, data, callback) { + this.socket.emit(event, this.context, data, callback); + } + + /** + * Wrapper for on event function + * @param event + * @param callback + */ + on(event, callback) { + this.socket.on(event, callback); + } + + /** + * Returns if the socket is connected + * @returns {*|boolean} + */ + get connected() { + return this.socket.connected; + } +} + /** * HTTP POST to an url with a post body * @param url {String} - the url to post to diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index 6172c8d..409128a 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -45,6 +45,21 @@ animation-duration: 5s animation-iteration-count: infinite +.socketStatusIndicator[socket-status='connected'] + background-color: $success + +.socketStatusIndicator[socket-status='reconnecting'] + background-color: mix($pending, $error) + animation-name: pulse-opacity + animation-duration: 5s + animation-iteration-count: infinite + +.socketStatusIndicator[socket-status='disconnected'] + background-color: $error + animation-name: pulse-opacity + animation-duration: 2s + animation-iteration-count: infinite + .pending background-color: $pending !important animation-name: pulse-opacity diff --git a/routes/bingo.js b/routes/bingo.js index 26d195d..11a0fda 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -12,6 +12,7 @@ const express = require('express'), globals = require('../lib/globals'); let pgPool = globals.pgPool; +let sockets = {}; /** * Class to manage the bingo data in the database. @@ -141,6 +142,15 @@ class BingoDataManager { return await this._queryDatabase(this.queries.updateLobbyExpire.sql, [lobbyId]); } + /** + * Returns all lobby ids + * @returns {Promise<*>} + */ + async getLobbyIds() { + let results = await this._queryAllResults(this.queries.getLobbyIds.sql, []); + return results.map(x => x.id); + } + /** * Returns the row of the lobby. * @param lobbyId {Number} - the id of the lobby @@ -650,12 +660,12 @@ class GridWrapper { let gridField = new GridFieldWrapper(result); let username = await (await this.player()).username(); let word = await gridField.word.content(); + let lobbyWrapper = await this.lobby(); + if (gridField.submitted) - await bdm.addInfoMessage(this.lobbyId, - `${username} toggled "${word}"`); + await lobbyWrapper.addInfoMessage(`${username} toggled "${word}"`); else - await bdm.addInfoMessage(this.lobbyId, - `${username} untoggled "${word}"`); + await lobbyWrapper.addInfoMessage(`${username} untoggled "${word}"`); return gridField; } } @@ -881,6 +891,7 @@ class RoundWrapper { let updateResult = await bdm.setRoundWinner(this.id, winnerId); if (updateResult) await this.setFinished(); + (await this.lobby()).socket.emit('statusChange', 'FINISHED'); return true; } } @@ -894,6 +905,7 @@ class LobbyWrapper { */ constructor(id, row) { this.id = id; + this.socket = sockets[id]; this._infoLoaded = false; if (row) this._assignProperties(row); @@ -1061,6 +1073,7 @@ class LobbyWrapper { await this._createRound(); await this._createGrids(); await this.setRoundStatus('ACTIVE'); + this.socket.emit('statusChange', 'ACTIVE'); } } @@ -1115,6 +1128,7 @@ class LobbyWrapper { await this.addWord(word); for (let word of removedWords) await this.removeWord(word.id); + this.socket.emit('wordsChange'); } } @@ -1144,6 +1158,16 @@ class LobbyWrapper { }; } + /** + * Adds an info message and emits the message event. + * @param message {String} - the info messages content + * @returns {Promise} + */ + async addInfoMessage(message) { + let result = await bdm.addInfoMessage(this.id, message); + this.socket.emit('message', await resolveMessage(new MessageWrapper(result))); + } + /** * Adds a player to the lobby. * @param playerId @@ -1151,8 +1175,10 @@ class LobbyWrapper { */ async addPlayer(playerId) { await bdm.addPlayerToLobby(playerId, this.id); - let username = await new PlayerWrapper(playerId).username(); - await bdm.addInfoMessage(this.id, `${username} joined.`); + let playerWrapper = new PlayerWrapper(playerId); + this.socket.emit('playerJoin', await resolvePlayer(playerWrapper)); + let username = await playerWrapper.username(); + await this.addInfoMessage(`${username} joined.`); await this._loadLobbyInfo(true); } @@ -1164,7 +1190,8 @@ class LobbyWrapper { async removePlayer(playerId) { await bdm.removePlayerFromLobby(playerId, this.id); let username = await new PlayerWrapper(playerId).username(); - await bdm.addInfoMessage(this.id, `${username} left.`); + this.socket.emit('playerLeave', playerId); + await this.addInfoMessage(`${username} left.`); await this._loadLobbyInfo(true); } @@ -1185,7 +1212,8 @@ class LobbyWrapper { async setRoundStatus(status) { let currentRound = await this.currentRound(); await currentRound.updateStatus(status); - await bdm.addInfoMessage(this.id, `Admin set round status to ${status}`); + await this.addInfoMessage(`Admin set round status to ${status}`); + this.socket.emit('statusChange', status); if (status === 'FINISHED') await bdm.clearGrids(this.id); @@ -1395,6 +1423,39 @@ async function getGridData(lobbyId, playerId) { return {fields: fieldGrid, bingo: await grid.bingo()}; } +/** + * Resolves a message wrapper object + * @param msgWrapper + * @returns {Promise<{author: {id: (*|MessageWrapper.author.id), username: String}, id: *, content: *, timestamp: Timestamp | * | number, htmlContent: *}>} + */ +async function resolveMessage(msgWrapper) { + return { + id: msgWrapper.id, + type: msgWrapper.type, + content: msgWrapper.content, + timestamp: msgWrapper.timestamp, + htmlContent: msgWrapper.htmlContent, + author: { + id: msgWrapper.author.id, + username: await msgWrapper.author.username() + } + }; +} + +/** + * Resolves a player wrapper object + * @param playerWrapper + * @param lobbyId + * @returns {Promise<{wins: PlayerWrapper.wins, id: *, username: (String|*)}>} + */ +async function resolvePlayer(playerWrapper, lobbyId) { + return { + id: playerWrapper.id, + username: await playerWrapper.username(), + wins: await playerWrapper.wins({lobbyId: lobbyId}) + }; +} + /** * Returns resolved message data. * @param lobbyId @@ -1412,232 +1473,267 @@ async function getMessageData(lobbyId) { // -- Router stuff +/** + * Creates a lobby socket if none exists. + * @param io + * @param lobbyId + */ +function createSocketIfNotExist(io, lobbyId) { + if (!sockets[lobbyId]) { + let lobbySocket = io.of(`/bingo/${lobbyId}`); + sockets[lobbyId] = lobbySocket; + + lobbySocket.on('connection', (socket) => { + socket.on('message', async (context, message) => { + try { + let result = await bdm.addUserMessage(lobbyId, context.playerId, message); + let messageWrapper = new MessageWrapper(result); + lobbySocket.emit('message', await resolveMessage(messageWrapper)); + } catch (err) { + console.log(err); + } + }); + }); + } +} + let bdm = new BingoDataManager(pgPool); -router.init = async () => { +router.init = async (bingoIo, io) => { await bdm.init(); -}; -router.use(async (req, res, next) => { - if (req.session.bingoPlayerId) - await bdm.updatePlayerExpiration(req.session.bingoPlayerId); - next(); -}); + for (let id of await bdm.getLobbyIds()) + createSocketIfNotExist(io, id); -router.get('/', async (req, res) => { - let playerId = req.session.bingoPlayerId; - let info = req.session.acceptedCookies? null: globals.cookieInfo; - let lobbyWrapper = new LobbyWrapper(req.query.g); - let playerWrapper = new PlayerWrapper(playerId); + router.use(async (req, res, next) => { + if (req.session.bingoPlayerId) + await bdm.updatePlayerExpiration(req.session.bingoPlayerId); + next(); + }); - if (playerId && await playerWrapper.exists() && req.query.g && await lobbyWrapper.exists()) { - let lobbyId = req.query.g; - - if (!(await lobbyWrapper.roundActive() && await playerWrapper.hasGrid(lobbyId))) { - if (!await lobbyWrapper.hasPlayer(playerId)) - await lobbyWrapper.addPlayer(playerId); - let playerData = await getPlayerData(lobbyWrapper); - let words = await getWordsData(lobbyWrapper); - let admin = await lobbyWrapper.admin(); - res.render('bingo/bingo-lobby', { - players: playerData, - isAdmin: (playerId === admin.id), - adminId: admin.id, - words: words, - wordString: words.join('\n'), - gridSize: await lobbyWrapper.gridSize(), - info: info, - messages: await getMessageData(lobbyId) - }); - } else { - if (await lobbyWrapper.hasPlayer(playerId) && await playerWrapper.hasGrid(lobbyId)) { + router.get('/', async (req, res) => { + let playerId = req.session.bingoPlayerId; + let info = req.session.acceptedCookies? null: globals.cookieInfo; + let lobbyWrapper = new LobbyWrapper(req.query.g); + let playerWrapper = new PlayerWrapper(playerId); + + if (playerId && await playerWrapper.exists() && req.query.g && await lobbyWrapper.exists()) { + let lobbyId = req.query.g; + createSocketIfNotExist(io, lobbyId); + + if (!(await lobbyWrapper.roundActive() && await playerWrapper.hasGrid(lobbyId))) { + if (!await lobbyWrapper.hasPlayer(playerId)) + await lobbyWrapper.addPlayer(playerId); let playerData = await getPlayerData(lobbyWrapper); - let grid = await getGridData(lobbyId, playerId); + let words = await getWordsData(lobbyWrapper); let admin = await lobbyWrapper.admin(); - res.render('bingo/bingo-round', { + res.render('bingo/bingo-lobby', { players: playerData, - grid: grid, isAdmin: (playerId === admin.id), adminId: admin.id, + words: words, + wordString: words.join('\n'), + gridSize: await lobbyWrapper.gridSize(), info: info, messages: await getMessageData(lobbyId) }); } else { - res.redirect('/bingo'); + if (await lobbyWrapper.hasPlayer(playerId) && await playerWrapper.hasGrid(lobbyId)) { + let playerData = await getPlayerData(lobbyWrapper); + let grid = await getGridData(lobbyId, playerId); + let admin = await lobbyWrapper.admin(); + res.render('bingo/bingo-round', { + players: playerData, + grid: grid, + isAdmin: (playerId === admin.id), + adminId: admin.id, + info: info, + messages: await getMessageData(lobbyId) + }); + } else { + res.redirect('/bingo'); + } } + } else { + res.render('bingo/bingo-create', { + info: info, + username: await playerWrapper.username() + }); } - } else { - res.render('bingo/bingo-create', { - info: info, - username: await playerWrapper.username() - }); - } -}); + }); -router.graphqlResolver = async (req, res) => { - let playerId = req.session.bingoPlayerId; - if (playerId) - await bdm.updatePlayerExpiration(playerId); + router.graphqlResolver = async (req, res) => { + let playerId = req.session.bingoPlayerId; + if (playerId) + await bdm.updatePlayerExpiration(playerId); - return { - // queries - lobby: async ({id}) => { - await bdm.updateLobbyExpiration(id); - return new LobbyWrapper(id); - }, - player: ({id}) => { - if (id) - return new PlayerWrapper(id); - else + return { + // queries + lobby: async ({id}) => { + await bdm.updateLobbyExpiration(id); + return new LobbyWrapper(id); + }, + player: ({id}) => { + if (id) + return new PlayerWrapper(id); + else if (playerId) return new PlayerWrapper(playerId); else res.status(400); - }, - // mutations - setUsername: async ({username}) => { - username = replaceTagSigns(username.substring(0, 30)).replace(/[\n\tšŸ‘‘šŸŒŸ]|^\s+|\s+$/gu, ''); // only allow 30 characters - if (username.length > 0) { - let playerWrapper = new PlayerWrapper(playerId); - - if (!playerId || !(await playerWrapper.exists())) { - req.session.bingoPlayerId = (await bdm.addPlayer(username)).id; - playerId = req.session.bingoPlayerId; - } else { - let oldName = await playerWrapper.username(); - await bdm.updatePlayerUsername(playerId, username); - if (req.query.g) - await bdm.addInfoMessage(req.query.g, `${oldName} changed username to ${username}`); - } - return new PlayerWrapper(playerId); - } else { - res.status(400); - return new GraphQLError('Username too short!'); - } - }, - createLobby: async({gridSize}) => { - if (playerId) - if (gridSize > 0 && gridSize < 10) { - let result = await bdm.createLobby(playerId, gridSize); - return new LobbyWrapper(result.id); - } else { - res.status(413); - } - res.status(400); - }, - mutateLobby: async ({id}) => { - let lobbyId = id; - await bdm.updateLobbyExpiration(lobbyId); - let lobbyWrapper = new LobbyWrapper(lobbyId); - return { - join: async () => { - if (playerId) { - await lobbyWrapper.addPlayer(playerId); - return lobbyWrapper; - } else { - res.status(400); - } - }, - leave: async () => { - if (playerId) { - await lobbyWrapper.removePlayer(playerId); - return true; - } else { - res.status(400); - } - }, - kickPlayer: async ({pid}) => { - let admin = await lobbyWrapper.admin(); - if (admin.id === playerId) { - await lobbyWrapper.removePlayer(pid); - return new PlayerWrapper(pid); - } else { - res.status(403); - return new GraphQLError('You are not an admin'); - } - }, - startRound: async () => { - let admin = await lobbyWrapper.admin(); - if (admin.id === playerId) { - await lobbyWrapper.startNewRound(); - return lobbyWrapper.currentRound(); + }, + // mutations + setUsername: async ({username}) => { + username = replaceTagSigns(username.substring(0, 30)).replace(/[\n\tšŸ‘‘šŸŒŸ]|^\s+|\s+$/gu, ''); // only allow 30 characters + if (username.length > 0) { + let playerWrapper = new PlayerWrapper(playerId); + + if (!playerId || !(await playerWrapper.exists())) { + req.session.bingoPlayerId = (await bdm.addPlayer(username)).id; + playerId = req.session.bingoPlayerId; } else { - res.status(403); - return new GraphQLError('You are not an admin'); + let oldName = await playerWrapper.username(); + await bdm.updatePlayerUsername(playerId, username); + + if (req.query.g) { + let lobbyWrapper = new LobbyWrapper(req.query.g); + lobbyWrapper.socket.emit('usernameChange', + await resolvePlayer(new PlayerWrapper(playerId), req.query.g)); + await lobbyWrapper.addInfoMessage(`${oldName} changed username to ${username}`); + } } - }, - setRoundStatus: async ({status}) => { - let admin = await lobbyWrapper.admin(); - if (admin.id === playerId) { - return await lobbyWrapper.setRoundStatus(status); + return new PlayerWrapper(playerId); + } else { + res.status(400); + return new GraphQLError('Username too short!'); + } + }, + createLobby: async({gridSize}) => { + if (playerId) + if (gridSize > 0 && gridSize < 10) { + let result = await bdm.createLobby(playerId, gridSize); + createSocketIfNotExist(io, result.id); + return new LobbyWrapper(result.id); } else { - res.status(403); - return new GraphQLError('You are not an admin'); + res.status(413); } - }, - setGridSize: async ({gridSize}) => { - if (gridSize > 0 && gridSize < 6) { + res.status(400); + }, + mutateLobby: async ({id}) => { + let lobbyId = id; + createSocketIfNotExist(io, lobbyId); + await bdm.updateLobbyExpiration(lobbyId); + let lobbyWrapper = new LobbyWrapper(lobbyId); + return { + join: async () => { + if (playerId) { + await lobbyWrapper.addPlayer(playerId); + return lobbyWrapper; + } else { + res.status(400); + } + }, + leave: async () => { + if (playerId) { + await lobbyWrapper.removePlayer(playerId); + return true; + } else { + res.status(400); + } + }, + kickPlayer: async ({pid}) => { let admin = await lobbyWrapper.admin(); if (admin.id === playerId) { - await lobbyWrapper.setGridSize(gridSize); - return lobbyWrapper; + await lobbyWrapper.removePlayer(pid); + return new PlayerWrapper(pid); } else { res.status(403); return new GraphQLError('You are not an admin'); } - } else { - res.status(400); - return new GraphQLError('Grid size too big!'); - } - }, - setWords: async({words}) => { - let admin = await lobbyWrapper.admin(); - if (admin.id === playerId) - if (words.length < 10000) { - await lobbyWrapper.setWords(words); - return lobbyWrapper; + }, + startRound: async () => { + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + await lobbyWrapper.startNewRound(); + return lobbyWrapper.currentRound(); } else { - res.status(413); // request entity too large - return new GraphQLError('Too many words'); + res.status(403); + return new GraphQLError('You are not an admin'); } - else - res.status(403); // forbidden - - }, - sendMessage: async ({message}) => { - if (await lobbyWrapper.hasPlayer(playerId)) { - let result = await bdm.addUserMessage(lobbyId, playerId, message); - return new MessageWrapper(result); - } else { - res.status(401); // unautorized - return new GraphQLError('You are not in the lobby'); - } - }, - submitBingo: async () => { - let isBingo = await (await (new PlayerWrapper(playerId)).grid({lobbyId: lobbyId})).bingo(); - let currentRound = await lobbyWrapper.currentRound(); - if (isBingo && await lobbyWrapper.hasPlayer(playerId)) { - let result = await currentRound.setWinner(playerId); - let username = await new PlayerWrapper(playerId).username(); - if (result) { - await bdm.addInfoMessage(lobbyId, `**${username}** won!`); - await bdm.clearGrids(lobbyId); - return currentRound; + }, + setRoundStatus: async ({status}) => { + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + return await lobbyWrapper.setRoundStatus(status); } else { - res.status(500); + res.status(403); + return new GraphQLError('You are not an admin'); } - } else { - res.status(400); - return new GraphQLError('Bingo check failed. This is not a bingo!'); + }, + setGridSize: async ({gridSize}) => { + if (gridSize > 0 && gridSize < 6) { + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + await lobbyWrapper.setGridSize(gridSize); + return lobbyWrapper; + } else { + res.status(403); + return new GraphQLError('You are not an admin'); + } + } else { + res.status(400); + return new GraphQLError('Grid size too big!'); + } + }, + setWords: async({words}) => { + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) + if (words.length < 10000) { + await lobbyWrapper.setWords(words); + return lobbyWrapper; + } else { + res.status(413); // request entity too large + return new GraphQLError('Too many words'); + } + else + res.status(403); // forbidden + + }, + sendMessage: async ({message}) => { + if (await lobbyWrapper.hasPlayer(playerId)) { + let result = await bdm.addUserMessage(lobbyId, playerId, message); + return new MessageWrapper(result); + } else { + res.status(401); // unautorized + return new GraphQLError('You are not in the lobby'); + } + }, + submitBingo: async () => { + let isBingo = await (await (new PlayerWrapper(playerId)).grid({lobbyId: lobbyId})).bingo(); + let currentRound = await lobbyWrapper.currentRound(); + if (isBingo && await lobbyWrapper.hasPlayer(playerId)) { + let result = await currentRound.setWinner(playerId); + let username = await new PlayerWrapper(playerId).username(); + if (result) { + await bdm.addInfoMessage(lobbyId, `**${username}** won!`); + await bdm.clearGrids(lobbyId); + return currentRound; + } else { + res.status(500); + } + } else { + res.status(400); + return new GraphQLError('Bingo check failed. This is not a bingo!'); + } + }, + toggleGridField: async ({location}) => { + let {row, column} = location; + return await (await (new PlayerWrapper(playerId)).grid({lobbyId: lobbyId})) + .toggleField(row, column); } - }, - toggleGridField: async ({location}) => { - let {row, column} = location; - return await (await (new PlayerWrapper(playerId)).grid({lobbyId: lobbyId})) - .toggleField(row, column); - } - }; - } + }; + } + }; }; }; diff --git a/sql/bingo/queries.yaml b/sql/bingo/queries.yaml index 90bb6fd..0dbbf35 100644 --- a/sql/bingo/queries.yaml +++ b/sql/bingo/queries.yaml @@ -77,6 +77,9 @@ getPlayerInLobby: getLobbyPlayers: sql: SELECT * FROM bingo.lobby_players WHERE lobby_players.lobby_id = $1; +getLobbyIds: + sql: SELECT lobbys.id FROM bingo.lobbys; + # returns all direct information about the lobby # params: # - {Number} - the id of the lobby diff --git a/views/bingo/bingo-lobby.pug b/views/bingo/bingo-lobby.pug index 4e559bf..f55c263 100644 --- a/views/bingo/bingo-lobby.pug +++ b/views/bingo/bingo-lobby.pug @@ -19,4 +19,4 @@ block content include includes/bingo-chat include includes/bingo-statusbar - script(type='text/javascript') refreshLobby(); + script(type='text/javascript') initRefresh(); diff --git a/views/bingo/bingo-round.pug b/views/bingo/bingo-round.pug index 2f42f61..e811dbc 100644 --- a/views/bingo/bingo-round.pug +++ b/views/bingo/bingo-round.pug @@ -23,4 +23,4 @@ block content b-column=field.column b-sub=`${field.submitted}`) span= field.word - script(type='text/javascript') refreshRound(); + script(type='text/javascript') initRefresh(); diff --git a/views/bingo/includes/bingo-statusbar.pug b/views/bingo/includes/bingo-statusbar.pug index d9e6637..ecf5e41 100644 --- a/views/bingo/includes/bingo-statusbar.pug +++ b/views/bingo/includes/bingo-statusbar.pug @@ -1,5 +1,5 @@ div(id='statusbar') - div(id='status-indicator' class='statusIndicator' status='idle') + div(id='status-indicator' class='statusIndicator socketStatusIndicator' status='idle') span(id='error-message') span(id='container-info') a(href='https://github.com/Trivernis/whooshy/issues') Bug/Feature diff --git a/views/includes/head.pug b/views/includes/head.pug index 2c11aef..21f70fd 100644 --- a/views/includes/head.pug +++ b/views/includes/head.pug @@ -1,2 +1,3 @@ -link(rel='stylesheet', href='/sass/style.sass') -script(type='text/javascript', src='/javascripts/common.js') \ No newline at end of file +link(rel='stylesheet' href='/sass/style.sass') +script(type='text/javascript' src='/javascripts/common.js') +script(type='text/javascript' src='/socket.io/socket.io.js')