From c3585ad08b5d6d5e5a7f5bc4ecee1d017b879467 Mon Sep 17 00:00:00 2001 From: Niels Simenon Date: Thu, 4 Nov 2021 01:51:43 +0100 Subject: [PATCH] Added socket reconnection. --- config/default.js | 36 +++++++------ src/app.js | 108 +++++++++++++++++++++++-------------- src/games/mash.js | 134 +++++++++++++++++++++++----------------------- 3 files changed, 155 insertions(+), 123 deletions(-) diff --git a/config/default.js b/config/default.js index 5e16307..02bada8 100644 --- a/config/default.js +++ b/config/default.js @@ -1,21 +1,23 @@ 'use strict'; module.exports = { - user: { - id: 'clive', - key: 'abcdefgh12345678', - username: 'Clive', - // optional - gender: 'male', - birthdate: new Date(1952, 11, 10), - avatar: 'https://i.imgur.com/IZwrjjG.png', - }, - socket: 'ws://127.0.0.1:3000/socket', - api: 'http://127.0.0.1:3000/api', - prefix: '~', - style: { - color: 'var(--message-56)', - }, - games: ['mash'], - channels: ['GamesNight'], + user: { + id: 'clive', + key: 'abcdefgh12345678', + username: 'Clive', + // optional + gender: 'male', + birthdate: new Date(1952, 11, 10), + avatar: 'https://i.imgur.com/IZwrjjG.png', + }, + uniqueUsername: true, + socket: 'ws://127.0.0.1:3000/socket', + api: 'http://127.0.0.1:3000/api', + prefix: '~', + style: { + color: 'var(--message-56)', + }, + games: ['mash', 'cursed'], + channels: ['GamesNight'], + reconnectDelay: 10, // seconds }; diff --git a/src/app.js b/src/app.js index f32901b..85aeb26 100644 --- a/src/app.js +++ b/src/app.js @@ -1,6 +1,7 @@ 'use strict'; const config = require('config'); +const { setTimeout: delay } = require('timers/promises'); const bhttp = require('bhttp'); const WebSocket = require('ws'); const fs = require('fs').promises; @@ -11,7 +12,10 @@ const points = {}; async function auth() { const httpSession = bhttp.session(); - const res = await httpSession.post(`${config.api}/session`, config.user, { + const res = await httpSession.post(`${config.api}/session`, { + ...config.user, + username: config.uniqueUsername ? `${config.user.username}-${new Date().getTime().toString().slice(-5)}` : config.user.username, + }, { encodeJSON: true, }); @@ -38,18 +42,6 @@ async function getWsId(httpSession) { return res.body; } -function connect(wsCreds, sessionCookie) { - const ws = new WebSocket(`${config.socket}?${new URLSearchParams({ v: wsCreds.wsId, t: wsCreds.timestamp }).toString()}`, [], { - headers: { - cookie: sessionCookie, - }, - }); - - logger.info(`Connected to ${config.socket}`); - - return ws; -} - async function setPoints(gameKey, user, value, mode = 'add') { const userKey = `${user.id}:${user.username}`; @@ -96,7 +88,7 @@ function getLeaderboard(game, { user, room }) { } function onConnect(data, bot) { - bot.transmit('joinRooms', { rooms: config.channels }); + bot.socket.transmit('joinRooms', { rooms: config.channels }); } function onRooms({ rooms, users }, bot) { @@ -108,7 +100,7 @@ function onRooms({ rooms, users }, bot) { /* eslint-enable no-param-reassign */ rooms.forEach((room) => { - bot.transmit('message', { + bot.socket.transmit('message', { roomId: room.id, body: `Hi, I am ${config.user.username}, your game host!`, style: config.style, @@ -184,29 +176,12 @@ async function initPoints() { } } -async function init() { - const { user, httpSession, sessionCookie } = await auth(); - const wsCreds = await getWsId(httpSession); - const ws = connect(wsCreds, sessionCookie); - - ws.transmit = (domain, data) => { - ws.send(JSON.stringify([domain, data])); - }; - - const bot = { - user, - ws, - httpSession, - rooms: [], - users: [], - transmit: ws.transmit, - }; - +function getGames(bot) { const games = config.games.reduce((acc, key) => { const game = require(`./games/${key}`); // eslint-disable-line global-require, import/no-dynamic-require const sendMessage = (body, roomId) => { - bot.transmit('message', { + bot.socket.transmit('message', { roomId, body: `[${game.name || key}] ${body}`, style: config.style, @@ -227,13 +202,66 @@ async function init() { }; }, {}); - ws.on('message', (msg) => { - const [domain, data] = JSON.parse(msg); + return games; +} - if (messageHandlers[domain]) { - messageHandlers[domain](data, bot, games); - } - }); +async function connect(wsCreds, sessionCookie, bot, games) { + const socket = { ws: { readyState: 0 } }; + + socket.connect = () => { + logger.info(`Attempting to connect to ${config.socket}`); + + socket.ws = new WebSocket(`${config.socket}?${new URLSearchParams({ v: wsCreds.wsId, t: wsCreds.timestamp }).toString()}`, [], { + headers: { + cookie: sessionCookie, + }, + }); + + socket.ws.on('message', (msg) => { + const [domain, data] = JSON.parse(msg); + + if (messageHandlers[domain]) { + messageHandlers[domain](data, bot, games); + } + }); + + socket.ws.on('close', async (info) => { + logger.error(`WebSocket closed, reconnecting in ${config.reconnectDelay} seconds: ${info}`); + + await delay(config.reconnectDelay * 1000); + socket.connect(); + }); + + socket.ws.on('error', async (error) => { + logger.error(`WebSocket error: ${error.message}`); + }); + + logger.info(`Connected to ${config.socket}`); + }; + + socket.transmit = (domain, data) => { + socket.ws.send(JSON.stringify([domain, data])); + }; + + socket.connect(); + + return socket; +} + +async function init() { + const { user, httpSession, sessionCookie } = await auth(); + const wsCreds = await getWsId(httpSession); + + const bot = { + user, + httpSession, + rooms: [], + users: [], + }; + + const games = getGames(bot); + + bot.socket = await connect(wsCreds, sessionCookie, bot, games); await initPoints(); } diff --git a/src/games/mash.js b/src/games/mash.js index 8b634e9..c1dc747 100644 --- a/src/games/mash.js +++ b/src/games/mash.js @@ -1,101 +1,103 @@ 'use strict'; const config = require('config'); + const words = require('../../assets/mash-words.json'); + let mash = null; function start(length, context, attempt = 0) { - const lengthWords = words[length]; + const lengthWords = words[length]; - if (!lengthWords) { - context.sendMessage(`No words with ${length} letters available`, context.room.id); + if (!lengthWords) { + context.sendMessage(`No words with ${length} letters available`, context.room.id); - return; - } - - if (mash) { - context.sendMessage(`The mash **${mash.anagram}** was not guessed, possible answers: ${mash.answers.map((answer) => `**${answer}**`).join(', ')}`, context.room.id); - context.logger.info(`Mash '${mash.anagram}' discarded`); - - mash = null; - } - - const wordEntries = Object.entries(lengthWords); - const [key, answers] = wordEntries[Math.floor(Math.random() * wordEntries.length)]; - - const anagram = key.split('').sort(() => Math.random() > .5 ? 1 : -1).join(''); - - if (answers.includes(anagram)) { - if (attempt >= 10) { - context.sendMessage(`Sorry, I did not find a mashable ${length}-letter word`); - return; + return; } - start(length, context, attempt + 1); + if (mash) { + context.sendMessage(`The mash **${mash.anagram}** was not guessed, possible answers: ${mash.answers.map((answer) => `**${answer}**`).join(', ')}`, context.room.id); + context.logger.info(`Mash '${mash.anagram}' discarded`); - return; - } + mash = null; + } - mash = { key, anagram, answers }; + const wordEntries = Object.entries(lengthWords); + const [key, answers] = wordEntries[Math.floor(Math.random() * wordEntries.length)]; - context.sendMessage(`Stomp stomp, here's your mash: **${mash.anagram}**`, context.room.id); - context.logger.info(`Mash started, '${anagram}' with answers ${answers.map((answer) => `'${answer}'`).join(', ')}`); + const anagram = key.split('').sort(() => (Math.random() > 0.5 ? 1 : -1)).join(''); + + if (answers.includes(anagram)) { + if (attempt >= 10) { + context.sendMessage(`Sorry, I did not find a mashable ${length}-letter word`); + return; + } + + start(length, context, attempt + 1); + + return; + } + + mash = { key, anagram, answers }; + + context.sendMessage(`Stomp stomp, here's your mash: **${mash.anagram}**`, context.room.id); + context.logger.info(`Mash started, '${anagram}' with answers ${answers.map((answer) => `'${answer}'`).join(', ')}`); } function play(word, context) { - if (word.length !== mash.key.length) { - context.sendMessage(`Your answer needs to be ${mash.key.length} letters, @${context.user.username}`, context.room.id); - return; - } + if (word.length !== mash.key.length) { + context.sendMessage(`Your answer needs to be ${mash.key.length} letters, @${context.user.username}`, context.room.id); + return; + } - const key = word.split('').sort().join(''); + const key = word.split('').sort().join(''); - if (key !== mash.key) { - context.sendMessage(`You are not using the letters in **${mash.anagram}**, @${context.user.username}`, context.room.id); - return; - } + if (key !== mash.key) { + context.sendMessage(`You are not using the letters in **${mash.anagram}**, @${context.user.username}`, context.room.id); + return; + } - if (word === mash.anagram) { - context.sendMessage(`@${context.user.username}... :expressionless:`, context.room.id); - return; - } + if (word === mash.anagram) { + context.sendMessage(`@${context.user.username}... :expressionless:`, context.room.id); + return; + } - if (mash.answers.includes(word)) { - context.sendMessage(mash.answers.length === 1 - ? `**${word}** is the right answer, @${context.user.username} now has **${context.user.points + 1} ${context.user.points === 0 ? 'point' : 'points'}**! There were no other options for **${mash.anagram}**.` - : `**${word}** is the right answer, @${context.user.username} now has **${context.user.points + 1} ${context.user.points === 0 ? 'point' : 'points'}**! Other options for **${mash.anagram}**: ${mash.answers.filter((answer) => answer !== word).map((word) => `**${word}**`).join(', ')}`, context.room.id); + if (mash.answers.includes(word)) { + context.sendMessage(mash.answers.length === 1 + ? `**${word}** is the right answer, @${context.user.username} now has **${context.user.points + 1} ${context.user.points === 0 ? 'point' : 'points'}**! There were no other options for **${mash.anagram}**.` + : `**${word}** is the right answer, @${context.user.username} now has **${context.user.points + 1} ${context.user.points === 0 ? 'point' : 'points'}**! Other options for **${mash.anagram}**: ${mash.answers.filter((answer) => answer !== word).map((answer) => `**${answer}**`).join(', ')}`, context.room.id); - context.logger.info(`Mash '${mash.anagram}' guessed by '${context.user.username}' with '${word}'`); - context.setPoints(context.user, 1); + context.logger.info(`Mash '${mash.anagram}' guessed by '${context.user.username}' with '${word}'`); + context.setPoints(context.user, 1); - mash = null; + mash = null; - setTimeout(() => start(word.length, context), 2000); - } + setTimeout(() => start(word.length, context), 2000); + } } function onCommand(args, context) { - const length = Number(args[0]); + const length = Number(args[0]); - if (!Number.isNaN(length)) { - start(length, context); - return; - } + if (!Number.isNaN(length)) { + start(length, context); + return; + } - if (!args[0] && !mash) { - context.sendMessage(`Start a mash with ${config.prefix}mash {length}`, context.room.id); - return; - } + if (!args[0] && !mash) { + context.sendMessage(`Start a mash with ${config.prefix}mash {length}`, context.room.id); + return; + } - if (!args[0] && mash) { - context.sendMessage(`The current mash is: **${mash.anagram}**`, context.room.id); - return; - } + if (!args[0] && mash) { + context.sendMessage(`The current mash is: **${mash.anagram}**`, context.room.id); + return; + } - play(args[0], context); + play(args[0], context); } module.exports = { - name: 'Mash', - onCommand, + name: 'Mash', + onCommand, };