Fixed issues

- fixed bingo button not shown on refresh
- fixed code style
- added eslint
pull/15/head
Trivernis 6 years ago
parent 2fb44df24d
commit 52bd2378fb

@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Chat to the bingo game (renderd with markdown-it)
- Postgres session storage
- sql-file directory `sql`
- LICENSE.md (GPL v3)
- eslint to dev dependencys
## Changed
@ -28,3 +29,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- mobile layout
- code style issues
- Bingo button not shown on refresh

@ -1,4 +1,4 @@
# whooshy
# whooshy [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0)
This repository is the node.js webserver running on `(beta.)trivernis.net`.

@ -29,7 +29,7 @@ async function init() {
return {
time: Date.now(),
bingo: bingoRouter.graphqlResolver(request, response)
}
};
};
// database setup
let pgPool = new pg.Pool({
@ -91,7 +91,7 @@ async function init() {
});
// error handler
app.use(function(err, req, res, next) {
app.use(function(err, req, res) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

@ -41,7 +41,7 @@ appInit().then((app) => {
*/
server.listen(port);
server.on('error', (error) => onError(error, server));
server.on('error', (error) => onError(error));
server.on('listening', () => onListening(server));
/**
@ -55,16 +55,12 @@ appInit().then((app) => {
function normalizePort(val) {
let port = parseInt(val, 10);
if (isNaN(port)) {
if (isNaN(port))
// named pipe
return val;
}
if (port >= 0) {
if (port >= 0)
// port number
return port;
}
return false;
}
@ -72,10 +68,9 @@ function normalizePort(val) {
* Event listener for HTTP server "error" event.
*/
function onError(error, server) {
if (error.syscall !== 'listen') {
function onError(error) {
if (error.syscall !== 'listen')
throw error;
}
let bind = typeof port === 'string'
? 'Pipe ' + port

724
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -26,5 +26,71 @@
"node-sass": "4.12.0",
"pg": "^7.11.0",
"pug": "2.0.3"
},
"devDependencies": {
"eslint": "^5.16.0",
"eslint-plugin-graphql": "^3.0.3",
"eslint-plugin-promise": "^4.1.1"
},
"eslintConfig": {
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"env": {
"node": true,
"browser": true,
"jquery": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:promise/recommended"
],
"rules": {
"semi": "error",
"semi-style": [
"error",
"last"
],
"no-await-in-loop": "warn",
"curly": [
"warn",
"multi",
"consistent"
],
"block-spacing": [
"warn",
"always"
],
"array-bracket-newline": [
"warn",
"consistent"
],
"camelcase": [
"error",
{
"properties": "always"
}
],
"comma-spacing": [
"error",
{
"after": true
}
],
"brace-style": [
"error",
"1tbs"
],
"no-console": "off",
"promise/no-promise-in-callback": "off",
"promise/always-return": "off",
"promise/catch-or-return": "off"
},
"plugins": [
"eslint-plugin-graphql",
"eslint-plugin-promise"
]
}
}

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars, no-undef */
/**
* Returns the value of the url-param 'game'
* @returns {string}
@ -17,7 +18,7 @@ function getGameParam() {
async function submitBingoWords() {
let textContent = document.querySelector('#bingo-textarea').value;
let words = textContent.replace(/[<>]/g, '').split('\n').filter((el) => {
return (!!el && el.length > 0) // remove empty strings and non-types from word array
return (!!el && el.length > 0); // remove empty strings and non-types from word array
});
if (words.length === 0) {
showError('You need to provide at least one word!');
@ -43,7 +44,7 @@ async function submitBingoWords() {
insertParam('game', gameid);
} else {
showError(`Failed to create game. HTTP Error: ${response.status}`);
console.error(response)
console.error(response);
}
}
}
@ -60,7 +61,7 @@ async function createFollowup() {
id
}
}
}`,null,`/graphql?game=${getGameParam()}`);
}`, null, `/graphql?game=${getGameParam()}`);
if (response.status === 200 && response.data.bingo.createFollowupGame) {
let gameid = response.data.bingo.createFollowupGame.id;
insertParam('game', gameid);
@ -88,7 +89,7 @@ async function submitUsername() {
}
}`, {
username: username
},`/graphql?game=${getGameParam()}`);
}, `/graphql?game=${getGameParam()}`);
if (response.status === 200) {
unameInput.value = '';
unameInput.placeholder = response.data.username;
@ -122,22 +123,59 @@ async function submitWord(word) {
}
}`, {
word: word
},`/graphql?game=${getGameParam()}`);
}, `/graphql?game=${getGameParam()}`);
if (response.status === 200 && response.data.bingo.toggleWord) {
let fieldGrid = response.data.bingo.toggleWord.fieldGrid;
for (let row of fieldGrid) {
for (let field of row) {
for (let row of fieldGrid)
for (let field of row)
document.querySelectorAll(`.bingo-word-panel[b-word="${field.base64Word}"]`).forEach(x => {
x.setAttribute('b-sub', field.submitted);
});
}
}
if (response.data.bingo.toggleWord.bingo) {
if (response.data.bingo.toggleWord.bingo)
document.querySelector('#bingo-button').setAttribute('class', '');
} else {
else
document.querySelector('#bingo-button').setAttribute('class', 'hidden');
} else {
showError(`Failed to submit word. HTTP Error: ${response.status}`);
console.error(response);
}
}
/**
* Refreshes the bingo grid. Shows the bingo button if a bingo is possible
* @returns {Promise<void>}
*/
async function refreshBingoGrid() {
let response = await postGraphqlQuery(`
query {
bingo {
activeGrid {
bingo
fieldGrid {
word
base64Word
submitted
}
}
}
}`, {}, `/graphql?game=${getGameParam()}`);
if (response.status === 200 && response.data.bingo.activeGrid) {
let fieldGrid = response.data.bingo.activeGrid.fieldGrid;
for (let row of fieldGrid)
for (let field of row)
document.querySelectorAll(`.bingo-word-panel[b-word="${field.base64Word}"]`).forEach(x => {
x.setAttribute('b-sub', field.submitted);
});
if (response.data.bingo.activeGrid.bingo)
document.querySelector('#bingo-button').setAttribute('class', '');
else
document.querySelector('#bingo-button').setAttribute('class', 'hidden');
} else {
showError(`Failed to submit word. HTTP Error: ${response.status}`);
console.error(response);
@ -162,12 +200,12 @@ async function submitBingo() {
}
}
}
}`,null,`/graphql?game=${getGameParam()}`);
}`, null, `/graphql?game=${getGameParam()}`);
if (response.status === 200 && response.data.bingo.submitBingo) {
let bingoSession = response.data.bingo.submitBingo;
if (bingoSession.bingos.length > 0) {
displayWinner(bingoSession.players.find(x => x.id === bingoSession.bingos[0]).username);
clearInterval(refrInterval)
clearInterval(refrInterval);
}
} else {
showError(`Failed to submit Bingo. HTTP Error: ${response.status}`);
@ -181,6 +219,7 @@ async function submitBingo() {
* @returns {Promise<void>}
*/
async function refresh() {
await refreshBingoGrid();
let response = await postGraphqlQuery(`
query {
bingo {
@ -205,7 +244,7 @@ async function refresh() {
if (bingoSession.bingos.length > 0) {
displayWinner(bingoSession.players.find(x => x.id === bingoSession.bingos[0]).username);
clearInterval(refrInterval)
clearInterval(refrInterval);
} else {
for (let player of bingoSession.players) {
let foundPlayerDiv = document.querySelector(`.player-container[b-pid='${player.id}'`);
@ -217,16 +256,16 @@ async function refresh() {
document.querySelector('#players-container').appendChild(playerDiv);
} else {
let playerNameSpan = foundPlayerDiv.querySelector('.player-name-span');
if (playerNameSpan.innerText !== player.username) {
if (playerNameSpan.innerText !== player.username)
playerNameSpan.innerText = player.username;
}
}
}
}
for (let chatMessage of bingoSession.getMessages) {
for (let chatMessage of bingoSession.getMessages)
if (!document.querySelector(`.chatMessage[msg-id='${chatMessage.id}'`))
addChatMessage(chatMessage);
}
} else {
if (response.status === 400)
clearInterval(refrInterval);
@ -288,7 +327,7 @@ async function sendChatMessage() {
type
}
}
}`,{message: message}, `/graphql?game=${getGameParam()}`);
}`, {message: message}, `/graphql?game=${getGameParam()}`);
if (response.status === 200) {
addChatMessage(response.data.bingo.sendChatMessage);
messageInput.value = '';
@ -307,14 +346,14 @@ function addChatMessage(messageObject) {
let msgSpan = document.createElement('span');
msgSpan.setAttribute('class', 'chatMessage');
msgSpan.setAttribute('msg-id', messageObject.id);
if (messageObject.type === "USER") {
if (messageObject.type === "USER")
msgSpan.innerHTML = `
<span class="chatUsername">${messageObject.username}:</span>
<span class="chatMessageContent">${messageObject.htmlContent}</span>`;
} else {
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
@ -338,7 +377,7 @@ function toggleChatView() {
if (contentContainer.getAttribute('class') === 'displayChat')
contentContainer.setAttribute('class', '');
else
contentContainer.setAttribute('class', 'displayChat')
contentContainer.setAttribute('class', 'displayChat');
}
window.addEventListener("unhandledrejection", function(promiseRejectionEvent) {
@ -349,9 +388,9 @@ window.addEventListener("unhandledrejection", function(promiseRejectionEvent) {
window.onload = () => {
if (document.querySelector('#chat-container'))
refresh();
if (window && !document.querySelector('#bingoform')) {
refrInterval = setInterval(refresh, 1000); // global variable to clear
}
if (window && !document.querySelector('#bingoform'))
refrInterval = setInterval(refresh, 1000); // eslint-disable-line no-undef
let gridSizeElem = document.querySelector('#bingo-grid-size');
document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value;
gridSizeElem.oninput = () => {

@ -1,16 +1,24 @@
/* eslint-disable no-unused-vars, no-undef */
/**
* HTTP POST to an url with a post body
* @param url {String} - the url to post to
* @param postBody {JSON|Object} - the json-object to post
* @returns {Promise<any>}
*/
function postData(url, postBody) {
let request = new XMLHttpRequest();
return new Promise((res, rej) => {
return new Promise((resolve, reject) => {
request.onload = () => {
res({
resolve({
status: request.status,
data: request.responseText
});
};
request.onerror = () => {
rej(request.error);
reject(request.error);
};
request.open('POST', url, true);
@ -19,10 +27,22 @@ function postData(url, postBody) {
});
}
/**
* HTTP POST to the current url
* @param postBody {JSON|Object} - the json-object to post
* @returns {Promise<any>}
*/
async function postLocData(postBody) {
return await postData('#', postBody);
}
/**
* HTTP POST to a graphql url endpoint (default '/graphql')
* @param query {String} - the graphql query to post
* @param [variables] {JSON} - optional variables used in the graphql query
* @param [url] {String} - optional alternative graphql endpoint
* @returns {Promise<{data: *, status: *}|{data: *, requestBody: {variables: *, query: *}, errors: *, status: *}>}
*/
async function postGraphqlQuery(query, variables, url) {
let body = {
query: query,
@ -31,29 +51,33 @@ async function postGraphqlQuery(query, variables, url) {
let response = await postData(url || '/graphql', body);
let resData = JSON.parse(response.data);
if (response.status === 200) {
if (response.status === 200)
return {
status: response.status,
data: resData.data,
};
} else {
else
return {
status: response.status,
data: resData.data,
errors: resData.errors,
requestBody: body
};
}
}
/**
* Inserts an url parameter
* @param key {String} - the key of the url parameter
* @param value {String} - the value of the url parameter
*/
function insertParam(key, value) {
key = encodeURI(key);
value = encodeURI(value);
let kvp = document.location.search.substr(1).split('&');
let i = kvp.length;
let x;
while (i--) {
x = kvp[i].split('=');
@ -63,10 +87,7 @@ function insertParam(key, value) {
break;
}
}
if (i < 0) {
if (i < 0)
kvp[kvp.length] = [key, value].join('=');
}
document.location.search = kvp.join('&');
}

@ -1,3 +1,10 @@
/* eslint-disable no-unused-vars, no-undef */
/**
* start the download of a subreddit
* @param subredditName {String} - the name of the subreddit to download
* @returns {Promise<any>}
*/
async function startSubredditDownload(subredditName) {
let data = await postLocData({
subreddit: subredditName
@ -5,6 +12,11 @@ async function startSubredditDownload(subredditName) {
return JSON.parse(data.data);
}
/**
* Get the status of a download
* @param downloadId {String} - the id of the download to get the status for
* @returns {Promise<any>}
*/
async function getDownloadStatus(downloadId) {
let data = await postLocData({
id: downloadId
@ -12,6 +24,11 @@ async function getDownloadStatus(downloadId) {
return JSON.parse(data.data);
}
/**
* refreshes information about a specific download
* @param downloadId {String} - the id of the download
* @returns {Promise<void>}
*/
async function refreshDownloadInfo(downloadId) {
let response = await getDownloadStatus(downloadId);
@ -20,7 +37,7 @@ async function refreshDownloadInfo(downloadId) {
let subredditName = dlDiv.getAttribute('subreddit-name');
if (response.status === 'pending') {
setTimeout(() => refreshDownloadInfo(downloadId), 1000)
setTimeout(() => refreshDownloadInfo(downloadId), 1000);
} else {
let dlLink = document.createElement('a');
dlLink.setAttribute('href', response.file);
@ -34,6 +51,10 @@ async function refreshDownloadInfo(downloadId) {
}
}
/**
* Submit a subreddit to download (called by button)
* @returns {Promise<void>}
*/
async function submitDownload() {
let subredditName = document.querySelector('#subreddit-input').value;
let response = await startSubredditDownload(subredditName);

@ -69,6 +69,9 @@ textarea
width: 100%
margin: 0.5rem 0
#chat-container .chatMessage
font-size: 1.2em
@media(min-device-width: 641px)
textarea
height: 80%
@ -177,7 +180,6 @@ textarea
grid-row-start: 1
grid-row-end: 1
display: grid
margin: 1rem
font-size: inherit
button
@ -206,7 +208,7 @@ textarea
grid-column-end: 1
grid-row-start: 3
grid-row-end: 4
height: 100%
height: calc(100% - 3px)
border: 1px solid $inactive
margin: 0 0.5rem
word-break: break-word

@ -57,6 +57,10 @@ textarea
a
color: $secondary
mark
background-color: $secondary
color: $primarySurface
::-webkit-scrollbar
width: 12px
height: 12px

@ -1,7 +1,5 @@
const express = require('express'),
router = express.Router(),
cproc = require('child_process'),
fsx = require('fs-extra'),
mdEmoji = require('markdown-it-emoji'),
mdMark = require('markdown-it-mark'),
mdSmartarrows = require('markdown-it-smartarrows'),
@ -10,8 +8,6 @@ const express = require('express'),
.use(mdMark)
.use(mdSmartarrows);
const rWordOnly = /^\w+$/;
let bingoSessions = {};
class BingoSession {
@ -38,9 +34,9 @@ class BingoSession {
addUser(user) {
let id = user.id;
this.users[id] = user;
if (user.username !== 'anonymous') {
if (user.username !== 'anonymous')
this.chatMessages.push(new BingoChatMessage(`**${user.username}** joined.`, "INFO"));
}
}
/**
@ -76,13 +72,13 @@ class BingoSession {
*/
getMessages(args) {
let input = args.input || null;
if (input && input.id) {
if (input && input.id)
return this.chatMessages.find(x => (x && x.id === input.id));
} else if (input && input.last) {
else if (input && input.last)
return this.chatMessages.slice(-input.last);
} else {
else
return this.chatMessages.slice(-10);
}
}
/**
@ -190,7 +186,7 @@ function inflateArray(array, minSize) {
let iterations = Math.ceil(minSize/array.length)-1;
for (let i = 0; i < iterations; i++)
resultArray = [...resultArray, ...resultArray];
return resultArray
return resultArray;
}
/**
@ -212,9 +208,9 @@ function generateWordGrid(dimensions, words) {
let grid = [];
for (let x = 0; x < dimensions[1]; x++) {
grid[x] = [];
for (let y = 0; y < dimensions[0]; y++) {
for (let y = 0; y < dimensions[0]; y++)
grid[x][y] = shuffledWords[(x * dimensions[0]) + y];
}
}
return (new BingoGrid(grid));
}
@ -235,55 +231,79 @@ function toggleHeared(base64Word, bingoGrid) {
}
/**
* Checks if a bingo exists in the bingo grid.
* @param bingoGrid {BingoGrid}
* @returns {boolean}
* Checks if a diagonal bingo is possible
* @param fg {Array<Array<boolean>>} - the grid with the checked (submitted) values
* @returns {boolean|boolean|*}
*/
function checkBingo(bingoGrid) {
let fg = bingoGrid.fieldGrid.map(x => x.map(y => y.submitted));
let diagonalBingo = true;
function checkBingoDiagnoal(fg) {
let bingoCheck = true;
// diagonal check
for (let i = 0; i < fg.length; i++)
diagonalBingo = fg[i][i] && diagonalBingo;
if (diagonalBingo) {
bingoGrid.bingo = true;
bingoCheck = fg[i][i] && bingoCheck;
if (bingoCheck)
return true;
}
diagonalBingo = true;
bingoCheck = true;
for (let i = 0; i < fg.length; i++)
diagonalBingo = fg[i][fg.length - i - 1] && diagonalBingo;
if (diagonalBingo) {
bingoGrid.bingo = true;
return true;
}
bingoCheck = fg[i][fg.length - i - 1] && bingoCheck;
return bingoCheck;
}
/**
* Checks if a vertical bingo is possible
* @param fg {Array<Array<boolean>>} - the grid with the checked (submitted) values
* @returns {boolean|boolean|*}
*/
function checkBingoVertical(fg) {
let bingoCheck = true;
// horizontal check
for (let row of fg) {
bingoCheck = true;
for (let field of row)
bingoCheck = field && bingoCheck;
if (bingoCheck) {
bingoGrid.bingo = true;
if (bingoCheck)
return true;
}
}
if (bingoCheck) {
bingoGrid.bingo = true;
return true;
}
bingoCheck = true;
return bingoCheck;
}
/**
* Checks if a horizontal bingo is possible
* @param fg {Array<Array<boolean>>} - the grid with the checked (submitted) values
* @returns {boolean|boolean|*}
*/
function checkBingoHorizontal(fg) {
let bingoCheck = true;
// vertical check
for (let i = 0; i < fg.length; i++) {
bingoCheck = true;
for (let j = 0; j < fg.length; j++)
bingoCheck = fg[j][i] && bingoCheck;
if (bingoCheck) {
bingoGrid.bingo = true;
if (bingoCheck)
return true;
}
}
if (bingoCheck) {
return bingoCheck;
}
/**
* Checks if a bingo exists in the bingo grid.
* @param bingoGrid {BingoGrid}
* @returns {boolean}
*/
function checkBingo(bingoGrid) {
let fg = bingoGrid.fieldGrid.map(x => x.map(y => y.submitted));
let diagonalBingo = checkBingoDiagnoal(fg);
if (diagonalBingo) {
bingoGrid.bingo = true;
return true;
}
let verticalCheck = checkBingoVertical(fg);
if (verticalCheck) {
bingoGrid.bingo = true;
return true;
}
let horizontalCheck = checkBingoHorizontal(fg);
if (horizontalCheck) {
bingoGrid.bingo = true;
return true;
}
@ -294,9 +314,9 @@ function checkBingo(bingoGrid) {
// -- Router stuff
router.use((req, res, next) => {
if (!req.session.bingoUser) {
if (!req.session.bingoUser)
req.session.bingoUser = new BingoUser();
}
next();
});
@ -311,9 +331,9 @@ router.get('/', (req, res) => {
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);
}
res.render('bingo/bingo-game', {
grid: bingoUser.grids[gameId].fieldGrid,
username: bingoUser.username,
@ -340,7 +360,7 @@ router.graphqlResolver = (req, res) => {
return bingoSession;
},
checkBingo: () => {
return checkBingo(bingoUser.grids[gameId])
return checkBingo(bingoUser.grids[gameId]);
},
activeGrid: () => {
return bingoUser.grids[gameId];
@ -348,7 +368,7 @@ router.graphqlResolver = (req, res) => {
// mutation
createGame: ({input}) => {
let words = input.words.filter((el) => { // remove empty strings and non-types from word array
return (!!el && el.length > 0)
return (!!el && el.length > 0);
});
let size = input.size;
if (words.length > 0 && size < 10 && size > 0) {
@ -404,14 +424,14 @@ router.graphqlResolver = (req, res) => {
}
},
createFollowupGame: () => {
if (bingoSession) {
if (bingoSession)
if (!bingoSession.followup)
return bingoSession.createFollowup();
else
return bingoSessions[bingoSession.followup];
} else {
else
res.status(400);
}
},
sendChatMessage: ({input}) => {
input.message = replaceTagSigns(input.message).substring(0, 250);

@ -2,7 +2,7 @@ const express = require('express');
const router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
router.get('/', function(req, res) {
res.render('index', { title: 'Trivernis.net' });
});

@ -18,10 +18,9 @@ class RedditDownload {
/**
* Generates an id for a subreddit download.
* @param subreddit
* @returns {string}
*/
function generateDownloadId(subreddit) {
function generateDownloadId() {
return Date.now().toString(16);
}
@ -32,7 +31,7 @@ function generateDownloadId(subreddit) {
*/
function startDownload(subreddit) {
if (rWordOnly.test(subreddit)) {
let downloadId = generateDownloadId(subreddit);
let downloadId = generateDownloadId();
let dlFilePath = `./public/static/${downloadId}.zip`;
let dlWebPath = `/static/${downloadId}.zip`;
let dl = new RedditDownload(dlWebPath);
@ -40,13 +39,11 @@ function startDownload(subreddit) {
dl.process = cproc.exec(`python3 -u riddle.py -o ../../public/static/${downloadId} -z --lzma ${subreddit}`,
{cwd: './scripts/reddit-riddle', env: {PYTHONIOENCODING: 'utf-8', PYTHONUNBUFFERED: true}},
(err, stdout) => {
if (err) {
if (err)
console.error(err);
} else {
else
console.log(`riddle.py: ${stdout}`);
}
});
dl.process.on('exit', (code) => {
if (code === 0)
dl.status = 'finished';
@ -57,20 +54,17 @@ function startDownload(subreddit) {
delete downloads[downloadId];
}, 300000); // delete the file after 5 minutes
});
dl.process.on('message', (msg) => {
console.log(msg)
console.log(msg);
});
downloads[downloadId] = dl;
return downloadId;
}
}
router.use('/files', express.static('./tmp'));
router.get('/', (req, res, next) => {
router.get('/', (req, res) => {
res.render('riddle');
});
@ -84,15 +78,14 @@ router.post('/', (req, res) => {
let id = req.body.id;
let download = downloads[id];
if (download) {
if (download)
res.send({
id: id,
status: download.status,
file: download.file
});
} else {
else
res.send({error: 'Unknown download ID', id: id});
}
}
});

@ -2,8 +2,8 @@ const express = require('express');
const router = express.Router();
/* GET users listing. */
router.get('/', function(req, res, next) {
res.send('respond with a resource');
router.get('/', function(req, res) {
res.send('There are no users :(');
});
module.exports = router;

Loading…
Cancel
Save