Changed to socket.io

- changed from graphql to socket.io for events and information
- removed refreshing graphql functions from frontend
pull/23/head
Trivernis 6 years ago
parent 5c7bf97995
commit 38420bd0dd

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

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

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

279
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,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="
}
}
}

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

@ -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<boolean>}
*/
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<boolean>}
*/
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<void>}
*/
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<void>}
*/
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<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.');
}
}
}
/**
* 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<boolean>}
*/
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<number>}
@ -120,25 +280,8 @@ async function joinLobby() {
* @returns {Promise<boolean>}
*/
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<void>}
*/
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<void>}
*/
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(`
<br><b>Commands: </b><br>
@ -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 <b>${uname}</b> 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<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);
}
}
@ -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<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');
@ -554,12 +580,14 @@ function addChatMessage(messageObject, player) {
msgSpan.innerHTML = `
<span class="chatUsername">${messageObject.author.username}:</span>
<span class="chatMessageContent">${messageObject.htmlContent}</span>`;
else
else
msgSpan.innerHTML = `
<span class="chatMessageContent ${messageObject.type}">${messageObject.htmlContent}</span>`;
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 = `<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!){
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<void>}
*/
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<Object>} - 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<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('disconnect', () => {
indicator.setAttribute('socket-status', 'disconnected');
showError('Disconnected from socket!');
});
if (wordContainer)
wordContainer.innerHTML = `<span class="bingoWord">
${response.data.bingo.lobby.words.map(x => x.content).join('</span><span class="bingoWord">')}</span>`;
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<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);
});
}
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;

@ -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.
@ -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<void>}
*/
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);
}
};
}
};
}
};
};
};

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