mirror of https://github.com/Trivernis/whooshy.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1855 lines
58 KiB
JavaScript
1855 lines
58 KiB
JavaScript
const express = require('express'),
|
|
router = express.Router(),
|
|
{ GraphQLError } = require('graphql'),
|
|
mdEmoji = require('markdown-it-emoji'),
|
|
mdMark = require('markdown-it-mark'),
|
|
mdSmartarrows = require('markdown-it-smartarrows'),
|
|
md = require('markdown-it')({
|
|
linkify: true,
|
|
typographer: true
|
|
})
|
|
.use(mdEmoji)
|
|
.use(mdMark)
|
|
.use(mdSmartarrows),
|
|
utils = require('../lib/utils'),
|
|
globals = require('../lib/globals');
|
|
|
|
let pgPool = globals.pgPool;
|
|
let sockets = {};
|
|
|
|
/**
|
|
* Class to manage the bingo data in the database.
|
|
*/
|
|
class BingoDataManager {
|
|
/**
|
|
* constructor functino
|
|
* @param postgresPool {pg.Pool} - the postgres pool
|
|
*/
|
|
constructor(postgresPool) {
|
|
this.pgPool = postgresPool;
|
|
this.queries = utils.parseSqlYaml('./sql/bingo');
|
|
}
|
|
|
|
async init() {
|
|
await this.pgPool.query(this.queries.createTables.sql);
|
|
setInterval(async () => await this._databaseCleanup(), 5*60*1000); // database cleanup every 5 minutes
|
|
}
|
|
|
|
/**
|
|
* Try-catch wrapper around the pgPool.query.
|
|
* @param query {String} - the sql query
|
|
* @param [values] {Array} - an array of values
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async _queryDatabase(query, values) {
|
|
try {
|
|
return await this.pgPool.query(query, values);
|
|
} catch (err) {
|
|
console.error(`Error on query "${query}" with values ${JSON.stringify(values)}.`);
|
|
console.error(err);
|
|
console.error(err.stack);
|
|
return {
|
|
rows: null
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queries the database and returns all resulting rows
|
|
* @param query {String} - the sql query
|
|
* @param values {Array} - an array of parameters needed in the query
|
|
* @returns {Promise<*>}
|
|
* @private
|
|
*/
|
|
async _queryAllResults(query, values) {
|
|
let result = await this._queryDatabase(query, values);
|
|
return result.rows || [];
|
|
}
|
|
|
|
/**
|
|
* Query the database and return the first result or null
|
|
* @param query {String} - the sql query
|
|
* @param values {Array} - an array of parameters needed in the query
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async _queryFirstResult(query, values) {
|
|
let result = await this._queryDatabase(query, values);
|
|
if (result.rows && result.rows.length > 0)
|
|
return result.rows[0];
|
|
}
|
|
|
|
/**
|
|
* Clears expired values from the database.
|
|
*/
|
|
async _databaseCleanup() {
|
|
await this._queryDatabase(this.queries.cleanup.sql);
|
|
}
|
|
|
|
/**
|
|
* Add a player to the players table
|
|
* @param username {String} - the username of the player
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async addPlayer(username) {
|
|
let result = await this._queryFirstResult(this.queries.addPlayer.sql, [username]);
|
|
if (result)
|
|
return result;
|
|
else
|
|
return {}; // makes things easier
|
|
}
|
|
|
|
/**
|
|
* Updates the username of a player
|
|
* @param playerId {Number} - the id of the player
|
|
* @param username {String} - the new username
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async updatePlayerUsername(playerId, username) {
|
|
return await this._queryFirstResult(this.queries.updatePlayerUsername.sql, [username, playerId]);
|
|
}
|
|
|
|
/**
|
|
* Returns the username for a player-id
|
|
* @param playerId {Number} - the id of the player
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getPlayerInfo(playerId) {
|
|
return await this._queryFirstResult(this.queries.getPlayerInfo.sql, [playerId]);
|
|
}
|
|
|
|
/**
|
|
* Updates the expiration date of a player
|
|
* @param playerId {Request} - thie id of the player
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async updatePlayerExpiration(playerId) {
|
|
await this._queryDatabase(this.queries.updatePlayerExpire.sql, [playerId]);
|
|
}
|
|
|
|
/**
|
|
* Creates a bingo lobby.
|
|
* @param playerId
|
|
* @param gridSize
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async createLobby(playerId, gridSize) {
|
|
return await this._queryFirstResult(this.queries.addLobby.sql, [playerId, gridSize]);
|
|
}
|
|
|
|
/**
|
|
* Updates the expiration date of a lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async updateLobbyExpiration(lobbyId) {
|
|
return await this._queryFirstResult(this.queries.updateLobbyExpire.sql, [lobbyId]);
|
|
}
|
|
|
|
/**
|
|
* Returns all lobby ids
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getLobbyIds() {
|
|
let results = await this._queryAllResults(this.queries.getLobbyIds.sql, []);
|
|
return results.map(x => x.id);
|
|
}
|
|
|
|
/**
|
|
* Returns the row of the lobby.
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getLobbyInfo(lobbyId) {
|
|
return await this._queryFirstResult(this.queries.getLobbyInfo.sql, [lobbyId]);
|
|
}
|
|
|
|
/**
|
|
* Checks if a player is in a lobby.
|
|
* @param playerId {Number} - the id of the player
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getPlayerInLobby(playerId, lobbyId) {
|
|
return (await this._queryFirstResult(this.queries.getPlayerInLobby.sql, [playerId, lobbyId]));
|
|
}
|
|
|
|
/**
|
|
* Returns all players in a lobby.
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getLobbyPlayers(lobbyId) {
|
|
return await this._queryAllResults(this.queries.getLobbyPlayers.sql, [lobbyId]);
|
|
}
|
|
|
|
/**
|
|
* Returns the last messages in a lobby with a limit
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @param [limit] {Number} - the maximum of messages to fetch
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getLobbyMessages(lobbyId, limit=20) {
|
|
return await this._queryAllResults(this.queries.getLobbyMessages.sql, [lobbyId, limit]);
|
|
}
|
|
|
|
/**
|
|
* Adds a player to a lobby.
|
|
* @param playerId {Number} - the id of the player
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async addPlayerToLobby(playerId, lobbyId) {
|
|
let entry = await this.getPlayerInLobby(playerId, lobbyId);
|
|
if (entry) {
|
|
entry.lobby_id = entry.id;
|
|
return entry;
|
|
} else {
|
|
return await this._queryFirstResult(this.queries.addPlayerToLobby.sql, [playerId, lobbyId]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the current round property of the lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @param roundId {Number} - the id of the round
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async setLobbyCurrentRound(lobbyId, roundId) {
|
|
return await this._queryFirstResult(this.queries.setLobbyCurrentRound.sql, [lobbyId, roundId]);
|
|
}
|
|
|
|
/**
|
|
* Updates the grid size of a lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @param gridSize {Number} - the new grid size
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async setLobbyGridSize(lobbyId, gridSize) {
|
|
return await this._queryFirstResult(this.queries.setLobbyGridSize.sql, [lobbyId, gridSize]);
|
|
}
|
|
|
|
/**
|
|
* Removes a player from a lobbby
|
|
* @param playerId {Number} - the id of the player
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async removePlayerFromLobby(playerId, lobbyId) {
|
|
return await this._queryFirstResult(this.queries.removePlayerFromLobby.sql, [playerId, lobbyId]);
|
|
}
|
|
|
|
/**
|
|
* Adds a word to a lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @param word {Number} - the id of the word
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async addWordToLobby(lobbyId, word) {
|
|
return await this._queryFirstResult(this.queries.addWord.sql, [lobbyId, word]);
|
|
}
|
|
|
|
/**
|
|
* Removes a word from the lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @param wordId {Number} - the id of the word
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async removeWordFromLobby(lobbyId, wordId) {
|
|
return await this._queryFirstResult(this.queries.removeLobbyWord.sql, [lobbyId, wordId]);
|
|
}
|
|
|
|
/**
|
|
* Returns all words used in a lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async getWordsForLobby(lobbyId) {
|
|
return await this._queryAllResults(this.queries.getWordsForLobby.sql, [lobbyId]);
|
|
}
|
|
|
|
/**
|
|
* Returns information about a word
|
|
* @param wordId {Number} - the id of the word
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getWordInfo(wordId) {
|
|
return await this._queryFirstResult(this.queries.getWordInfo.sql, [wordId]);
|
|
}
|
|
|
|
/**
|
|
* Returns all rounds of a lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getLobbyRounds(lobbyId) {
|
|
return await this._queryAllResults(this.queries.getLobbyRounds.sql, [lobbyId]);
|
|
}
|
|
|
|
/**
|
|
* Returns the round row of a round
|
|
* @param roundId {Number} - the id of the round
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getRoundInfo(roundId) {
|
|
return await this._queryFirstResult(this.queries.getRoundInfo.sql, [roundId]);
|
|
}
|
|
|
|
/**
|
|
* Updates the status of a round
|
|
* @param roundId {Number} - the id of the round
|
|
* @param status {String<8>} - the new status
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async updateRoundStatus(roundId, status) {
|
|
return await this._queryFirstResult(this.queries.updateRoundStatus.sql, [roundId, status]);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of wins the player had in a lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @param playerId {Number} - the id of the player
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getLobbyPlayerWins(lobbyId, playerId) {
|
|
return await this._queryFirstResult(this.queries.getLobbyPlayerWins.sql, [lobbyId, playerId]);
|
|
}
|
|
|
|
/**
|
|
* Adds a grid for a user to a lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @param playerId {Number} - the id of the user
|
|
* @param roundId {Number} - the id of the round
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async addGrid(lobbyId, playerId, roundId) {
|
|
return await this._queryFirstResult(this.queries.addGrid.sql, [playerId, lobbyId, roundId]);
|
|
}
|
|
|
|
/**
|
|
* Clears all grids for a lobby.
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async clearGrids(lobbyId) {
|
|
return await this._queryFirstResult(this.queries.clearLobbyGrids.sql, [lobbyId]);
|
|
}
|
|
|
|
/**
|
|
* Adds a word to a grid with specific location
|
|
* @param gridId {Number} - the id of the gird
|
|
* @param wordId {Number} - the id of the word
|
|
* @param row {Number} - the number of the row
|
|
* @param column {Number} - the number of the column
|
|
*/
|
|
async addWordToGrid(gridId, wordId, row, column) {
|
|
return await this._queryFirstResult(this.queries.addWordToGrid.sql, [gridId, wordId, row, column]);
|
|
}
|
|
|
|
/**
|
|
* Adds words to the grid
|
|
* @param gridId {Number} - the id of the grid
|
|
* @param words {Array<{wordId: Number, row: Number, column:Number}>}
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async addWordsToGrid(gridId, words) {
|
|
let valueSql = buildSqlParameters(4, words.length, 0);
|
|
let values = [];
|
|
for (let word of words) {
|
|
values.push(gridId);
|
|
values.push(word.wordId);
|
|
values.push(word.row);
|
|
values.push(word.column);
|
|
}
|
|
return await this._queryFirstResult(
|
|
this.queries.addWordToGridStrip.sql + valueSql + ' RETURNING *', values);
|
|
}
|
|
|
|
/**
|
|
* Returns all words in the grid with location
|
|
* @param gridId {Number} - the id of the grid
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getWordsInGrid(gridId) {
|
|
return await this._queryAllResults(this.queries.getWordsForGridId.sql, [gridId]);
|
|
}
|
|
|
|
/**
|
|
* Returns the grid row for a player and lobby id
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @param playerId {Number} - the id of the player
|
|
* @param roundId {Number} - the id of the round
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getGridForPlayerLobbyRound(lobbyId, playerId, roundId) {
|
|
return await this._queryFirstResult(this.queries.getGridByPlayerLobbyRound.sql, [playerId, lobbyId, roundId]);
|
|
}
|
|
|
|
/**
|
|
* Returns the grid row for a grid id
|
|
* @param gridId {Number} - the id of the grid
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getGridInfo(gridId) {
|
|
return await this._queryFirstResult(this.queries.getGridInfo.sql, [gridId]);
|
|
}
|
|
|
|
/**
|
|
* Sets a field of the grid to submitted/unsubmitted depending on the previous value
|
|
* @param gridId {Number} - the id of the grid
|
|
* @param fieldRow {Number} - the row of the field
|
|
* @param fieldColumn {Number} - the column of the field
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async toggleGridFieldSubmitted(gridId, fieldRow, fieldColumn) {
|
|
return await this._queryFirstResult(this.queries.toggleGridFieldSubmitted.sql, [gridId, fieldRow, fieldColumn]);
|
|
}
|
|
|
|
/**
|
|
* Adds a round for a lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async addRound(lobbyId) {
|
|
return await this._queryFirstResult(this.queries.addRound.sql, [lobbyId]);
|
|
}
|
|
|
|
/**
|
|
* Updates a round to the "FINISHED" status and sets the finish time
|
|
* @param roundId {Number}
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async setRoundFinished(roundId) {
|
|
return await this._queryFirstResult(this.queries.setRoundFinished.sql, [roundId]);
|
|
}
|
|
|
|
/**
|
|
* Updates the rounds winner
|
|
* @param roundId {Number} - the id of the round
|
|
* @param winnerId {Number} - the id of the winner
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async setRoundWinner(roundId, winnerId) {
|
|
return await this._queryFirstResult(this.queries.setRoundWinner.sql, [roundId, winnerId]);
|
|
}
|
|
|
|
/**
|
|
* Inserts a message of type "USER" into the database
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @param playerId {Number} - the id of the author (player)
|
|
* @param messageContent {String} - the content of the message
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async addUserMessage(lobbyId, playerId, messageContent) {
|
|
return await this._queryFirstResult(this.queries.addUserMessage.sql, [playerId, lobbyId, messageContent]);
|
|
}
|
|
|
|
/**
|
|
* Edits a message
|
|
* @param messageId {Number} - the id of the message
|
|
* @param messageContent {String} - the new content of the message
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async editMessage(messageId, messageContent) {
|
|
return await this._queryFirstResult(this.queries.editMessage.sql, [messageId, messageContent]);
|
|
}
|
|
|
|
/**
|
|
* Deletes a message
|
|
* @param messageId {Number} - the id of the message
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async deleteMessage(messageId) {
|
|
return await this._queryFirstResult(this.queries.deleteMessage.sql, [messageId]);
|
|
}
|
|
|
|
/**
|
|
* Returns the data of a message
|
|
* @param messageId {Number} - the id of the message
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getMessageData(messageId) {
|
|
return await this._queryFirstResult(this.queries.getMessageData.sql, [messageId]);
|
|
}
|
|
|
|
/**
|
|
* Adds a message of type "INFO" to the lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @param messageContent {String} - the content of the info message
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async addInfoMessage(lobbyId, messageContent) {
|
|
return await this._queryFirstResult(this.queries.addInfoMessage.sql, [lobbyId, messageContent]);
|
|
}
|
|
|
|
/**
|
|
* Removes all words of a lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async clearLobbyWords(lobbyId) {
|
|
return await this._queryFirstResult(this.queries.clearLobbyWords.sql, [lobbyId]);
|
|
}
|
|
|
|
/**
|
|
* Returns a single entry from grid_words
|
|
* @param gridId {Number} - the id of the grid
|
|
* @param row {Number} - the row of the field
|
|
* @param column {Number} - the column of the row
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async getGridField(gridId, row, column) {
|
|
return await this._queryFirstResult(this.queries.getGriedField.sql, [gridId, row, column]);
|
|
}
|
|
}
|
|
|
|
class WordWrapper {
|
|
/**
|
|
* constructor
|
|
* @param id {Number} - the id of the word
|
|
* @param [row] {Object} - the database row of the word
|
|
*/
|
|
constructor(id, row) {
|
|
this.id = id;
|
|
this._infoLoaded = false;
|
|
if (row)
|
|
this._assignProperties(row);
|
|
}
|
|
|
|
/**
|
|
* Loads the word information from the database
|
|
* @returns {Promise<void>}
|
|
* @private
|
|
*/
|
|
async _loadWordInfo() {
|
|
if (!this._infoLoaded) {
|
|
let row = await bdm.getWordInfo(this.id);
|
|
if (row)
|
|
this._assignProperties(row);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assign the row as properties
|
|
* @param row {Object} - the word row
|
|
* @private
|
|
*/
|
|
_assignProperties(row) {
|
|
this._content = row.content;
|
|
this._heared = row.heared;
|
|
this._lobbyId = row.lobbyId;
|
|
this._infoLoaded = this._content && this._heared && this._lobbyId;
|
|
}
|
|
|
|
/**
|
|
* Returns a new lobby wrapper for the words lobby
|
|
* @returns {Promise<LobbyWrapper>}
|
|
*/
|
|
async lobby() {
|
|
await this._loadWordInfo();
|
|
return new LobbyWrapper(this._lobbyId);
|
|
}
|
|
|
|
/**
|
|
* Returns the word iteself
|
|
* @returns {Promise<Object.content|*>}
|
|
*/
|
|
async content() {
|
|
await this._loadWordInfo();
|
|
return this._content;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of people that heared the word
|
|
* @returns {Promise<Object.heared|(function())>}
|
|
*/
|
|
async heared() {
|
|
await this._loadWordInfo();
|
|
return this._heared;
|
|
}
|
|
|
|
/**
|
|
* Returns if the word was confirmed heared
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async confirmed() {
|
|
await this._loadWordInfo();
|
|
return true; // TODO confirmation logic
|
|
}
|
|
}
|
|
|
|
class GridFieldWrapper {
|
|
/**
|
|
* constructor
|
|
* @param row {Object} - the resulting row
|
|
*/
|
|
constructor(row) {
|
|
this.row = row.grid_row;
|
|
this.column = row.grid_column;
|
|
this.submitted = row.submitted;
|
|
this.word = new WordWrapper(row.word_id, row);
|
|
this.grid = new GridWrapper(row.grid_id);
|
|
}
|
|
}
|
|
|
|
class GridWrapper {
|
|
constructor(id) {
|
|
this.id = id;
|
|
this._infoLoaded = false;
|
|
}
|
|
|
|
/**
|
|
* Loads all direct grid information
|
|
* @returns {Promise<void>}
|
|
* @private
|
|
*/
|
|
async _loadGridInfo() {
|
|
if (!this._infoLoaded) {
|
|
let result = await bdm.getGridInfo(this.id);
|
|
if (result) {
|
|
this.playerId = result.player_id;
|
|
this.lobbyId = result.lobby_id;
|
|
this.size = result.grid_size;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a matrix of the submitted-values of each grid field
|
|
* @returns {Promise<Array[[boolean]]>}
|
|
* @private
|
|
*/
|
|
async _getSubmittedMatrix() {
|
|
await this._loadGridInfo();
|
|
let rows = await bdm.getWordsInGrid(this.id);
|
|
let matrix = [];
|
|
for (let i = 0; i < this.size; i++) {
|
|
matrix[i] = [];
|
|
for (let j = 0; j < this.size; j++)
|
|
matrix[i][j] = rows.find(x => (x.grid_row === i && x.grid_column === j)).submitted;
|
|
}
|
|
return matrix;
|
|
}
|
|
|
|
/**
|
|
* Returns all fields in the grid
|
|
* @returns {Promise<Array>}
|
|
*/
|
|
async fields() {
|
|
let rows = await bdm.getWordsInGrid(this.id);
|
|
let fields = [];
|
|
for (let row of rows)
|
|
fields.push(new GridFieldWrapper(row));
|
|
return fields;
|
|
}
|
|
|
|
/**
|
|
* Returns a field with the current row and column
|
|
* @param row {Number} - the fields row
|
|
* @param column {Number} - the fields column
|
|
* @returns {Promise<GridFieldWrapper>}
|
|
*/
|
|
async field({row, column}) {
|
|
let result = await bdm.getGridField(this.id, row, column);
|
|
return new GridFieldWrapper(result);
|
|
}
|
|
|
|
/**
|
|
* Returns if a bingo is possible
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async bingo() {
|
|
let subMatrix = await this._getSubmittedMatrix();
|
|
return checkBingo(subMatrix);
|
|
}
|
|
|
|
/**
|
|
* Returns the lobby of the grid
|
|
* @returns {Promise<LobbyWrapper>}
|
|
*/
|
|
async lobby() {
|
|
await this._loadGridInfo();
|
|
return new LobbyWrapper(this.lobbyId);
|
|
}
|
|
|
|
/**
|
|
* Returns the player of the grid
|
|
* @returns {Promise<PlayerWrapper>}
|
|
*/
|
|
async player() {
|
|
await this._loadGridInfo();
|
|
return new PlayerWrapper(this.playerId);
|
|
}
|
|
|
|
/**
|
|
* Toggles submitted of a grid field
|
|
* @param row {Number} - the row of the field
|
|
* @param column {Number} - the column of the field
|
|
* @returns {Promise<GridFieldWrapper>}
|
|
*/
|
|
async toggleField(row, column) {
|
|
let result = await bdm.toggleGridFieldSubmitted(this.id, row, column);
|
|
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 lobbyWrapper.addInfoMessage(`${username} toggled "${word}"`);
|
|
else
|
|
await lobbyWrapper.addInfoMessage(`${username} untoggled "${word}"`);
|
|
return gridField;
|
|
}
|
|
}
|
|
|
|
class MessageWrapper {
|
|
/**
|
|
* constructor
|
|
* @param row {Object} - the database row of the message
|
|
*/
|
|
constructor(row) {
|
|
this.id = row.id;
|
|
this.content = row.content;
|
|
this.htmlContent = md.renderInline(preMarkdownParse(this.content));
|
|
this.author = new PlayerWrapper(row.player_id);
|
|
this.lobby = new LobbyWrapper(row.lobby_id);
|
|
this.type = row.type;
|
|
this.created = row.created;
|
|
}
|
|
}
|
|
|
|
class PlayerWrapper {
|
|
/**
|
|
* constructor
|
|
* @param id {Number} - the id of the player
|
|
*/
|
|
constructor(id) {
|
|
this.id = id;
|
|
this._infoLoaded = false;
|
|
}
|
|
|
|
/**
|
|
* Loads all player information
|
|
* @returns {Promise<Boolean>}
|
|
* @private
|
|
*/
|
|
async _loadPlayerInfo() {
|
|
if (!this._infoLoaded) {
|
|
let result = await bdm.getPlayerInfo(this.id);
|
|
if (result) {
|
|
this._uname = result.username;
|
|
this.expire = result.expire;
|
|
this._infoLoaded = true;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns if the player exists
|
|
* @returns {Promise<Boolean>}
|
|
*/
|
|
async exists() {
|
|
return await this._loadPlayerInfo();
|
|
}
|
|
|
|
/**
|
|
* Returns the grid for a specific lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<GridWrapper>}
|
|
*/
|
|
async grid({lobbyId}) {
|
|
let currentRound = (await new LobbyWrapper(lobbyId).currentRound()).id;
|
|
let result = await bdm.getGridForPlayerLobbyRound(lobbyId, this.id, currentRound);
|
|
if (result)
|
|
return new GridWrapper(result.id);
|
|
}
|
|
|
|
/**
|
|
* Returns if the user has a valid grid.
|
|
* @param lobbyId
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async hasGrid(lobbyId) {
|
|
let grid = await this.grid({lobbyId: lobbyId});
|
|
if (grid) {
|
|
let fields = await grid.fields();
|
|
let lobbyWrapper = new LobbyWrapper(lobbyId);
|
|
return fields.length === (await lobbyWrapper.gridSize()) ** 2;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the username of the player
|
|
* @returns {Promise<String|null>}
|
|
*/
|
|
async username() {
|
|
await this._loadPlayerInfo();
|
|
return this._uname;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of wins of a player in a lobby
|
|
* @param lobbyId {Number} - the id of the lobby
|
|
* @returns {Promise<null|PlayerWrapper.wins>}
|
|
*/
|
|
async wins({lobbyId}) {
|
|
let result = await bdm.getLobbyPlayerWins(lobbyId, this.id);
|
|
if (result && result.wins)
|
|
return result.wins;
|
|
else
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class RoundWrapper {
|
|
/**
|
|
* constructor
|
|
* @param id {Number} - the id of the round
|
|
* @param [row] {Object} - already queried row of the row
|
|
*/
|
|
constructor(id, row) {
|
|
this.id = id;
|
|
this._infoLoaded = false;
|
|
if (row)
|
|
this._assignProperties(row);
|
|
}
|
|
|
|
/**
|
|
* Adds data to the round wrapper
|
|
* @param row
|
|
* @private
|
|
*/
|
|
_assignProperties(row) {
|
|
this._start = row.start;
|
|
this._finish = row.finish;
|
|
this._status = row.status;
|
|
this._lobbyId = row.lobby_id;
|
|
this._winnerId = row.winner;
|
|
this._infoLoaded = true;
|
|
}
|
|
|
|
/**
|
|
* Loads the round info from the database
|
|
* @returns {Promise<void>}
|
|
* @private
|
|
*/
|
|
async _loadRoundInfo() {
|
|
if (!this._infoLoaded) {
|
|
let row = await bdm.getRoundInfo(this.id);
|
|
if (row)
|
|
this._assignProperties(row);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the winner of the bingo round (if exists)
|
|
* @returns {Promise<PlayerWrapper>}
|
|
*/
|
|
async winner() {
|
|
await this._loadRoundInfo();
|
|
if (this._winnerId)
|
|
return new PlayerWrapper(this._winnerId);
|
|
}
|
|
|
|
/**
|
|
* Returns the start timestamp of the round
|
|
* @returns {Promise<String>}
|
|
*/
|
|
async start() {
|
|
await this._loadRoundInfo();
|
|
return this._start;
|
|
}
|
|
|
|
/**
|
|
* Returns the finish timestamp of the round if it exists
|
|
* @returns {Promise<String|null>}
|
|
*/
|
|
async finish() {
|
|
await this._loadRoundInfo();
|
|
return this._finish;
|
|
}
|
|
|
|
/**
|
|
* Returns the status of a round
|
|
* @returns {Promise<String>}
|
|
*/
|
|
async status() {
|
|
await this._loadRoundInfo();
|
|
return this._status;
|
|
}
|
|
|
|
/**
|
|
* Returns the lobby the round is belonging to
|
|
* @returns {Promise<LobbyWrapper>}
|
|
*/
|
|
async lobby() {
|
|
await this._loadRoundInfo();
|
|
return new LobbyWrapper(this._lobbyId);
|
|
}
|
|
|
|
/**
|
|
* Updates the status of the round to a new one
|
|
* @param status {String<8>} - the new status
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async updateStatus(status) {
|
|
let updateResult = await bdm.updateRoundStatus(this.id, status);
|
|
if (updateResult)
|
|
this._assignProperties(updateResult);
|
|
}
|
|
|
|
/**
|
|
* Sets the round to finished
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async setFinished() {
|
|
let updateResult = await bdm.setRoundFinished(this.id);
|
|
if (updateResult)
|
|
this._assignProperties(updateResult);
|
|
}
|
|
|
|
/**
|
|
* Sets the winner of the round
|
|
* @param winnerId {Number} - the id of the winner
|
|
*/
|
|
async setWinner(winnerId) {
|
|
let status = await this.status();
|
|
if (status !== "FINISHED") {
|
|
let updateResult = await bdm.setRoundWinner(this.id, winnerId);
|
|
if (updateResult)
|
|
await this.setFinished();
|
|
(await this.lobby()).socket.emit('statusChange', 'FINISHED', await resolvePlayer(new PlayerWrapper(winnerId)));
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
class LobbyWrapper {
|
|
/**
|
|
* constructor
|
|
* @param id {Number} - the id of the lobby
|
|
* @param [row] {Object} - the optional row object of the lobby to load info from
|
|
*/
|
|
constructor(id, row) {
|
|
this.id = id;
|
|
this.socket = sockets[id];
|
|
this._infoLoaded = false;
|
|
if (row)
|
|
this._assignProperties(row);
|
|
}
|
|
|
|
/**
|
|
* Loads information about the lobby if it hasn't been loaded yet
|
|
* @param [force] {Boolean} - forces a data reload
|
|
* @returns {Promise<void>}
|
|
* @private
|
|
*/
|
|
async _loadLobbyInfo(force) {
|
|
if (!this._infoLoaded && !force) {
|
|
let row = await bdm.updateLobbyExpiration(this.id);
|
|
this._assignProperties(row);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assigns properties to the lobby wrapper
|
|
* @param row {Object} - the row to assign properties from
|
|
* @private
|
|
*/
|
|
_assignProperties(row) {
|
|
if (row) {
|
|
this.admin_id = row.admin_id;
|
|
this.grid_size = row.grid_size;
|
|
this.expire = row.expire;
|
|
this.current_round = row.current_round;
|
|
this.last_round = row.last_round;
|
|
this._infoLoaded = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits an event is a socket exists for the lobby
|
|
*/
|
|
emit() {
|
|
if (this.socket)
|
|
this.socket.emit(...arguments);
|
|
}
|
|
|
|
/**
|
|
* Returns if the lobby exists (based on one loaded attribute)
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async exists() {
|
|
await this._loadLobbyInfo();
|
|
return !!this.expire;
|
|
}
|
|
|
|
/**
|
|
* returns the players in the lobby
|
|
* @returns {Promise<Array>}
|
|
*/
|
|
async players() {
|
|
let rows = await bdm.getLobbyPlayers(this.id);
|
|
let players = [];
|
|
for (let row of rows)
|
|
players.push(new PlayerWrapper(row.player_id));
|
|
return players;
|
|
}
|
|
|
|
/**
|
|
* Returns the admin of the lobby
|
|
* @returns {Promise<PlayerWrapper>}
|
|
*/
|
|
async admin() {
|
|
await this._loadLobbyInfo();
|
|
return new PlayerWrapper(this.admin_id);
|
|
}
|
|
|
|
/**
|
|
* Returns the active round of the lobby
|
|
* @returns {Promise<RoundWrapper>}
|
|
*/
|
|
async currentRound() {
|
|
await this._loadLobbyInfo();
|
|
if (this.current_round)
|
|
return new RoundWrapper(this.current_round);
|
|
}
|
|
|
|
/**
|
|
* Returns all round of a lobby
|
|
* @returns {Promise<Array>}
|
|
*/
|
|
async rounds() {
|
|
let rows = await bdm.getLobbyRounds(this.id);
|
|
let rounds = [];
|
|
for (let row of rows)
|
|
rounds.push(new RoundWrapper(row.id, row));
|
|
return rounds;
|
|
}
|
|
|
|
/**
|
|
* Returns the grid-size of the lobby
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async gridSize() {
|
|
await this._loadLobbyInfo();
|
|
return this.grid_size;
|
|
}
|
|
|
|
/**
|
|
* Returns a number of messages send in the lobby
|
|
* @param limit
|
|
* @returns {Promise<Array>}
|
|
*/
|
|
async messages({limit}) {
|
|
let rows = await bdm.getLobbyMessages(this.id, limit);
|
|
let messages = [];
|
|
for (let row of rows)
|
|
messages.push(new MessageWrapper(row));
|
|
return messages.reverse();
|
|
}
|
|
|
|
/**
|
|
* Returns all words in a lobby
|
|
* @returns {Promise<Array>}
|
|
*/
|
|
async words() {
|
|
let rows = await bdm.getWordsForLobby(this.id);
|
|
let words = [];
|
|
for (let row of rows)
|
|
words.push(new WordWrapper(row.id, row));
|
|
return words;
|
|
}
|
|
|
|
/**
|
|
* Creates a new round
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async _createRound() {
|
|
let result = await bdm.addRound(this.id);
|
|
if (result && result.id) {
|
|
let updateResult = await bdm.setLobbyCurrentRound(this.id, result.id);
|
|
this._assignProperties(updateResult);
|
|
return result.id;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a grid for each player
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async _createGrids() {
|
|
let words = await this.words();
|
|
let players = await this.players();
|
|
let currentRound = this.current_round;
|
|
for (let player of players) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
let gridId = (await bdm.addGrid(this.id, player.id, currentRound)).id;
|
|
let gridContent = generateWordGrid(this.grid_size, words);
|
|
|
|
let gridWords = [];
|
|
for (let i = 0; i < gridContent.length; i++)
|
|
for (let j = 0; j < gridContent[i].length; j++)
|
|
gridWords.push({wordId: gridContent[i][j].id, row: i, column: j});
|
|
await bdm.addWordsToGrid(gridId, gridWords);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new round and new grids for each player
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async startNewRound() {
|
|
let words = await this.words();
|
|
if (words && words.length > 0) {
|
|
let currentRound = await this.currentRound();
|
|
if (currentRound)
|
|
await currentRound.setFinished();
|
|
await this._createRound();
|
|
await this._createGrids();
|
|
await this.setRoundStatus('ACTIVE');
|
|
this.emit('statusChange', 'ACTIVE');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the grid size of the lobby
|
|
* @param gridSize {Number} - the new grid size
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async setGridSize(gridSize) {
|
|
let updateResult = await bdm.setLobbyGridSize(this.id, gridSize);
|
|
this._assignProperties(updateResult);
|
|
}
|
|
|
|
/**
|
|
* Returns if the specific player exists in the lobby
|
|
* @param playerId
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async hasPlayer(playerId) {
|
|
let result = (await bdm.getPlayerInLobby(playerId, this.id));
|
|
return (result && result.player_id);
|
|
}
|
|
|
|
/**
|
|
* Adds a word to the lobby
|
|
* @param word
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async addWord(word) {
|
|
await bdm.addWordToLobby(this.id, word);
|
|
}
|
|
|
|
/**
|
|
* Removes a word from the lobby
|
|
* @param wordId
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async removeWord(wordId) {
|
|
await bdm.removeWordFromLobby(this.id, wordId);
|
|
}
|
|
|
|
/**
|
|
* Sets the words of the lobby
|
|
* @param words
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async setWords(words) {
|
|
if (words.length > 0 && !await this.roundActive()) {
|
|
words = words.map(x => x.substring(0, 200));
|
|
let {newWords, removedWords} = await this._filterWords(words);
|
|
for (let word of newWords)
|
|
await this.addWord(word);
|
|
for (let word of removedWords)
|
|
await this.removeWord(word.id);
|
|
this.emit('wordsChange');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filters the bingo words
|
|
* @param words
|
|
* @returns {Promise<{removedWords: *[], newWords: *[]}>}
|
|
* @private
|
|
*/
|
|
async _filterWords(words) {
|
|
let curWords = await this.words();
|
|
let currentWords = [];
|
|
let currentWordContent = [];
|
|
for (let word of curWords) {
|
|
currentWordContent.push(await word.content());
|
|
currentWords.push({
|
|
id: word.id,
|
|
content: (await word.content())
|
|
});
|
|
}
|
|
let newWords = words.filter(x => (!currentWordContent.includes(x)));
|
|
let removedWords = currentWords.filter(x => !words.includes(x.content));
|
|
|
|
return {
|
|
newWords: newWords,
|
|
removedWords: removedWords
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Adds an info message and emits the message event.
|
|
* @param message {String} - the info messages content
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async addInfoMessage(message) {
|
|
let result = await bdm.addInfoMessage(this.id, message);
|
|
this.emit('message', await resolveMessage(new MessageWrapper(result)));
|
|
}
|
|
|
|
/**
|
|
* Adds a player to the lobby.
|
|
* @param playerId
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async addPlayer(playerId) {
|
|
await bdm.addPlayerToLobby(playerId, this.id);
|
|
let playerWrapper = new PlayerWrapper(playerId);
|
|
this.emit('playerJoin', await resolvePlayer(playerWrapper));
|
|
let username = await playerWrapper.username();
|
|
await this.addInfoMessage(`${username} joined.`);
|
|
await this._loadLobbyInfo(true);
|
|
}
|
|
|
|
/**
|
|
* Removes a player from the lobby
|
|
* @param playerId
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async removePlayer(playerId) {
|
|
await bdm.removePlayerFromLobby(playerId, this.id);
|
|
let username = await new PlayerWrapper(playerId).username();
|
|
this.emit('playerLeave', playerId);
|
|
await this.addInfoMessage(`${username} left.`);
|
|
await this._loadLobbyInfo(true);
|
|
}
|
|
|
|
/**
|
|
* Returns if the lobby is in an active round
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async roundActive() {
|
|
let currentRound = await this.currentRound();
|
|
return currentRound && (await currentRound.status()) === 'ACTIVE';
|
|
}
|
|
|
|
/**
|
|
* Sets the status of the current round
|
|
* @param status {String} - the status
|
|
* @returns {Promise<RoundWrapper>}
|
|
*/
|
|
async setRoundStatus(status) {
|
|
let currentRound = await this.currentRound();
|
|
await currentRound.updateStatus(status);
|
|
await this.addInfoMessage(`Admin set round status to ${status}`);
|
|
this.emit('statusChange', status);
|
|
|
|
if (status === 'FINISHED')
|
|
await bdm.clearGrids(this.id);
|
|
return currentRound;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the parameterized value sql for inserting.
|
|
* @param columnCount
|
|
* @param rowCount
|
|
* @param [offset]
|
|
* @returns {string}
|
|
*/
|
|
function buildSqlParameters(columnCount, rowCount, offset) {
|
|
let sql = '';
|
|
for (let i = 0; i < rowCount; i++) {
|
|
sql += '(';
|
|
for (let j = 0; j < columnCount; j++)
|
|
sql += `$${(i*columnCount)+j+1+offset},`;
|
|
sql = sql.replace(/,$/, '') + '),';
|
|
}
|
|
return sql.replace(/,$/, '');
|
|
}
|
|
|
|
/**
|
|
* Replaces tag signs with html-escaped signs.
|
|
* @param htmlString
|
|
* @returns {string}
|
|
*/
|
|
function replaceTagSigns(htmlString) {
|
|
return htmlString.replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
/**
|
|
* Shuffles the elements in an array
|
|
* @param array {Array<*>}
|
|
* @returns {Array<*>}
|
|
*/
|
|
function shuffleArray(array) {
|
|
let counter = array.length;
|
|
while (counter > 0) {
|
|
let index = Math.floor(Math.random() * counter);
|
|
counter--;
|
|
let temp = array[counter];
|
|
array[counter] = array[index];
|
|
array[index] = temp;
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/**
|
|
* Inflates an array to a minimum Size
|
|
* @param array {Array} - the array to inflate
|
|
* @param minSize {Number} - the minimum size that the array needs to have
|
|
* @returns {Array}
|
|
*/
|
|
function inflateArray(array, minSize) {
|
|
let resultArray = array;
|
|
let iterations = Math.ceil(minSize/array.length)-1;
|
|
for (let i = 0; i < iterations; i++)
|
|
resultArray = [...resultArray, ...resultArray];
|
|
return resultArray;
|
|
}
|
|
|
|
/**
|
|
* Generates a word grid with random word placements in the given dimensions
|
|
* @param size {Array<Number>} - the dimensions of the grid
|
|
* @param words {Array<*>} - the words included in the grid
|
|
* @returns {Array}
|
|
*/
|
|
function generateWordGrid(size, words) {
|
|
let shuffledWords = shuffleArray(inflateArray(words, size**2));
|
|
let grid = [];
|
|
for (let x = 0; x < size; x++) {
|
|
grid[x] = [];
|
|
for (let y = 0; y < size; y++)
|
|
grid[x][y] = shuffledWords[(x * size) + y];
|
|
}
|
|
return grid;
|
|
}
|
|
|
|
/**
|
|
* Checks if a diagonal bingo is possible
|
|
* @param fg {Array<Array<boolean>>} - the grid with the checked (submitted) values
|
|
* @returns {boolean|boolean|*}
|
|
*/
|
|
function checkBingoDiagnoal(fg) {
|
|
let bingoCheck = true;
|
|
// diagonal check
|
|
for (let i = 0; i < fg.length; i++)
|
|
bingoCheck = fg[i][i] && bingoCheck;
|
|
if (bingoCheck)
|
|
return true;
|
|
bingoCheck = true;
|
|
for (let i = 0; i < fg.length; i++)
|
|
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;
|
|
for (let row of fg) {
|
|
bingoCheck = true;
|
|
for (let field of row)
|
|
bingoCheck = field && bingoCheck;
|
|
if (bingoCheck)
|
|
return 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)
|
|
return true;
|
|
}
|
|
return bingoCheck;
|
|
}
|
|
|
|
/**
|
|
* Checks if a bingo exists in the bingo grid.
|
|
* @param fg {Array<[Boolean]>}
|
|
* @returns {boolean}
|
|
*/
|
|
function checkBingo(fg) {
|
|
let diagonalBingo = checkBingoDiagnoal(fg);
|
|
let verticalCheck = checkBingoVertical(fg);
|
|
let horizontalCheck = checkBingoHorizontal(fg);
|
|
return diagonalBingo || verticalCheck || horizontalCheck;
|
|
}
|
|
|
|
/**
|
|
* Parses the message and replaces all links with markdown-links and images with markdown-images.
|
|
* @param message {String} - the raw message
|
|
*/
|
|
function preMarkdownParse(message) {
|
|
let linkMatch = /(^|[^(])https?:\/\/((([\w-]+\.)+[\w-]+)(\S*))([^)]|$)/g;
|
|
let imageMatch = /.*\.(\w+)/g;
|
|
let imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg'];
|
|
let links = message.match(linkMatch);
|
|
|
|
if (links)
|
|
for (let link of links) {
|
|
let linkGroups = linkMatch.exec(link);
|
|
let imgGroups = imageMatch.exec(link);
|
|
|
|
if (imgGroups && imgGroups[1] && imageExtensions.includes(imgGroups[1]))
|
|
message = message.replace(link, `![${linkGroups[1]}](${link})`);
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
/**
|
|
* Gets player data for a lobby
|
|
* @param lobbyWrapper
|
|
* @returns {Promise<Array>}
|
|
*/
|
|
async function getPlayerData(lobbyWrapper) {
|
|
let playerData = [];
|
|
let adminId = (await lobbyWrapper.admin()).id;
|
|
|
|
for (let player of await lobbyWrapper.players())
|
|
playerData.push({
|
|
id: player.id,
|
|
wins: await player.wins({lobbyId: lobbyWrapper.id}),
|
|
username: await player.username(),
|
|
isAdmin: (player.id === adminId)
|
|
});
|
|
playerData.sort((a, b) => (a.isAdmin? -1 : (b.wins - a.wins) || a.id));
|
|
return playerData;
|
|
}
|
|
|
|
/**
|
|
* Gets data for all words of a lobby
|
|
* @param lobbyWrapper
|
|
* @returns {Promise<Array>}
|
|
*/
|
|
async function getWordsData(lobbyWrapper) {
|
|
let wordList = [];
|
|
|
|
for (let word of await lobbyWrapper.words())
|
|
wordList.push(await word.content());
|
|
return wordList;
|
|
}
|
|
|
|
/**
|
|
* Returns a completely resolved grid
|
|
* @param lobbyId
|
|
* @param playerId
|
|
* @returns {Promise<{bingo: boolean, fields: Array}>}
|
|
*/
|
|
async function getGridData(lobbyId, playerId) {
|
|
let playerWrapper = new PlayerWrapper(playerId);
|
|
let lobbyWrapper = new LobbyWrapper(lobbyId);
|
|
let grid = await playerWrapper.grid({lobbyId: lobbyId});
|
|
let fields = await grid.fields();
|
|
let fieldGrid = [];
|
|
|
|
for (let i = 0; i < await lobbyWrapper.gridSize(); i++) {
|
|
fieldGrid[i] = [];
|
|
for (let j = 0; j < await lobbyWrapper.gridSize(); j++) {
|
|
let field = fields.find(x => (x.row === i && x.column === j));
|
|
fieldGrid[i][j] = {
|
|
row: field.row,
|
|
column: field.column,
|
|
word: await field.word.content(),
|
|
submitted: field.submitted
|
|
};
|
|
}
|
|
}
|
|
|
|
return {fields: fieldGrid, bingo: await grid.bingo()};
|
|
}
|
|
|
|
/**
|
|
* Resolves a message wrapper object
|
|
* @param msgWrapper
|
|
* @returns {Promise<{author: {id: (*|MessageWrapper.author.id), username: String}, id: *, content: *, timestamp: Timestamp | * | number, htmlContent: *}>}
|
|
*/
|
|
async function resolveMessage(msgWrapper) {
|
|
return {
|
|
id: msgWrapper.id,
|
|
type: msgWrapper.type,
|
|
content: msgWrapper.content,
|
|
timestamp: msgWrapper.timestamp,
|
|
htmlContent: msgWrapper.htmlContent,
|
|
author: {
|
|
id: msgWrapper.author.id,
|
|
username: await msgWrapper.author.username()
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resolves a player wrapper object
|
|
* @param playerWrapper
|
|
* @param lobbyId
|
|
* @returns {Promise<{wins: PlayerWrapper.wins, id: *, username: (String|*)}>}
|
|
*/
|
|
async function resolvePlayer(playerWrapper, lobbyId) {
|
|
return {
|
|
id: playerWrapper.id,
|
|
username: await playerWrapper.username(),
|
|
wins: await playerWrapper.wins({lobbyId: lobbyId})
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resolves a fieldWrapper object
|
|
* @param fieldWrapper
|
|
* @returns {Promise<{submitted: (Object.submitted|*), column: *, bingo: boolean, row: (Object.grid_row|number|*)}>}
|
|
*/
|
|
async function resolveGridField(fieldWrapper) {
|
|
return {
|
|
row: fieldWrapper.row,
|
|
column: fieldWrapper.column,
|
|
submitted: fieldWrapper.submitted,
|
|
bingo: await fieldWrapper.grid.bingo()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns resolved message data.
|
|
* @param lobbyId
|
|
* @returns {Promise<Array>}
|
|
*/
|
|
async function getMessageData(lobbyId) {
|
|
let lobbyWrapper = new LobbyWrapper(lobbyId);
|
|
let messages = await lobbyWrapper.messages({limit: 20});
|
|
let msgReturn = [];
|
|
for (let message of messages)
|
|
msgReturn.push(Object.assign(message, {
|
|
playerId: message.author.id,
|
|
username: await message.author.username()
|
|
}));
|
|
return msgReturn;
|
|
}
|
|
|
|
// -- 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.error(err);
|
|
}
|
|
});
|
|
socket.on('messageEdit', async (context, message, messageId) => {
|
|
try {
|
|
let row = await bdm.getMessageData(messageId);
|
|
if (row.player_id === Number(context.playerId)) {
|
|
let result = await bdm.editMessage(messageId, message);
|
|
let messageWrapper = new MessageWrapper(result);
|
|
lobbySocket.emit('messageEdit', await resolveMessage(messageWrapper));
|
|
} else {
|
|
socket.emit('userError', "You are only allowed to edit your messages.");
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
});
|
|
socket.on('messageDelete', async (context, messageId) => {
|
|
try {
|
|
let row = await bdm.getMessageData(messageId);
|
|
|
|
if (row.player_id === Number(context.playerId)) {
|
|
await bdm.deleteMessage(messageId);
|
|
lobbySocket.emit('messageDelete', messageId);
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
});
|
|
socket.on('fieldToggle', async (context, location) => {
|
|
let {row, column} = location;
|
|
let result = await (await (new PlayerWrapper(context.playerId)).grid({lobbyId: lobbyId}))
|
|
.toggleField(row, column);
|
|
socket.emit('fieldChange', await resolveGridField(result));
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
let bdm = new BingoDataManager(pgPool);
|
|
|
|
router.init = async (bingoIo, io) => {
|
|
await bdm.init();
|
|
|
|
for (let id of await bdm.getLobbyIds())
|
|
createSocketIfNotExist(io, id);
|
|
|
|
router.use(async (req, res, next) => {
|
|
if (req.session.bingoPlayerId)
|
|
await bdm.updatePlayerExpiration(req.session.bingoPlayerId);
|
|
next();
|
|
});
|
|
|
|
router.get('/', async (req, res) => {
|
|
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 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)) {
|
|
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(),
|
|
changelog: md.render(globals.changelog),
|
|
primaryJoin: (req.query.g && await lobbyWrapper.exists())
|
|
});
|
|
}
|
|
});
|
|
|
|
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
|
|
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) {
|
|
let lobbyWrapper = new LobbyWrapper(req.query.g);
|
|
if (await lobbyWrapper.exists()) {
|
|
lobbyWrapper.emit('usernameChange',
|
|
await resolvePlayer(new PlayerWrapper(playerId), req.query.g));
|
|
await lobbyWrapper.addInfoMessage(`${oldName} changed username to ${username}`);
|
|
}
|
|
}
|
|
}
|
|
return new PlayerWrapper(playerId);
|
|
} else {
|
|
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, result);
|
|
} else {
|
|
res.status(413);
|
|
}
|
|
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.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();
|
|
} else {
|
|
res.status(403);
|
|
return new GraphQLError('You are not an admin');
|
|
}
|
|
},
|
|
setRoundStatus: async ({status}) => {
|
|
let admin = await lobbyWrapper.admin();
|
|
if (admin.id === playerId) {
|
|
return await lobbyWrapper.setRoundStatus(status);
|
|
} else {
|
|
res.status(403);
|
|
return new GraphQLError('You are not an admin');
|
|
}
|
|
},
|
|
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);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
};
|
|
};
|
|
|
|
module.exports = router;
|