Merge pull request #23 from Trivernis/feature/WebSockets

Feature/web sockets
pull/24/head
Trivernis 6 years ago committed by GitHub
commit 65dfa26bf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,6 +4,27 @@ 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
- compression and minify
## Changed
- frontend to use socket.io instead of graphql for refreshing
- use of socket.io for toggeling binogo fields
### Removed
- graphql frontend functions to send messages and refresh
### Fixed
- Socket reconnect doesn't load old messages (#20)
- error message on create ui load
## [0.1.1] - 2019-05-21
### Fixed

@ -1,9 +1,12 @@
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'),
minify = require('express-minify'),
compression = require('compression'),
uglifyEs = require('uglify-es'),
session = require('express-session'),
pgSession = require('connect-pg-simple')(session),
fsx = require('fs-extra'),
@ -20,6 +23,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,15 +42,19 @@ 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'));
app.set('view engine', 'pug');
app.set('trust proxy', 1);
app.use(compression());
app.use(minify({
uglifyJsModule: uglifyEs
}));
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
@ -98,7 +108,7 @@ async function init() {
res.status(err.status || 500);
res.render('error');
});
return app;
return [app, server];
}
module.exports = init;

@ -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.
*/

363
package-lock.json generated

@ -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,58 @@
"delayed-stream": "~1.0.0"
}
},
"commander": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
},
"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="
},
"compressible": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz",
"integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==",
"requires": {
"mime-db": ">= 1.40.0 < 2"
}
},
"compression": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
"requires": {
"accepts": "~1.3.5",
"bytes": "3.0.0",
"compressible": "~2.0.16",
"debug": "2.6.9",
"on-headers": "~1.0.2",
"safe-buffer": "5.1.2",
"vary": "~1.1.2"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1043,6 +1138,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",
@ -1618,6 +1781,32 @@
}
}
},
"express-minify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/express-minify/-/express-minify-1.0.0.tgz",
"integrity": "sha512-04/iYxB79jGeNZBBkbAW7L7FMG4Wtu78F1SayXIKiJD6MfqYnOI3DD8no7QOntgedYCdYUpj+Skg8QWR/2WnMQ==",
"requires": {
"clean-css": "^4.1.7",
"on-headers": "^1.0.1",
"uglify-js": "^3.0.28"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"uglify-js": {
"version": "3.5.14",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.14.tgz",
"integrity": "sha512-dgyjIw8KFK6AyVl5vm2tEqPewv5TKGEiiVFLI1LbF+oHua/Njd8tZk3lIbF1AWU1rNdEg7scaceADb4zqCcWXg==",
"requires": {
"commander": "~2.20.0",
"source-map": "~0.6.1"
}
}
}
},
"express-session": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.16.1.tgz",
@ -2579,6 +2768,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 +2915,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 +3853,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 +4033,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 +5068,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 +5721,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",
@ -5526,6 +5850,27 @@
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"uglify-es": {
"version": "3.3.9",
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
"integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
"requires": {
"commander": "~2.13.0",
"source-map": "~0.6.1"
},
"dependencies": {
"commander": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
"integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA=="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"uglify-js": {
"version": "2.8.29",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
@ -5777,6 +6122,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 +6175,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="
}
}
}

@ -6,12 +6,14 @@
"start": "node ./bin/www"
},
"dependencies": {
"compression": "^1.7.4",
"connect-pg-simple": "5.0.0",
"cookie-parser": "1.4.4",
"debug": "4.1.1",
"express": "4.17.0",
"express-compile-sass": "latest",
"express-graphql": "0.8.0",
"express-minify": "^1.0.0",
"express-session": "latest",
"fs-extra": "8.0.1",
"graphql": "14.3.0",
@ -25,7 +27,9 @@
"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",
"uglify-es": "^3.3.9"
},
"devDependencies": {
"eslint": "^5.16.0",

@ -1,66 +1,13 @@
/* eslint-disable no-unused-vars, no-undef */
/**
* Returns the value of the url-param 'g'
* @returns {string}
*/
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
return '';
}
class BingoGraphqlHelper {
/**
* Spawns a notification when the window is inactive (hidden).
* @param body
* @param title
*/
function spawnNotification(body, title) {
if (Notification.permission !== 'denied' && document[getHiddenNames().hidden]) {
let options = {
body: body,
icon: '/favicon.ico'
};
let n = new Notification(title, options);
}
}
/**
* Submits the value of the username-input to set the username.
* @returns {Promise<Boolean>}
*/
async function submitUsername() {
let unameInput = document.querySelector('#input-username');
let username = unameInput.value;
if (username.length > 1 && username.length <= 30) {
return await 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<boolean>}
*/
async function setUsername(username) {
static async setUsername(username) {
username = username.replace(/^\s+|\s+$/g, '');
let uname = username.replace(/[\n\t👑🌟]|^\s+|\s+$/gu, '');
if (uname.length === username.length) {
@ -72,7 +19,7 @@ async function setUsername(username) {
username
}
}
}`, {username: username}, '/graphql?g='+getLobbyParam());
}`, {username: username}, '/graphql?g=' + getLobbyParam());
if (response.status === 200) {
return response.data.bingo.setUsername.username;
} else {
@ -80,47 +27,20 @@ async function setUsername(username) {
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<number>}
*/
async function ping() {
let start = new Date().getTime();
let response = await postGraphqlQuery(`
query {
time
}`);
console.log(`Ping: ${(new Date().getTime()) - start} ms`);
return (new Date().getTime()) - start;
}
/**
* Joins a lobby or says to create one if none is found
* @returns {Promise<void>}
*/
async function joinLobby() {
if (getLobbyParam()) {
if (await submitUsername())
window.location.reload();
} else {
showError('No lobby found. Please create one.');
}
}
/**
* Creates a lobby and redirects to the lobby.
/**
* Creates a lobby via the graphql endpoint
* @returns {Promise<boolean>}
*/
async function createLobby() {
if (await submitUsername()) {
static async createLobby() {
let response = await postGraphqlQuery(`
mutation {
bingo {
@ -139,13 +59,12 @@ async function createLobby() {
return false;
}
}
}
/**
* Lets the player leave the lobby
/**
* Leaves a lobby via the graphql endpoint
* @returns {Promise<void>}
*/
async function leaveLobby() {
static async leaveLobby() {
let response = await postGraphqlQuery(`
mutation($lobbyId:ID!){
bingo {
@ -161,14 +80,14 @@ async function leaveLobby() {
showError('Failed to leave lobby');
console.error(response);
}
}
}
/**
* Kicks a player by id.
/**
* Kicks a player
* @param pid
* @returns {Promise<void>}
*/
async function kickPlayer(pid) {
static async kickPlayer(pid) {
let response = await postGraphqlQuery(`
mutation ($lobbyId: ID!, $playerId:ID!) {
bingo {
@ -187,6 +106,240 @@ async function kickPlayer(pid) {
showError('Failed to kick player!');
console.error(response);
}
}
/**
* Loads information about the rounds winner and the round stats.
* @returns {Promise<boolean>}
*/
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<void>}
*/
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 = `<span class="bingoWord">
${response.data.bingo.lobby.words.map(x => x.content).join('</span><span class="bingoWord">')}</span>`;
} else {
showError('Failed to load words.');
}
}
/**
* Sets the settings of the lobby
* @param words
* @param gridSize
* @returns {Promise<LobbyWrapper.words|*|properties.words|{default, type}|boolean>}
*/
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.');
}
}
/**
* Refreshes the bingo chat
* @returns {Promise<void>}
*/
static async 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);
}
} catch (err) {
showError('Failed to refresh messages');
console.error(err);
}
}
}
/**
* Returns the value of the url-param 'g'
* @returns {string}
*/
function getLobbyParam() {
let matches = window.location.href.match(/\??&?g=(\d+)/);
if (matches)
return matches[1];
else
return '';
}
/**
* Spawns a notification when the window is inactive (hidden).
* @param body
* @param title
*/
function spawnNotification(body, title) {
if (Notification.permission !== 'denied' && document[getHiddenNames().hidden]) {
let options = {
body: body,
icon: '/favicon.ico'
};
let n = new Notification(title, options);
}
}
/**
* Submits the value of the username-input to set the username.
* @returns {Promise<Boolean>}
*/
async function submitUsername() {
let unameInput = document.querySelector('#input-username');
let username = unameInput.value;
if (username.length > 1 && username.length <= 30) {
return await BingoGraphqlHelper.setUsername(username);
} else {
showError('You need to provide a username (min. 2 characters, max. 30)!');
return false;
}
}
/**
* Function that displays the ping in the console.
* @returns {Promise<number>}
*/
async function ping() {
let start = new Date().getTime();
let response = await postGraphqlQuery(`
query {
time
}`);
console.log(`Ping: ${(new Date().getTime()) - start} ms`);
return (new Date().getTime()) - start;
}
/**
* Joins a lobby or says to create one if none is found
* @returns {Promise<void>}
*/
async function joinLobby() {
if (getLobbyParam()) {
if (await submitUsername())
window.location.reload();
} else {
showError('No lobby found. Please create one.');
}
}
/**
* Creates a lobby and redirects to the lobby.
* @returns {Promise<boolean>}
*/
async function createLobby() {
if (await submitUsername())
await BingoGraphqlHelper.createLobby();
}
/**
* Lets the player leave the lobby
* @returns {Promise<void>}
*/
async function leaveLobby() {
await BingoGraphqlHelper.leaveLobby();
}
/**
* Kicks a player by id.
* @param pid
* @returns {Promise<void>}
*/
async function kickPlayer(pid) {
await BingoGraphqlHelper.kickPlayer(pid);
}
/**
@ -197,11 +350,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(`
<br><b>Commands: </b><br>
@ -228,7 +382,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 <b>${uname}</b> now.`);
} else {
reply('You need to provide a username');
@ -252,67 +406,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<LobbyWrapper.words|*|properties.words|{default, type}|boolean>}
*/
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);
}
}
@ -321,11 +419,13 @@ async function setLobbySettings(words, gridSize) {
* @returns {Promise<boolean>}
*/
async function startRound() {
let roundStart = document.querySelector('#button-round-start');
let textinput = document.querySelector('#input-bingo-words');
let words = getLobbyWords();
if (words.length > 0) {
roundStart.setAttribute('class', 'pending');
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(`
@ -345,6 +445,7 @@ async function startRound() {
console.error(response);
showError('Error when starting round.');
}
roundStart.setAttribute('class', '');
}
} else {
throw new Error('No words provided.');
@ -372,31 +473,8 @@ async function submitFieldToggle(wordPanel) {
let column = Number(wordPanel.getAttribute('b-column'));
let wordClass = wordPanel.getAttribute('class');
wordPanel.setAttribute('class', wordClass + ' pending');
let response = await postGraphqlQuery(`
mutation($lobbyId:ID!, $row:Int!, $column:Int!){
bingo {
mutateLobby(id:$lobbyId) {
toggleGridField(location:{row:$row, column:$column}) {
submitted
grid {
bingo
}
}
}
}
}`, {lobbyId: getLobbyParam(), row: row, column: column});
wordPanel.setAttribute('class', wordClass);
if (response.status === 200) {
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
document.querySelector('#container-bingo-button').setAttribute('class', 'hidden');
} else {
console.error(response);
showError('Error when submitting field toggle');
}
socket.emit('fieldToggle', {row: row, column: column});
}
/**
@ -425,7 +503,7 @@ async function setRoundFinished() {
/**
* Submits bingo
* @returns {Promise<void>}
* @returns {Promise<boolean>}
*/
async function submitBingo() {
let response = await postGraphqlQuery(`
@ -446,8 +524,7 @@ async function submitBingo() {
}`, {lobbyId: getLobbyParam()});
if (response.status === 200 && response.data.bingo.mutateLobby.submitBingo) {
let round = response.data.bingo.mutateLobby.submitBingo;
displayWinner(round);
return true;
} else {
console.error(response);
showError('Failed to submit bingo');
@ -456,10 +533,10 @@ async function submitBingo() {
/**
* Displays the winner of the game in a popup.
* @param roundInfo {Object} - the round object as returned by graphql
* @param winner {Object} - the round object as returned by graphql
*/
function displayWinner(roundInfo) {
let name = roundInfo.winner.username;
function displayWinner(winner) {
let name = winner.username;
let winnerDiv = document.createElement('div');
let greyoverDiv = document.createElement('div');
winnerDiv.setAttribute('class', 'popup');
@ -502,47 +579,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<boolean>}
*/
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');
@ -558,8 +603,10 @@ function addChatMessage(messageObject, player) {
msgSpan.innerHTML = `
<span class="chatMessageContent ${messageObject.type}">${messageObject.htmlContent}</span>`;
if (messageObject.author && messageObject.author.id !== player)
if (messageObject.type === 'USER' && 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 +624,133 @@ function addPlayer(player, options) {
if (options.isAdmin && player.id !== options.admin)
playerContainer.innerHTML = `<button class="kickPlayerButton" onclick="kickPlayer(${player.id})"></button>`;
playerContainer.innerHTML += `<span class="playernameSpan">${player.username}</span>`;
if (player.id === options.admin)
playerContainer.innerHTML += "<span class='adminSpan'> 👑</span>";
document.querySelector('#player-list').appendChild(playerContainer);
}
/**
* Refreshes the bingo chat
* @returns {Promise<void>}
* Returns the current player id
* @returns {Promise<*>}
*/
async function refreshChat() {
try {
let response = await postGraphqlQuery(`
query($lobbyId:ID!){
async function getPlayerInfo() {
let result = await postGraphqlQuery(`
query ($lobbyId:ID!) {
bingo {
player {
id
username
}
lobby(id:$lobbyId) {
messages {
id
type
htmlContent
content
author {
admin {
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);
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 refresh messages');
console.error(response);
}
} catch (err) {
showError('Failed to refresh messages');
console.error(err);
showError('Failed to fetch player Id');
console.error(result);
}
}
/**
* Refreshes the player list
* @returns {Promise<void>}
* Initializes all socket events
* @param data
*/
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
}
}
}
}
`, {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);
}
}
function initSocketEvents(data) {
let playerId = data.id;
let indicator = document.querySelector('#status-indicator');
indicator.setAttribute('status', 'error');
/**
* Removes players that are not existent in the player array
* @param players {Array<Object>} - player id response of graphql
*/
function removeLeftPlayers(players) {
for (let playerEntry of document.querySelectorAll('.playerEntryContainer'))
if (!players.find(x => (x.id === playerEntry.getAttribute('b-pid'))))
playerEntry.remove();
}
socket.on('connect', () => {
indicator.setAttribute('socket-status', 'connected');
});
/**
* 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('reconnect', async () => {
indicator.setAttribute('socket-status', 'connected');
await BingoGraphqlHelper.refreshChat();
});
/**
* 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('disconnect', () => {
indicator.setAttribute('socket-status', 'disconnected');
showError('Disconnected from socket!');
});
/**
* refreshes the lobby and calls itself with a timeout
* @returns {Promise<void>}
*/
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('reconnecting', () => {
indicator.setAttribute('socket-status', 'reconnecting');
});
if (wordContainer)
wordContainer.innerHTML = `<span class="bingoWord">
${response.data.bingo.lobby.words.map(x => x.content).join('</span><span class="bingoWord">')}</span>`;
socket.on('error', (error) => {
showError(`Socket Error: ${JSON.stringify(error)}`);
});
if (currentRound && currentRound.status === 'ACTIVE' && Number(currentRound.id) !== Number(getRoundParam())) {
insertParam('r', currentRound.id);
spawnNotification('The round started!', 'Bingo');
}
socket.on('message', (msg) => {
addChatMessage(msg, playerId);
});
socket.on('statusChange', (status, winner) => {
if (status === 'FINISHED' && winner) {
if (document.querySelector('#container-bingo-round'))
displayWinner(winner);
} else {
showError('Failed to refresh lobby');
console.error(response);
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) => {
document.querySelector(`.playerEntryContainer[b-pid='${playerObject.id}'] .playerNameSpan`).innerText = playerObject.username;
});
socket.on('wordsChange', async () => {
try {
await BingoGraphqlHelper.loadLobbyWords();
} catch (err) {
showError('Failed to refresh lobby');
console.error(err);
} finally {
setTimeout(refreshLobby, 1000);
showError('Failed to load new lobby words.');
}
});
socket.on('fieldChange', (field) => {
let wordPanel = document.querySelector(`.bingoWordPanel[b-row='${field.row}'][b-column='${field.column}']`);
wordPanel.setAttribute('b-sub', field.submitted);
wordPanel.setAttribute('class', 'bingoWordPanel');
if (field.bingo)
document.querySelector('#container-bingo-button').setAttribute('class', '');
else
document.querySelector('#container-bingo-button').setAttribute('class', 'hidden');
});
}
/**
* Checks the status of the lobby and the current round.
* @returns {Promise<void>}
* 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);
});
let chatContent = document.querySelector('#chat-content');
chatContent.scrollTop = chatContent.scrollHeight;
}
window.addEventListener("unhandledrejection", function (promiseRejectionEvent) {
@ -812,10 +761,10 @@ window.addEventListener("unhandledrejection", function (promiseRejectionEvent) {
// prevent ctrl + s
window.addEventListener("keydown", async (e) => {
if (e.which === 83 && (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)) {
e.preventDefault();
if (document.querySelector('#input-bingo-words')) {
e.preventDefault();
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 +779,5 @@ window.onload = async () => {
}
}
};
let socket = null;

@ -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

@ -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

@ -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.
@ -138,7 +139,16 @@ class BingoDataManager {
* @returns {Promise<*>}
*/
async updateLobbyExpiration(lobbyId) {
return await this._queryDatabase(this.queries.updateLobbyExpire.sql, [lobbyId]);
return await this._queryFirstResult(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);
}
/**
@ -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', await resolvePlayer(new PlayerWrapper(winnerId)));
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);
@ -907,7 +919,7 @@ class LobbyWrapper {
*/
async _loadLobbyInfo(force) {
if (!this._infoLoaded && !force) {
let row = await bdm.getLobbyInfo(this.id);
let row = await bdm.updateLobbyExpiration(this.id);
this._assignProperties(row);
}
}
@ -928,6 +940,14 @@ class LobbyWrapper {
}
}
/**
* Emits an event is a socket exists for the lobby
*/
emit() {
if (this.socket)
this.socket.emit(...arguments);
}
/**
* Returns if the lobby exists (based on one loaded attribute)
* @returns {Promise<boolean>}
@ -1061,6 +1081,7 @@ class LobbyWrapper {
await this._createRound();
await this._createGrids();
await this.setRoundStatus('ACTIVE');
this.emit('statusChange', 'ACTIVE');
}
}
@ -1115,6 +1136,7 @@ class LobbyWrapper {
await this.addWord(word);
for (let word of removedWords)
await this.removeWord(word.id);
this.emit('wordsChange');
}
}
@ -1144,6 +1166,16 @@ class LobbyWrapper {
};
}
/**
* Adds an info message and emits the message event.
* @param message {String} - the info messages content
* @returns {Promise<void>}
*/
async addInfoMessage(message) {
let result = await bdm.addInfoMessage(this.id, message);
this.emit('message', await resolveMessage(new MessageWrapper(result)));
}
/**
* Adds a player to the lobby.
* @param playerId
@ -1151,8 +1183,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.emit('playerJoin', await resolvePlayer(playerWrapper));
let username = await playerWrapper.username();
await this.addInfoMessage(`${username} joined.`);
await this._loadLobbyInfo(true);
}
@ -1164,7 +1198,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.emit('playerLeave', playerId);
await this.addInfoMessage(`${username} left.`);
await this._loadLobbyInfo(true);
}
@ -1185,7 +1220,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.emit('statusChange', status);
if (status === 'FINISHED')
await bdm.clearGrids(this.id);
@ -1395,6 +1431,53 @@ 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})
};
}
/**
* Resolves a fieldWrapper object
* @param fieldWrapper
* @returns {Promise<{submitted: (Object.submitted|*), column: *, bingo: boolean, row: (Object.grid_row|number|*)}>}
*/
async function resolveGridField(fieldWrapper) {
return {
row: fieldWrapper.row,
column: fieldWrapper.column,
submitted: fieldWrapper.submitted,
bingo: await fieldWrapper.grid.bingo()
};
}
/**
* Returns resolved message data.
* @param lobbyId
@ -1412,19 +1495,51 @@ 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);
}
});
socket.on('fieldToggle', async (context, location) => {
let {row, column} = location;
let result = await (await (new PlayerWrapper(context.playerId)).grid({lobbyId: lobbyId}))
.toggleField(row, column);
socket.emit('fieldChange', await resolveGridField(result));
});
});
}
}
let bdm = new BingoDataManager(pgPool);
router.init = async () => {
router.init = async (bingoIo, io) => {
await bdm.init();
};
router.use(async (req, res, next) => {
for (let id of await bdm.getLobbyIds())
createSocketIfNotExist(io, id);
router.use(async (req, res, next) => {
if (req.session.bingoPlayerId)
await bdm.updatePlayerExpiration(req.session.bingoPlayerId);
next();
});
});
router.get('/', async (req, res) => {
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);
@ -1432,6 +1547,7 @@ router.get('/', async (req, res) => {
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))
@ -1472,9 +1588,9 @@ router.get('/', async (req, res) => {
username: await playerWrapper.username()
});
}
});
});
router.graphqlResolver = async (req, res) => {
router.graphqlResolver = async (req, res) => {
let playerId = req.session.bingoPlayerId;
if (playerId)
await bdm.updatePlayerExpiration(playerId);
@ -1506,8 +1622,15 @@ router.graphqlResolver = async (req, res) => {
} 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}`);
if (req.query.g) {
let lobbyWrapper = new LobbyWrapper(req.query.g);
if (await lobbyWrapper.exists()) {
lobbyWrapper.emit('usernameChange',
await resolvePlayer(new PlayerWrapper(playerId), req.query.g));
await lobbyWrapper.addInfoMessage(`${oldName} changed username to ${username}`);
}
}
}
return new PlayerWrapper(playerId);
} else {
@ -1519,7 +1642,8 @@ router.graphqlResolver = async (req, res) => {
if (playerId)
if (gridSize > 0 && gridSize < 10) {
let result = await bdm.createLobby(playerId, gridSize);
return new LobbyWrapper(result.id);
createSocketIfNotExist(io, result.id);
return new LobbyWrapper(result.id, result);
} else {
res.status(413);
}
@ -1527,6 +1651,7 @@ router.graphqlResolver = async (req, res) => {
},
mutateLobby: async ({id}) => {
let lobbyId = id;
createSocketIfNotExist(io, lobbyId);
await bdm.updateLobbyExpiration(lobbyId);
let lobbyWrapper = new LobbyWrapper(lobbyId);
return {
@ -1639,6 +1764,7 @@ router.graphqlResolver = async (req, res) => {
};
}
};
};
};
module.exports = router;

@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS bingo.lobbys (
admin_id serial references bingo.players(id) ON DELETE SET NULL,
grid_size integer DEFAULT 3 NOT NULL,
current_round integer,
expire timestamp DEFAULT (NOW() + interval '1 hour' )
expire timestamp DEFAULT (NOW() + interval '4 hour' )
);
-- lobbys-players table

@ -48,7 +48,7 @@ addLobby:
# params:
# - {Number} - the id of the lobby
updateLobbyExpire:
sql: UPDATE bingo.lobbys SET expire = (NOW() + interval '1 hours') WHERE id = $1;
sql: UPDATE bingo.lobbys SET expire = (NOW() + interval '4 hours') WHERE id = $1 RETURNING *;
# inserts a player into a lobby
# params:
@ -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

@ -19,4 +19,4 @@ block content
include includes/bingo-chat
include includes/bingo-statusbar
script(type='text/javascript') refreshLobby();
script(type='text/javascript') initRefresh();

@ -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();

@ -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

@ -1,2 +1,3 @@
link(rel='stylesheet', href='/sass/style.sass')
script(type='text/javascript', src='/javascripts/common.js')
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')

Loading…
Cancel
Save