Merge pull request #7 from Trivernis/develop

Develop
pull/8/head
Trivernis 6 years ago committed by GitHub
commit 84914376d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

2
.gitignore vendored

@ -4,3 +4,5 @@ node_modules
scripts/* scripts/*
tmp tmp
config.yaml config.yaml
sessions
sessions-journal

@ -6,7 +6,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- CHANGELOG.md Changelog - CHANGELOG.md Changelog
- content to the README.md - content to the README.md
- Chat to the bingo game (renderd with markdown-it)
- Postgres session storage
- sql-file directory `sql`
## Changed
- changed export of `app.js` to the asynchronous init function that returns the app object
- `bin/www` now calls the init function of `app.js`
### Removed
- sqlite3 sesssion storage
### Fixed
- mobile layout

124
app.js

@ -5,6 +5,8 @@ const createError = require('http-errors'),
logger = require('morgan'), logger = require('morgan'),
compileSass = require('express-compile-sass'), compileSass = require('express-compile-sass'),
session = require('express-session'), session = require('express-session'),
pg = require('pg'),
pgSession = require('connect-pg-simple')(session),
fsx = require('fs-extra'), fsx = require('fs-extra'),
yaml = require('js-yaml'), yaml = require('js-yaml'),
graphqlHTTP = require('express-graphql'), graphqlHTTP = require('express-graphql'),
@ -21,68 +23,86 @@ let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml'));
if (fsx.existsSync('config.yaml')) if (fsx.existsSync('config.yaml'))
Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml')));
let graphqlResolver = (request, response) => { async function init() {
return { // grapql default resolver
let graphqlResolver = (request, response) => {
return {
time: Date.now(), time: Date.now(),
bingo: bingoRouter.graphqlResolver(request, response) bingo: bingoRouter.graphqlResolver(request, response)
} }
}; };
let app = express(); // database setup
let pgPool = new pg.Pool({
host: settings.postgres.host,
port: settings.postgres.port,
user: settings.postgres.user,
password: settings.postgres.password,
database: settings.postgres.database
});
await pgPool.query(fsx.readFileSync('./sql/createSessionTable.sql', 'utf-8'));
// view engine setup let app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.set('trust proxy', 1);
app.use(logger('dev')); // view engine setup
app.use(express.json()); app.set('views', path.join(__dirname, 'views'));
app.use(express.urlencoded({ extended: false })); app.set('view engine', 'pug');
app.use(cookieParser()); app.set('trust proxy', 1);
app.use(session({
secret: settings.sessions.secret,
resave: false,
saveUninitialized: true,
cookie: {
expires: 10000000
}
}));
app.use('/sass', compileSass({
root: './public/stylesheets/sass',
sourceMap: true,
watchFiles: true,
logToConsole: true
}));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter); app.use(logger('dev'));
app.use('/users', usersRouter); app.use(express.json());
app.use(/\/riddle(\/.*)?/, riddleRouter); app.use(express.urlencoded({ extended: false }));
app.use('/bingo', bingoRouter); app.use(cookieParser());
app.use('/graphql', graphqlHTTP((request, response) => { app.use(session({
return { store: new pgSession({
schema: buildSchema(importSchema('./graphql/schema.graphql')), pool: pgPool,
rootValue: graphqlResolver(request, response), tableName: 'user_sessions'
context: {session: request.session}, }),
graphiql: true secret: settings.sessions.secret,
}; resave: false,
})); saveUninitialized: true,
cookie: {
maxAge: 30 * 24 * 60 * 60 * 1000 // maxAge 30 days
}
}));
app.use('/sass', compileSass({
root: './public/stylesheets/sass',
sourceMap: true,
watchFiles: true,
logToConsole: true
}));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use(/\/riddle(\/.*)?/, riddleRouter);
app.use('/bingo', bingoRouter);
app.use('/graphql', graphqlHTTP((request, response) => {
return {
schema: buildSchema(importSchema('./graphql/schema.graphql')),
rootValue: graphqlResolver(request, response),
context: {session: request.session},
graphiql: true
};
}));
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use(function(req, res, next) { app.use(function(req, res, next) {
next(createError(404)); next(createError(404));
}); });
// error handler // error handler
app.use(function(err, req, res, next) { app.use(function(err, req, res, next) {
// set locals, only providing error in development // set locals, only providing error in development
res.locals.message = err.message; res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {}; res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page // render the error page
res.status(err.status || 500); res.status(err.status || 500);
res.render('error'); res.render('error');
}); });
return app;
}
module.exports = app; module.exports = init;
//app.listen(settings.port); //app.listen(settings.port);

@ -4,7 +4,7 @@
* Module dependencies. * Module dependencies.
*/ */
const app = require('../app'); const appInit = require('../app');
const debug = require('debug')('whooshy:server'); const debug = require('debug')('whooshy:server');
const http = require('http'); const http = require('http');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
@ -26,25 +26,31 @@ try {
*/ */
let port = normalizePort(process.env.PORT || settings.port || '3000'); let port = normalizePort(process.env.PORT || settings.port || '3000');
app.set('port', port);
/** appInit().then((app) => {
* Create HTTP server. app.set('port', port);
*/
let server = http.createServer(app); /**
* Create HTTP server.
*/
/** let server = http.createServer(app);
* Listen on provided port, on all network interfaces.
*/
server.listen(port); /**
server.on('error', onError); * Listen on provided port, on all network interfaces.
server.on('listening', onListening); */
/** server.listen(port);
* Normalize a port into a number, string, or false. server.on('error', (error) => onError(error, server));
*/ server.on('listening', () => onListening(server));
/**
* Normalize a port into a number, string, or false.
*/
}).catch((err) => {
console.error(err.message);
console.error(err.stack);
});
function normalizePort(val) { function normalizePort(val) {
let port = parseInt(val, 10); let port = parseInt(val, 10);
@ -66,7 +72,7 @@ function normalizePort(val) {
* Event listener for HTTP server "error" event. * Event listener for HTTP server "error" event.
*/ */
function onError(error) { function onError(error, server) {
if (error.syscall !== 'listen') { if (error.syscall !== 'listen') {
throw error; throw error;
} }
@ -94,7 +100,7 @@ function onError(error) {
* Event listener for HTTP server "listening" event. * Event listener for HTTP server "listening" event.
*/ */
function onListening() { function onListening(server) {
let addr = server.address(); let addr = server.address();
let bind = typeof addr === 'string' let bind = typeof addr === 'string'
? 'pipe ' + addr ? 'pipe ' + addr

@ -3,3 +3,10 @@ sessions:
maxAge: 1000000 maxAge: 1000000
port: 3000 port: 3000
postgres:
host: localhost
port: 5432
user: whooshy
password: whooshypassword
database: whooshy

@ -10,10 +10,13 @@ type BingoMutation {
toggleWord(input: WordInput!): BingoGrid toggleWord(input: WordInput!): BingoGrid
# set the username of the current session # set the username of the current session
setUsername(input: UsernameInput): BingoUser setUsername(input: UsernameInput!): BingoUser
# recreates the active game to a follow-up # recreates the active game to a follow-up
createFollowupGame: BingoGame createFollowupGame: BingoGame
# sends a message to the current sessions chat
sendChatMessage(input: MessageInput!): ChatMessage
} }
type BingoQuery { type BingoQuery {
@ -28,36 +31,6 @@ type BingoQuery {
activeGrid: BingoGrid activeGrid: BingoGrid
} }
input CreateGameInput {
# the words used to fill the bingo grid
words: [String!]!
# the size of the bingo grid
size: Int! = 3
}
input WordInput {
# the normal word string
word: String
# the base64-encoded word
base64Word: String
}
input UsernameInput {
# the username string
username: String!
}
input IdInput {
# the id
id: ID!
}
type BingoGame { type BingoGame {
# the id of the bingo game # the id of the bingo game
@ -80,6 +53,9 @@ type BingoGame {
# the id of the followup game if it has been created # the id of the followup game if it has been created
followup: ID followup: ID
# Returns the last n chat-messages
getMessages(input: MessageQueryInput): [ChatMessage!]
} }
type BingoUser { type BingoUser {
@ -117,3 +93,83 @@ type BingoField {
# the base64 encoded word # the base64 encoded word
base64Word: String base64Word: String
} }
type ChatMessage {
# the id of the message
id: ID!
# the content of the message
content: String!
# the content of the message rendered by markdown-it
htmlContent: String
# the type of the message
type: MessageType!
# the username of the sender
username: String
# the time the message was send (in milliseconds)
datetime: String!
}
# #
# input Types #
# #
input CreateGameInput {
# the words used to fill the bingo grid
words: [String!]!
# the size of the bingo grid
size: Int! = 3
}
input WordInput {
# the normal word string
word: String
# the base64-encoded word
base64Word: String
}
input UsernameInput {
# the username string
username: String!
}
input IdInput {
# the id
id: ID!
}
input MessageInput {
# the message
message: String!
}
input MessageQueryInput {
# search for a specific id
id: ID
# get the last n messages
last: Int = 10
}
# #
# enum Types #
# #
enum MessageType {
USER
ERROR
INFO
}

182
package-lock.json generated

@ -176,6 +176,15 @@
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
}, },
"ascii2mathml": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/ascii2mathml/-/ascii2mathml-0.6.2.tgz",
"integrity": "sha512-tkPONh2Y7ZpuGQw6AiRnExX/CSYf5C2T/rF+UMJq5n0Us7+QjL8VY5ZE16xo9wcXhdqPkl/F7lzEzU9HX0YKng==",
"optional": true,
"requires": {
"minimist": "^1.2.0"
}
},
"asn1": { "asn1": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@ -407,6 +416,11 @@
} }
} }
}, },
"buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
},
"bytes": { "bytes": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
@ -591,6 +605,14 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
}, },
"connect-pg-simple": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-5.0.0.tgz",
"integrity": "sha512-WZ7xkN+qe5bbDLgZ1L9GxnSbr155cJHmfNRzVR5hBvqio7Pg/vuH7Cf8lPUSFClQjtybYSejUqyO54sYt4cg+w==",
"requires": {
"pg": "^7.4.3"
}
},
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@ -787,6 +809,11 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
}, },
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
},
"error-ex": { "error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -2269,6 +2296,14 @@
"invert-kv": "^1.0.0" "invert-kv": "^1.0.0"
} }
}, },
"linkify-it": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz",
"integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==",
"requires": {
"uc.micro": "^1.0.1"
}
},
"load-json-file": { "load-json-file": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
@ -2327,6 +2362,51 @@
"object-visit": "^1.0.0" "object-visit": "^1.0.0"
} }
}, },
"markdown-it": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
"integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
"requires": {
"argparse": "^1.0.7",
"entities": "~1.1.1",
"linkify-it": "^2.0.0",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
}
},
"markdown-it-emoji": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz",
"integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw="
},
"markdown-it-linkify-images": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-1.1.0.tgz",
"integrity": "sha1-xVedk4bXcgxUbPWD/glq3FmAyFk="
},
"markdown-it-mark": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-2.0.0.tgz",
"integrity": "sha1-RqGqlHEFrtgYiXjgoBYXnkBPQsc="
},
"markdown-it-math": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/markdown-it-math/-/markdown-it-math-4.1.1.tgz",
"integrity": "sha512-LQ0hREgMgN4tNcy2PGyw1XypjmKJjc+ZzATMuDIVD/Bagr5SGL198uHleVdiFDrNdXpqVmL4N1KD1GYyftMakQ==",
"requires": {
"ascii2mathml": "^0.6.2"
}
},
"markdown-it-smartarrows": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/markdown-it-smartarrows/-/markdown-it-smartarrows-1.0.1.tgz",
"integrity": "sha1-tXDpwP+YEuDbas4Zr6W6ErZLuac="
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
},
"media-typer": { "media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -2715,6 +2795,11 @@
"os-tmpdir": "^1.0.0" "os-tmpdir": "^1.0.0"
} }
}, },
"packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"parse-json": { "parse-json": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
@ -2776,6 +2861,62 @@
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
}, },
"pg": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-7.11.0.tgz",
"integrity": "sha512-YO4V7vCmEMGoF390LJaFaohWNKaA2ayoQOEZmiHVcAUF+YsRThpf/TaKCgSvsSE7cDm37Q/Cy3Gz41xiX/XjTw==",
"requires": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "0.1.3",
"pg-pool": "^2.0.4",
"pg-types": "~2.0.0",
"pgpass": "1.x",
"semver": "4.3.2"
},
"dependencies": {
"semver": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz",
"integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c="
}
}
},
"pg-connection-string": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz",
"integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc="
},
"pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
},
"pg-pool": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.6.tgz",
"integrity": "sha512-hod2zYQxM8Gt482q+qONGTYcg/qVcV32VHVPtktbBJs0us3Dj7xibISw0BAAXVMCzt8A/jhfJvpZaxUlqtqs0g=="
},
"pg-types": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.0.1.tgz",
"integrity": "sha512-b7y6QM1VF5nOeX9ukMQ0h8a9z89mojrBHXfJeSug4mhL0YpxNBm83ot2TROyoAmX/ZOX3UbwVO4EbH7i1ZZNiw==",
"requires": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
}
},
"pgpass": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz",
"integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=",
"requires": {
"split": "^1.0.0"
}
},
"pify": { "pify": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
@ -2799,6 +2940,29 @@
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
}, },
"postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
},
"postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU="
},
"postgres-date": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.4.tgz",
"integrity": "sha512-bESRvKVuTrjoBluEcpv2346+6kgB7UlnqWZsnbnCccTNq/pqfj1j6oBaN5+b/NrDXepYUT/HKadqv3iS9lJuVA=="
},
"postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"requires": {
"xtend": "^4.0.0"
}
},
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
@ -3485,6 +3649,14 @@
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz",
"integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==" "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA=="
}, },
"split": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
"integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
"requires": {
"through": "2"
}
},
"split-string": { "split-string": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -3638,6 +3810,11 @@
"inherits": "2" "inherits": "2"
} }
}, },
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"to-fast-properties": { "to-fast-properties": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
@ -3742,6 +3919,11 @@
"mime-types": "~2.1.24" "mime-types": "~2.1.24"
} }
}, },
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"uglify-js": { "uglify-js": {
"version": "2.8.29", "version": "2.8.29",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",

@ -6,6 +6,7 @@
"start": "node ./bin/www" "start": "node ./bin/www"
}, },
"dependencies": { "dependencies": {
"connect-pg-simple": "^5.0.0",
"cookie-parser": "1.4.4", "cookie-parser": "1.4.4",
"debug": "4.1.1", "debug": "4.1.1",
"express": "4.16.4", "express": "4.16.4",
@ -17,8 +18,14 @@
"graphql-import": "0.7.1", "graphql-import": "0.7.1",
"http-errors": "1.7.2", "http-errors": "1.7.2",
"js-yaml": "latest", "js-yaml": "latest",
"markdown-it": "^8.4.2",
"markdown-it-emoji": "^1.4.0",
"markdown-it-mark": "^2.0.0",
"markdown-it-math": "^4.1.1",
"markdown-it-smartarrows": "^1.0.1",
"morgan": "1.9.1", "morgan": "1.9.1",
"node-sass": "4.12.0", "node-sass": "4.12.0",
"pg": "^7.11.0",
"pug": "2.0.3" "pug": "2.0.3"
} }
} }

@ -76,8 +76,9 @@ async function createFollowup() {
*/ */
async function submitUsername() { async function submitUsername() {
let unameInput = document.querySelector('#username-input'); let unameInput = document.querySelector('#username-input');
let username = unameInput.value; let username = unameInput.value.replace(/^\s+|\s+$/g, '');
let response = await postGraphqlQuery(` if (username.length > 1 && username !== 'anonymous') {
let response = await postGraphqlQuery(`
mutation($username:String!) { mutation($username:String!) {
bingo { bingo {
setUsername(input: {username: $username}) { setUsername(input: {username: $username}) {
@ -86,16 +87,19 @@ async function submitUsername() {
} }
} }
}`, { }`, {
username: username username: username
},`/graphql?game=${getGameParam()}`); },`/graphql?game=${getGameParam()}`);
if (response.status === 200) { if (response.status === 200) {
unameInput.value = ''; unameInput.value = '';
unameInput.placeholder = response.data.username; unameInput.placeholder = response.data.username;
document.querySelector('#username-form').remove(); document.querySelector('#username-form').remove();
document.querySelector('.greyover').remove(); document.querySelector('.greyover').remove();
} else {
showError(`Failed to submit username. HTTP Error: ${response.status}`);
console.error(response);
}
} else { } else {
showError(`Failed to submit username. HTTP Error: ${response.status}`); showError('You need to provide a username (minimum 2 characters)!');
console.error(response);
} }
} }
@ -187,6 +191,12 @@ async function refresh() {
username username
id id
} }
getMessages {
id
username
type
htmlContent
}
} }
} }
}`, null, `/graphql?game=${getGameParam()}`); }`, null, `/graphql?game=${getGameParam()}`);
@ -213,6 +223,10 @@ async function refresh() {
} }
} }
} }
for (let chatMessage of bingoSession.getMessages) {
if (!document.querySelector(`.chatMessage[msg-id='${chatMessage.id}'`))
addChatMessage(chatMessage);
}
} else { } else {
if (response.status === 400) if (response.status === 400)
clearInterval(refrInterval); clearInterval(refrInterval);
@ -260,6 +274,52 @@ function showError(errorMessage) {
}, 10000); }, 10000);
} }
async function sendChatMessage() {
let messageInput = document.querySelector('#chat-input');
if (messageInput.value && messageInput.value.length > 0) {
let message = messageInput.value;
let response = await postGraphqlQuery(`
mutation($message: String!) {
bingo {
sendChatMessage(input: { message: $message }) {
id
htmlContent
username
type
}
}
}`,{message: message}, `/graphql?game=${getGameParam()}`);
if (response.status === 200) {
addChatMessage(response.data.bingo.sendChatMessage);
messageInput.value = '';
} else {
console.error(response);
showError('Error when sending message.');
}
}
}
/**
* Adds a message to the chat
* @param messageObject {Object} - the message object returned by graphql
*/
function addChatMessage(messageObject) {
let msgSpan = document.createElement('span');
msgSpan.setAttribute('class', 'chatMessage');
msgSpan.setAttribute('msg-id', messageObject.id);
if (messageObject.type === "USER") {
msgSpan.innerHTML = `
<span class="chatUsername">${messageObject.username}:</span>
<span class="chatMessageContent">${messageObject.htmlContent}</span>`;
} else {
msgSpan.innerHTML = `
<span class="chatMessageContent ${messageObject.type}">${messageObject.htmlContent}</span>`;
}
let chatContent = document.querySelector('#chat-content');
chatContent.appendChild(msgSpan);
chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom
}
/** /**
* Executes the provided function if the key-event is an ENTER-key * Executes the provided function if the key-event is an ENTER-key
* @param event {Event} - the generated key event * @param event {Event} - the generated key event
@ -270,12 +330,25 @@ function submitOnEnter(event, func) {
func(); func();
} }
/**
* Toggles the displayChat class on the content container to switch between chat-view and grid view
*/
function toggleChatView() {
let contentContainer = document.querySelector('#content-container');
if (contentContainer.getAttribute('class') === 'displayChat')
contentContainer.setAttribute('class', '');
else
contentContainer.setAttribute('class', 'displayChat')
}
window.addEventListener("unhandledrejection", function(promiseRejectionEvent) { window.addEventListener("unhandledrejection", function(promiseRejectionEvent) {
promiseRejectionEvent.promise.catch(err => console.log(err)); promiseRejectionEvent.promise.catch(err => console.log(err));
showError('Connection problems... Is the server down?'); showError('Connection problems... Is the server down?');
}); });
window.onload = () => { window.onload = () => {
if (document.querySelector('#chat-container'))
refresh();
if (window && !document.querySelector('#bingoform')) { if (window && !document.querySelector('#bingoform')) {
refrInterval = setInterval(refresh, 1000); // global variable to clear refrInterval = setInterval(refresh, 1000); // global variable to clear
} }

@ -21,19 +21,54 @@ textarea
height: 80% height: 80%
#content-container #content-container
grid-template-columns: 0 100% !important grid-template-columns: 0 100% !important
grid-template-rows: 10% 80% 10% !important grid-template-rows: 10% 40% 40% 10% !important
#players-container div
display: none #players-container, #chat-container
display: none !important
padding: 0 padding: 0
.errorDiv
grid-column-start: 1 !important
grid-column-end: 4 !important
#content-container.displayChat
grid-template-columns: 100% 0 !important
grid-template-rows: 0 25% 65% 10% !important
#players-container, #chat-container
display: block !important
#words-container
display: none !important
#username-form #username-form
width: calc(100% - 2rem) !important width: calc(100% - 2rem) !important
left: 0 !important left: 0 !important
#hide-player-container-button #hide-player-container-button
display: none display: none
.popup .popup
width: calc(100% - 2rem) !important width: calc(100% - 2rem) !important
left: 0 !important left: 0 !important
#button-container
grid-column-start: 2 !important
grid-column-end: 3 !important
#chat-button-container
display: inline-block
grid-row-start: 4
grid-row-end: 4
grid-column-start: 1
grid-column-end: 4
overflow: hidden
margin: 0 0.5rem
button
width: 100%
margin: 0.5rem 0
@media(min-device-width: 641px) @media(min-device-width: 641px)
textarea textarea
height: 80% height: 80%
@ -41,6 +76,8 @@ textarea
#words-container #words-container
width: 100% width: 100%
height: 100% height: 100%
#chat-button-container
display: none
.number-input .number-input
width: 4rem width: 4rem
@ -126,11 +163,14 @@ textarea
#content-container #content-container
display: grid display: grid
grid-template-columns: 20% 80% grid-template-columns: 25% 75%
grid-template-rows: 10% 80% 10% grid-template-rows: 10% 40% 40% 10%
height: 100% height: 100%
width: 100% width: 100%
div
overflow: auto
#button-container #button-container
grid-column-start: 1 grid-column-start: 1
grid-column-end: 1 grid-column-end: 1
@ -150,28 +190,74 @@ textarea
grid-column-start: 1 grid-column-start: 1
grid-column-end: 1 grid-column-end: 1
grid-row-start: 2 grid-row-start: 2
grid-row-end: 2 grid-row-end: 3
h1 h1
margin: 0 0 1rem 0 margin: 0 0 1rem 0
#words-container #words-container
grid-column-start: 2 grid-column-start: 2
grid-column-end: 2 grid-column-end: 3
grid-row-start: 2 grid-row-start: 2
grid-row-end: 2 grid-row-end: 4
#chat-container
grid-column-start: 1
grid-column-end: 1
grid-row-start: 3
grid-row-end: 4
height: 100%
border: 1px solid $inactive
margin: 0 0.5rem
word-break: break-word
#chat-content
height: calc(100% - 2.5rem)
background-color: $primary
overflow: auto
font-size: 0.8em
.chatMessage
display: list-item
padding: 0.2rem
.chatUsername
color: $inactive
.ERROR
color: $error
.INFO
color: $inactive
font-style: italic
.chatMessageContent
img
width: 100%
height: auto
transition-duration: 0.5s
border-radius: 0.5em
img:hover
border-radius: 0
#chat-input
width: 100%
margin: 0 0 0 0
height: 2.5rem
border-radius: 0
.errorDiv .errorDiv
grid-column-start: 2 grid-column-start: 2
grid-column-end: 2 grid-column-end: 3
grid-row-start: 3 grid-row-start: 4
grid-row-end: 3 grid-row-end: 4
background-color: $error background-color: $error
text-align: center text-align: center
margin: 0.75rem 0 margin: 0.75rem 0
border-radius: 1rem border-radius: 1rem
height: calc(100% - 1.5rem) height: calc(100% - 1.5rem)
display: table display: table
span span
display: table-cell display: table-cell
font-size: 1.8rem font-size: 1.8rem
@ -210,4 +296,4 @@ textarea
z-index: 99 z-index: 99
top: 0 top: 0
left: 0 left: 0
background-color: rgba(0,0,0,0.5) background-color: rgba(0, 0, 0, 0.5)

@ -4,5 +4,4 @@
background: lighten($primary, 10%) background: lighten($primary, 10%)
color: $primarySurface color: $primarySurface
border: 2px solid $primarySurface border: 2px solid $primarySurface
border-radius: $borderRadius
transition-duration: 0.2s transition-duration: 0.2s

@ -12,11 +12,11 @@
@media (min-device-width: 641px) @media (min-device-width: 641px)
html html
font-size: 4vw font-size: 2.5vw
@media (min-device-width: 961px) @media (min-device-width: 961px)
html html
font-size: 3vw font-size: 2.2vw
@media (min-device-width: 1025px) @media (min-device-width: 1025px)
html html
@ -53,3 +53,20 @@ input
textarea textarea
background-color: lighten($primary, 15%) background-color: lighten($primary, 15%)
a
color: $secondary
::-webkit-scrollbar
width: 12px
height: 12px
::-webkit-scrollbar-thumb
background: darken($secondary, 5)
transition-duration: 0.2s
::-webkit-scrollbar-thumb:hover
background: $secondary
::-webkit-scrollbar-track
background: lighten($primary, 5)

@ -1,7 +1,16 @@
const express = require('express'), const express = require('express'),
router = express.Router(), router = express.Router(),
cproc = require('child_process'), cproc = require('child_process'),
fsx = require('fs-extra'); fsx = require('fs-extra'),
mdEmoji = require('markdown-it-emoji'),
mdMark = require('markdown-it-mark'),
mdSmartarrows = require('markdown-it-smartarrows'),
mdMath = require('markdown-it-math'),
md = require('markdown-it')()
.use(mdEmoji)
.use(mdMark)
.use(mdSmartarrows)
.use(mdMath);
const rWordOnly = /^\w+$/; const rWordOnly = /^\w+$/;
@ -21,6 +30,7 @@ class BingoSession {
this.bingos = []; // array with the users that already had bingo this.bingos = []; // array with the users that already had bingo
this.finished = false; this.finished = false;
this.followup = null; this.followup = null;
this.chatMessages = [];
} }
/** /**
@ -30,6 +40,9 @@ class BingoSession {
addUser(user) { addUser(user) {
let id = user.id; let id = user.id;
this.users[id] = user; this.users[id] = user;
if (user.username !== 'anonymous') {
this.chatMessages.push(new BingoChatMessage(`**${user.username}** joined.`, "INFO"));
}
} }
/** /**
@ -53,8 +66,54 @@ class BingoSession {
let followup = new BingoSession(this.words, this.gridSize); let followup = new BingoSession(this.words, this.gridSize);
this.followup = followup.id; this.followup = followup.id;
bingoSessions[followup.id] = followup; bingoSessions[followup.id] = followup;
followup.chatMessages = this.chatMessages;
followup.chatMessages.push(new BingoChatMessage('**Rematch**', "INFO"));
return followup; return followup;
} }
/**
* Graphql endpoint to get the last n messages or messages by id
* @param args {Object} - arguments passed by graphql
* @returns {[]}
*/
getMessages(args) {
let input = args.input || null;
if (input && input.id) {
return this.chatMessages.find(x => (x && x.id === input.id));
} else if (input && input.last) {
return this.chatMessages.slice(-input.last);
} else {
return this.chatMessages.slice(-10);
}
}
/**
* Sends the message that a user toggled a word.
* @param base64Word
* @param bingoUser
*/
sendToggleInfo(base64Word, bingoUser) {
let word = Buffer.from(base64Word, 'base64').toString();
let toggleMessage = new BingoChatMessage(`**${bingoUser.username}** toggled phrase "${word}".`, "INFO");
this.chatMessages.push(toggleMessage);
}
}
class BingoChatMessage {
/**
* Chat Message class constructor
* @param messageContent {String} - the messages contents
* @param type {String} - the type constant of the message (USER, ERROR, INFO)
* @param [username] {String} - the username of the user who send this message
*/
constructor(messageContent, type="USER", username) {
this.id = generateBingoId();
this.content = messageContent;
this.htmlContent = md.renderInline(messageContent);
this.datetime = Date.now();
this.username = username;
this.type = type;
}
} }
class BingoUser { class BingoUser {
@ -96,6 +155,15 @@ class BingoGrid {
} }
} }
/**
* Replaces tag signs with html-escaped signs.
* @param htmlString
* @returns {string}
*/
function replaceTagSigns(htmlString) {
return htmlString.replace(/</g, '&#60;').replace(/>/g, '&#62;');
}
/** /**
* Shuffles the elements in an array * Shuffles the elements in an array
* @param array {Array<*>} * @param array {Array<*>}
@ -242,7 +310,8 @@ router.get('/', (req, res) => {
if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { if (bingoSessions[gameId] && !bingoSessions[gameId].finished) {
bingoUser.game = gameId; bingoUser.game = gameId;
let bingoSession = bingoSessions[gameId]; let bingoSession = bingoSessions[gameId];
bingoSession.addUser(bingoUser); if (!bingoSession.users[bingoUser.id])
bingoSession.addUser(bingoUser);
if (!bingoUser.grids[gameId]) { if (!bingoUser.grids[gameId]) {
bingoUser.grids[gameId] = generateWordGrid([bingoSession.gridSize, bingoSession.gridSize], bingoSession.words); bingoUser.grids[gameId] = generateWordGrid([bingoSession.gridSize, bingoSession.gridSize], bingoSession.words);
@ -285,6 +354,7 @@ router.graphqlResolver = (req, res) => {
}); });
let size = input.size; let size = input.size;
if (words.length > 0 && size < 10 && size > 0) { if (words.length > 0 && size < 10 && size > 0) {
words = words.slice(0, 10000); // only allow up to 10000 words in the bingo
let game = new BingoSession(words, size); let game = new BingoSession(words, size);
bingoSessions[game.id] = game; bingoSessions[game.id] = game;
@ -316,6 +386,7 @@ router.graphqlResolver = (req, res) => {
input.base64Word = input.base64Word || Buffer.from(input.word).toString('base-64'); input.base64Word = input.base64Word || Buffer.from(input.word).toString('base-64');
if (bingoUser.grids[gameId]) { if (bingoUser.grids[gameId]) {
toggleHeared(input.base64Word, bingoUser.grids[gameId]); toggleHeared(input.base64Word, bingoUser.grids[gameId]);
bingoSession.sendToggleInfo(input.base64Word, bingoUser);
return bingoUser.grids[gameId]; return bingoUser.grids[gameId];
} else { } else {
res.status(400); res.status(400);
@ -343,6 +414,16 @@ router.graphqlResolver = (req, res) => {
} else { } else {
res.status(400); res.status(400);
} }
},
sendChatMessage: ({input}) => {
input.message = replaceTagSigns(input.message).substring(0, 250);
if (bingoSession && input.message) {
let userMessage = new BingoChatMessage(input.message, 'USER', bingoUser.username);
bingoSession.chatMessages.push(userMessage);
return userMessage;
} else {
res.status(400);
}
} }
}; };
}; };

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS "user_sessions" (
"sid" varchar NOT NULL COLLATE "default",
"sess" json NOT NULL,
"expire" timestamp(6) NOT NULL,
PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE
)
WITH (OIDS=FALSE);

@ -4,7 +4,7 @@ block content
if username === 'anonymous' if username === 'anonymous'
div(class='greyover') div(class='greyover')
div(id='username-form', onkeypress='submitOnEnter(event, submitUsername)') div(id='username-form', onkeypress='submitOnEnter(event, submitUsername)')
input(type='text', id='username-input', placeholder=username) input(type='text', id='username-input', placeholder=username, maxlength="30")
span Maximum is 30 characters. span Maximum is 30 characters.
button(onclick='submitUsername()') Set Username button(onclick='submitUsername()') Set Username
div(id='content-container') div(id='content-container')
@ -13,6 +13,9 @@ block content
each player in players each player in players
div(class='player-container', b-pid=`${player.id}`) div(class='player-container', b-pid=`${player.id}`)
span(class='player-name-span')= player.username span(class='player-name-span')= player.username
div(id='chat-container')
div(id='chat-content')
input(id='chat-input' type='text', placeholder='chat', onkeypress='submitOnEnter(event, sendChatMessage)' maxlength="250")
div(id='words-container') div(id='words-container')
each val in grid each val in grid
div(class='bingo-word-row') div(class='bingo-word-row')
@ -20,4 +23,6 @@ block content
div(class='bingo-word-panel', onclick=`submitWord('${field.base64Word}')`, b-word=field.base64Word, b-sub=`${field.submitted}`) div(class='bingo-word-panel', onclick=`submitWord('${field.base64Word}')`, b-word=field.base64Word, b-sub=`${field.submitted}`)
span= field.word span= field.word
div(id='button-container') div(id='button-container')
button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo! button(id='bingo-button', onclick='submitBingo()', class='hidden') Bingo!
div(id='chat-button-container')
button(id='chat-toggle-button', onclick='toggleChatView()') toggle Chat

@ -4,5 +4,6 @@ html
title Bingo by Trivernis title Bingo by Trivernis
script(type='text/javascript', src='/javascripts/bingo-web.js') script(type='text/javascript', src='/javascripts/bingo-web.js')
link(rel='stylesheet', href='/sass/bingo/style.sass') link(rel='stylesheet', href='/sass/bingo/style.sass')
base(target='_blank')
body body
block content block content

@ -10,4 +10,4 @@ block content
div(class='stretchDiv') div(class='stretchDiv')
button(onclick='submitBingoWords()') Submit button(onclick='submitBingoWords()') Submit
span(id='word-count') Please provide at least 9 phrases: span(id='word-count') Please provide at least 9 phrases:
textarea(id='bingo-textarea', placeholder='Bingo Words') textarea(id='bingo-textarea', placeholder='Bingo Words (max 10,000)', maxlength=1000000)

Loading…
Cancel
Save