From ad782a5126a8354204bc8ce2714f8165d6f58d9d Mon Sep 17 00:00:00 2001 From: Niels Simenon Date: Wed, 19 Oct 2022 01:24:13 +0200 Subject: [PATCH] Merged SChat and IRC support. --- config/default.js | 3 +- src/app.js | 342 +------------------------------------------- src/games/mash.js | 4 +- src/games/say.js | 7 +- src/games/trivia.js | 4 +- src/irc.js | 160 ++------------------- src/play.js | 225 +++++++++++++++++++++++++++++ src/schat.js | 181 +++++++++++++++++++++++ src/utils/style.js | 12 +- 9 files changed, 442 insertions(+), 496 deletions(-) create mode 100644 src/play.js create mode 100644 src/schat.js diff --git a/config/default.js b/config/default.js index ff60b01..690d8b5 100644 --- a/config/default.js +++ b/config/default.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = { - platform: 'irc', + platform: 'schat', user: { nick: 'aisha', username: 'Aisha', @@ -12,6 +12,7 @@ module.exports = { port: 6697, reconnectDelay: 10, // seconds prefix: '~', + labels: true, greeting: 'Hi, I am aisha, your game host!', usernamePrefix: '@', channels: ['##pendulum'], diff --git a/src/app.js b/src/app.js index 589bfa1..0fad5ec 100644 --- a/src/app.js +++ b/src/app.js @@ -1,343 +1,15 @@ '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; -const logger = require('simple-node-logger').createSimpleLogger(); -const { argv } = require('yargs'); -const instance = process.env.NODE_APP_INSTANCE || 'main'; -const points = {}; +const initSchat = require('./schat'); +const initIrc = require('./irc'); -logger.setLevel(argv.level || 'info'); - -async function auth() { - const httpSession = bhttp.session(); - const username = config.uniqueUsername ? `${config.user.username}-${new Date().getTime().toString().slice(-5)}` : config.user.username; - - const res = await httpSession.post(`${config.api}/session`, { - ...config.user, - username, - }, { - encodeJSON: true, - }); - - if (res.statusCode !== 200) { - throw new Error(`Failed to authenticate: ${res.body.toString()}`); - } - - logger.info(`Authenticated as '${username}' with ${config.api}`); - - return { - user: res.body, - httpSession, - sessionCookie: res.headers['set-cookie'][0], - }; +if (config.platform === 'irc') { + initIrc(); + return; } -async function getWsId(httpSession) { - const res = await httpSession.get(`${config.api}/socket`); - - if (res.statusCode !== 200) { - throw new Error(`Failed to retrieve WebSocket ID: ${res.body.toString()}`); - } - - return res.body; +if (config.platform === 'schat') { + initSchat(); } - -async function setPoints(defaultKey, user, value, { mode = 'add', key } = {}) { - const gameKey = key || defaultKey; - - if (!user) { - logger.warn(`Failed to set ${gameKey} points for missing user`); - return; - } - - const userKey = `${user.id}:${user.username}`; - - if (!points[gameKey]) { - points[gameKey] = {}; - } - - if (mode === 'add') { - points[gameKey][userKey] = (points[gameKey][userKey] || 0) + value; - } - - if (mode === 'set') { - points[gameKey][userKey] = value; - } - - await fs.writeFile(`./points-${instance}.json`, JSON.stringify(points, null, 4)); -} - -function getPoints(game, rawUsername, { user, room, command }) { - const username = rawUsername?.replace(/^@/, ''); - const gamePoints = points[command] || points[game.key]; - - const userPoints = username - ? Object.entries(gamePoints || {}).find(([identifier]) => identifier.split(':')[1] === username)?.[1] - : gamePoints?.[`${user?.id}:${user?.username}`]; - - game.sendMessage(`${username ? `**${username}** has` : 'You have'} scored **${userPoints || 0}** points in ${game.name}, @${user.username}`, room.id); -} - -function getLeaderboard(game, { user, room, command }) { - const leaderboard = points[command] || points[game.key]; - - if (!leaderboard || Object.keys(leaderboard).length === 0) { - game.sendMessage(`No points scored in ${game.name} yet!`, room.id); - - return; - } - - const curatedLeaderboard = Object.entries(leaderboard) - .sort(([, scoreA], [, scoreB]) => scoreB - scoreA) - .map(([userKey, score]) => { - const username = userKey.split(':')[1]; - return `**${username === user.username ? '@' : ''}${username}** at **${score}** points`; - }) - .slice(0, 10) - .join(', '); - - game.sendMessage(`The top ${Math.min(Object.keys(leaderboard).length, 10)} *${game.name}* players are: ${curatedLeaderboard}`, room.id); -} - -function onConnect(data, bot) { - bot.socket.transmit('joinRooms', { rooms: config.channels }); -} - -function onRooms({ rooms, users }, bot) { - logger.info(`Joined ${rooms.map((room) => room.name).join(', ')}`); - - /* eslint-disable no-param-reassign */ - bot.rooms = rooms.reduce((acc, room) => ({ ...acc, [room.id]: room }), {}); - bot.users = { ...bot.users, ...users }; - /* eslint-enable no-param-reassign */ - - rooms.forEach((room) => { - bot.socket.transmit('message', { - roomId: room.id, - body: `Hi, I am ${config.user.username}, your game host!`, - style: config.style, - }); - }); -} - -/* eslint-disable no-param-reassign */ -function onJoin(data, bot) { - if (bot.rooms[data.roomId] && !bot.rooms[data.roomId].userIds?.includes(data.user.id)) { - bot.users[data.user.id] = data.user; - bot.rooms[data.roomId].userIds.push(data.user.id); - } -} - -function onLeave(data, bot) { - if (bot.rooms[data.roomId]) { - bot.rooms[data.roomId].userIds = bot.rooms[data.roomId].userIds.filter((userId) => userId !== data.userId); - } -} - -function onMessage(message, bot, games) { - const body = message.originalBody || message.body; - const [, command, subcommand] = body?.match(new RegExp(`^${config.prefix}(\\w+)(?:\\:(\\w+))?`)) || []; - const user = bot.users[message.userId] || message.user; - const room = bot.rooms[message.roomId]; - - if (command) { - const args = body.split(/\s+/).slice(1); - const game = games[command]; - - if (['leaderboard', 'lead', 'leader', 'leaders', 'scoreboard', 'best'].includes(subcommand) && games[command]) { - getLeaderboard(games[command], { user, room, command }); - return; - } - - if (['points', 'score'].includes(subcommand) && games[command]) { - getPoints(games[command], args[0], { user, room, command }); - return; - } - - if (game && game.onCommand) { - if (user) { - user.points = points[game.key]?.[`${user.id}:${user.username}`] || 0; - } - - game.onCommand(args, { - ...game, - command, - subcommand, - bot, - message, - user, - room, - points: points[game.key] || {}, - logger, - }); - } - } - - Object.values(games).forEach((game) => game.onMessage?.(message, { - ...game, - bot, - message, - user: user && { - ...user, - points: points[game.key]?.[`${user.id}:${user.username}`] || 0, - }, - room, - logger, - })); -} - -const messageHandlers = { - connect: onConnect, - rooms: onRooms, - message: onMessage, - join: onJoin, - leave: onLeave, -}; - -async function initPoints() { - try { - const pointsFile = await fs.readFile(`./points-${instance}.json`, 'utf-8'); - - Object.assign(points, JSON.parse(pointsFile)); - } catch (error) { - if (error.code === 'ENOENT') { - logger.info('Creating new points file'); - - await fs.writeFile(`./points-${instance}.json`, '{}'); - initPoints(); - } - } -} - -function getGames(bot) { - const games = config.games.reduce((acc, key) => { - const game = require(`./games/${key.game || key}`); // eslint-disable-line global-require, import/no-dynamic-require - - const sendMessage = (body, roomId, options, recipient) => { - bot.socket.transmit('message', { - roomId, - recipient, - type: recipient ? 'whisper' : 'message', - body: options?.label === false ? body : `[${game.name || key}] ${body}`, - style: config.style, - }); - }; - - const setGamePoints = (userId, score, options) => setPoints(key, userId, score, options); - - const curatedGame = { - ...game, - ...(key.game && key), - name: game.name || key, - key, - sendMessage, - setPoints: setGamePoints, - }; - - if (game.onStart) { - game.onStart({ ...curatedGame, bot }); - } - - return { - ...acc, - [key]: curatedGame, - ...game.commands?.reduce((commandAcc, command) => ({ ...commandAcc, [command]: curatedGame }), {}), - }; - }, {}); - - return games; -} - -function handleError(error, socket, domain, data) { - logger.error(`${domain} '${JSON.stringify(data)}' triggered error: ${error.message} ${error.stack}`); - - if (data?.roomId) { - socket.transmit('message', { - body: ':zap::robot::zap: Many fragments! Some large, some small.', - type: 'message', - roomId: data.roomId, - }); - } -} - -async function connect(bot, games) { - const socket = { ws: { readyState: 0 } }; - - socket.connect = async () => { - try { - const { user, httpSession, sessionCookie } = await auth(); - const wsCreds = await getWsId(httpSession); - - bot.user = user; - bot.httpSession = httpSession; - - 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', async (msg) => { - const [domain, data] = JSON.parse(msg); - - logger.debug(`Received ${domain}: ${JSON.stringify(data)}`); - - if (messageHandlers[domain]) { - try { - await messageHandlers[domain](data, bot, games); - } catch (error) { - handleError(error, socket, domain, data); - } - } - }); - - 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}`); - } catch (error) { - logger.error(`Failed to connect, retrying in ${config.reconnectDelay} seconds: ${error.message}`); - - await delay(config.reconnectDelay * 1000); - socket.connect(); - } - }; - - socket.transmit = (domain, data) => { - socket.ws.send(JSON.stringify([domain, data])); - }; - - socket.connect(); - - return socket; -} - -async function init() { - const bot = { - rooms: [], - users: [], - }; - - const games = getGames(bot); - - bot.socket = await connect(bot, games); - - await initPoints(); -} - -init(); diff --git a/src/games/mash.js b/src/games/mash.js index b68ca49..2821304 100644 --- a/src/games/mash.js +++ b/src/games/mash.js @@ -196,9 +196,9 @@ function onCommand(args, context) { } function onMessage(message, context) { - const mash = mashes.get(context.room.id); + const mash = mashes.get(context.room?.id); - if (mash && context.user?.id !== config.user.id) { + if (mash && message.type === 'message' && context.user?.id !== config.user.id) { play(message.body, context, true); } } diff --git a/src/games/say.js b/src/games/say.js index 8cf7aa5..049968e 100644 --- a/src/games/say.js +++ b/src/games/say.js @@ -22,7 +22,12 @@ function onCommand(args, context) { const room = Object.values(context.bot.rooms).find((botRoom) => botRoom.name === roomName); if (room) { - context.sendMessage(args.slice(1).join(' '), roomName, { label: false }); + context.sendMessage(args.slice(1).join(' '), room.id, { label: false }); + return; + } + + if (context.message.recipient === config.user.username) { + context.sendMessage(args.join(' '), null, { type: 'message', label: false }, context.user.username); } return; diff --git a/src/games/trivia.js b/src/games/trivia.js index b4477d2..2c73729 100644 --- a/src/games/trivia.js +++ b/src/games/trivia.js @@ -60,7 +60,7 @@ async function playRound(context, round = 0) { const question = game.questions[round]; - context.sendMessage(`${style.bold(style.purple(`Question ${round + 1}/${game.questions.length}`))} ${style.gray(`(${question.category})`)}: ${question.question}`, context.room.id); + context.sendMessage(`${style.bold(style.purple(`Question ${round + 1}/${game.questions.length}`))} ${style.silver(`(${question.category})`)}: ${question.question}`, context.room.id); context.logger.info(`Trivia asked "${question.question}" with answer: ${question.answer}`); try { @@ -192,7 +192,7 @@ function onCommand(args, context) { } async function onMessage(message, context) { - const game = games.get(context.room.id); + const game = games.get(context.room?.id); if (!game || context.user?.id === config.user?.id) { return; diff --git a/src/irc.js b/src/irc.js index 1b1d4af..b6d0fd9 100644 --- a/src/irc.js +++ b/src/irc.js @@ -1,18 +1,18 @@ 'use strict'; const config = require('config'); -const fs = require('fs').promises; const irc = require('irc-upd'); const logger = require('simple-node-logger').createSimpleLogger(); const { argv } = require('yargs'); // const timers = require('timers/promises'); -const style = require('./utils/style'); +const { + onMessage, + getGames, +} = require('./play'); logger.setLevel(argv.level || 'info'); -const points = {}; - const client = new irc.Client(config.server, config.user.nick, { userName: config.user.username, realName: config.user.realName, @@ -21,159 +21,16 @@ const client = new irc.Client(config.server, config.user.nick, { secure: true, }); -async function setPoints(defaultKey, user, value, { mode = 'add', key } = {}) { - const gameKey = key || defaultKey; - - if (!user) { - logger.warn(`Failed to set ${gameKey} points for missing user`); - return; - } - - const userKey = `${user.id}:${user.username}`; - - if (!points[gameKey]) { - points[gameKey] = {}; - } - - if (mode === 'add') { - points[gameKey][userKey] = (points[gameKey][userKey] || 0) + value; - } - - if (mode === 'set') { - points[gameKey][userKey] = value; - } - - await fs.writeFile(`./points-${config.user.nick}.json`, JSON.stringify(points, null, 4)); -} - -function getPoints(game, rawUsername, { user, room, command }) { - const username = rawUsername?.replace(new RegExp(`^${config.usernamePrefix}`), ''); - const gamePoints = points[command] || points[game.key]; - - const userPoints = username - ? Object.entries(gamePoints || {}).find(([identifier]) => identifier.split(':')[1] === username)?.[1] - : gamePoints?.[`${user?.id}:${user?.username}`]; - - game.sendMessage(`${username ? `${style.bold(username)} has` : 'You have'} scored ${style.bold(userPoints || 0)} points in ${game.name}, ${config.usernamePrefix}${user.username}`, room.id); -} - -function getLeaderboard(game, { user, room, command }) { - const leaderboard = points[command] || points[game.key]; - - if (!leaderboard || Object.keys(leaderboard).length === 0) { - game.sendMessage(`No points scored in ${game.name} yet!`, room.id); - - return; - } - - const curatedLeaderboard = Object.entries(leaderboard) - .sort(([, scoreA], [, scoreB]) => scoreB - scoreA) - .map(([userKey, score]) => { - const username = userKey.split(':')[1]; - return `${style.bold(`${username === user.username ? config.usernamePrefix : ''}${username}`)} at ${style.bold(score)} points`; - }) - .slice(0, 10) - .join(', '); - - game.sendMessage(`The top ${Math.min(Object.keys(leaderboard).length, 10)} ${style.italic(game.name)} players are: ${curatedLeaderboard}`, room.id); -} - -function getGames(bot) { - const games = config.games.reduce((acc, key) => { - const game = require(`./games/${key.game || key}`); // eslint-disable-line global-require, import/no-dynamic-require - - const sendMessage = (body, roomId, options, recipient) => { - console.log(roomId || recipient, body); - client.say(roomId || recipient, body); - }; - - const setGamePoints = (userId, score, options) => setPoints(key, userId, score, options); - - const curatedGame = { - ...game, - ...(key.game && key), - name: game.name || key, - key, - sendMessage, - setPoints: setGamePoints, - }; - - if (game.onStart) { - game.onStart({ ...curatedGame, bot }); - } - - return { - ...acc, - [key]: curatedGame, - ...game.commands?.reduce((commandAcc, command) => ({ ...commandAcc, [command]: curatedGame }), {}), - }; - }, {}); - - return games; -} - -function onMessage(message, bot, games) { - const body = message.originalBody || message.body; - const [, command, subcommand] = body?.match(new RegExp(`^${config.prefix}(\\w+)(?:\\:(\\w+))?`)) || []; - // const user = bot.users[message.userId] || message.user; - const user = { username: message.from, id: message.from }; - const room = { id: message.to, name: message.to }; - - if (command) { - const args = body.split(/\s+/).slice(1); - const game = games[command]; - - if (['leaderboard', 'lead', 'leader', 'leaders', 'scoreboard', 'best'].includes(subcommand) && games[command]) { - getLeaderboard(games[command], { user, room, command }); - return; - } - - if (['points', 'score'].includes(subcommand) && games[command]) { - getPoints(games[command], args[0], { user, room, command }); - return; - } - - if (game && game.onCommand) { - if (user) { - user.points = points[game.key]?.[`${user.id}:${user.username}`] || 0; - } - - game.onCommand(args, { - ...game, - command, - subcommand, - bot, - message, - user, - room, - points: points[game.key] || {}, - logger, - }); - } - } - - Object.values(games).forEach((game) => game.onMessage?.(message, { - ...game, - bot, - message, - user: user && { - ...user, - points: points[game.key]?.[`${user.id}:${user.username}`] || 0, - }, - room, - logger, - })); -} - async function init() { const bot = { + client, rooms: config.channels.map((channel) => ({ id: channel, name: channel, })), }; - const games = getGames(bot); + const games = await getGames(bot, config.user.nick); client.addListener('registered', () => { logger.info('Connected!'); @@ -195,12 +52,13 @@ async function init() { from, to, body, + type: 'message', }, bot, games)); client.addListener('error', (error) => { - console.error(error); + logger.error(error); }); }); } -init(); +module.exports = init; diff --git a/src/play.js b/src/play.js new file mode 100644 index 0000000..9d231b3 --- /dev/null +++ b/src/play.js @@ -0,0 +1,225 @@ +'use strict'; + +const config = require('config'); +const fs = require('fs').promises; +const logger = require('simple-node-logger').createSimpleLogger(); +const { argv } = require('yargs'); +// const timers = require('timers/promises'); + +const style = require('./utils/style'); + +logger.setLevel(argv.level || 'info'); + +const points = {}; + +async function initPoints(identifier) { + try { + const pointsFile = await fs.readFile(`./points-${identifier}.json`, 'utf-8'); + + Object.assign(points, JSON.parse(pointsFile)); + } catch (error) { + if (error.code === 'ENOENT') { + logger.info('Creating new points file'); + + await fs.writeFile(`./points-${identifier}.json`, '{}'); + initPoints(); + } + } +} + +async function setPoints(identifier, defaultKey, user, value, { mode = 'add', key } = {}) { + const gameKey = key || defaultKey; + + if (!user) { + logger.warn(`Failed to set ${gameKey} points for missing user`); + return; + } + + const userKey = `${user.id}:${user.username}`; + + if (!points[gameKey]) { + points[gameKey] = {}; + } + + if (mode === 'add') { + points[gameKey][userKey] = (points[gameKey][userKey] || 0) + value; + } + + if (mode === 'set') { + points[gameKey][userKey] = value; + } + + await fs.writeFile(`./points-${identifier}.json`, JSON.stringify(points, null, 4)); +} + +function getPoints(game, rawUsername, { user, room, command }) { + const username = rawUsername?.replace(new RegExp(`^${config.usernamePrefix}`), ''); + const gamePoints = points[command] || points[game.key]; + + const userPoints = username + ? Object.entries(gamePoints || {}).find(([identifier]) => identifier.split(':')[1] === username)?.[1] + : gamePoints?.[`${user?.id}:${user?.username}`]; + + game.sendMessage(`${username ? `${style.bold(username)} has` : 'You have'} scored ${style.bold(userPoints || 0)} points in ${game.name}, ${config.usernamePrefix}${user.username}`, room.id); +} + +function getLeaderboard(game, { user, room, command }) { + const leaderboard = points[command] || points[game.key]; + + if (!leaderboard || Object.keys(leaderboard).length === 0) { + game.sendMessage(`No points scored in ${game.name} yet!`, room.id); + + return; + } + + const curatedLeaderboard = Object.entries(leaderboard) + .sort(([, scoreA], [, scoreB]) => scoreB - scoreA) + .map(([userKey, score]) => { + const username = userKey.split(':')[1]; + return `${style.bold(`${username === user.username ? config.usernamePrefix : ''}${username}`)} at ${style.bold(score)} points`; + }) + .slice(0, 10) + .join(', '); + + game.sendMessage(`The top ${Math.min(Object.keys(leaderboard).length, 10)} ${style.italic(game.name)} players are: ${curatedLeaderboard}`, room.id); +} + +async function getGames(bot, identifier) { + await initPoints(identifier); + + const games = config.games.reduce((acc, key) => { + const game = require(`./games/${key.game || key}`); // eslint-disable-line global-require, import/no-dynamic-require + + const sendMessage = (body, roomId, options, recipient) => { + const curatedBody = options?.label === false || config.labels === false ? body : `${style.grey(`[${game.name || key}]`)} ${body}`; + + if (config.platform === 'irc') { + bot.client.say(roomId || recipient, curatedBody); + } + + if (config.platform === 'schat') { + bot.socket.transmit('message', { + roomId, + recipient, + type: recipient && options.type !== 'message' ? 'whisper' : 'message', + body: curatedBody, + style: config.style, + }); + } + }; + + const setGamePoints = (userId, score, options) => setPoints(identifier, key, userId, score, options); + + const curatedGame = { + ...game, + ...(key.game && key), + name: game.name || key, + key, + sendMessage, + setPoints: setGamePoints, + }; + + if (game.onStart) { + game.onStart({ ...curatedGame, bot }); + } + + return { + ...acc, + [key]: curatedGame, + ...game.commands?.reduce((commandAcc, command) => ({ ...commandAcc, [command]: curatedGame }), {}), + }; + }, {}); + + return games; +} + +function getMessageUser(message, bot) { + if (config.platform === 'irc') { + return { + username: message.from, + id: message.from, + }; + } + + if (config.platform === 'schat') { + return bot.users[message.userId] || message.user; + } + + return null; +} + +function getMessageRoom(message, bot) { + if (config.platform === 'irc') { + return { + id: message.to, + name: message.to, + }; + } + + if (config.platform === 'schat') { + return bot.rooms[message.roomId]; + } + + return null; +} + +function onMessage(message, bot, games) { + const body = message.originalBody || message.body; + const [, command, subcommand] = body?.match(new RegExp(`^${config.prefix}(\\w+)(?:\\:(\\w+))?`)) || []; + const user = getMessageUser(message, bot); + const room = getMessageRoom(message, bot); + + if (command) { + const args = body.split(/\s+/).slice(1); + const game = games[command]; + + if (['leaderboard', 'lead', 'leader', 'leaders', 'scoreboard', 'best'].includes(subcommand) && games[command]) { + getLeaderboard(games[command], { user, room, command }); + return; + } + + if (['points', 'score'].includes(subcommand) && games[command]) { + getPoints(games[command], args[0], { user, room, command }); + return; + } + + if (game && game.onCommand) { + if (user) { + user.points = points[game.key]?.[`${user.id}:${user.username}`] || 0; + } + + game.onCommand(args, { + ...game, + command, + subcommand, + bot, + message, + user, + room, + points: points[game.key] || {}, + logger, + }); + } + } + + Object.values(games).forEach((game) => game.onMessage?.(message, { + ...game, + bot, + message, + user: user && { + ...user, + points: points[game.key]?.[`${user.id}:${user.username}`] || 0, + }, + room, + logger, + })); +} + +module.exports = { + onMessage, + getGames, + getLeaderboard, + getPoints, + setPoints, + initPoints, +}; diff --git a/src/schat.js b/src/schat.js new file mode 100644 index 0000000..e9ff717 --- /dev/null +++ b/src/schat.js @@ -0,0 +1,181 @@ +'use strict'; + +const config = require('config'); +const { setTimeout: delay } = require('timers/promises'); +const bhttp = require('bhttp'); +const WebSocket = require('ws'); +const logger = require('simple-node-logger').createSimpleLogger(); +const { argv } = require('yargs'); + +const { + onMessage, + getGames, +} = require('./play'); + +const instance = process.env.NODE_APP_INSTANCE || 'main'; + +logger.setLevel(argv.level || 'info'); + +async function auth() { + const httpSession = bhttp.session(); + const username = config.uniqueUsername ? `${config.user.username}-${new Date().getTime().toString().slice(-5)}` : config.user.username; + + const res = await httpSession.post(`${config.api}/session`, { + ...config.user, + username, + }, { + encodeJSON: true, + }); + + if (res.statusCode !== 200) { + throw new Error(`Failed to authenticate: ${res.body.toString()}`); + } + + logger.info(`Authenticated as '${username}' with ${config.api}`); + + return { + user: res.body, + httpSession, + sessionCookie: res.headers['set-cookie'][0], + }; +} + +async function getWsId(httpSession) { + const res = await httpSession.get(`${config.api}/socket`); + + if (res.statusCode !== 200) { + throw new Error(`Failed to retrieve WebSocket ID: ${res.body.toString()}`); + } + + return res.body; +} + +function onConnect(data, bot) { + bot.socket.transmit('joinRooms', { rooms: config.channels }); +} + +function onRooms({ rooms, users }, bot) { + logger.info(`Joined ${rooms.map((room) => room.name).join(', ')}`); + + /* eslint-disable no-param-reassign */ + bot.rooms = rooms.reduce((acc, room) => ({ ...acc, [room.id]: room }), {}); + bot.users = { ...bot.users, ...users }; + /* eslint-enable no-param-reassign */ + + rooms.forEach((room) => { + bot.socket.transmit('message', { + roomId: room.id, + body: `Hi, I am ${config.user.username}, your game host!`, + style: config.style, + }); + }); +} + +/* eslint-disable no-param-reassign */ +function onJoin(data, bot) { + if (bot.rooms[data.roomId] && !bot.rooms[data.roomId].userIds?.includes(data.user.id)) { + bot.users[data.user.id] = data.user; + bot.rooms[data.roomId].userIds.push(data.user.id); + } +} + +function onLeave(data, bot) { + if (bot.rooms[data.roomId]) { + bot.rooms[data.roomId].userIds = bot.rooms[data.roomId].userIds.filter((userId) => userId !== data.userId); + } +} + +const messageHandlers = { + connect: onConnect, + rooms: onRooms, + message: onMessage, + join: onJoin, + leave: onLeave, +}; + +function handleError(error, socket, domain, data) { + logger.error(`${domain} '${JSON.stringify(data)}' triggered error: ${error.message} ${error.stack}`); + + if (data?.roomId) { + socket.transmit('message', { + body: ':zap::robot::zap: Many fragments! Some large, some small.', + type: 'message', + roomId: data.roomId, + }); + } +} + +async function connect(bot, games) { + const socket = { ws: { readyState: 0 } }; + + socket.connect = async () => { + try { + const { user, httpSession, sessionCookie } = await auth(); + const wsCreds = await getWsId(httpSession); + + bot.user = user; + bot.httpSession = httpSession; + + 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', async (msg) => { + const [domain, data] = JSON.parse(msg); + + logger.debug(`Received ${domain}: ${JSON.stringify(data)}`); + + if (messageHandlers[domain]) { + try { + await messageHandlers[domain](data, bot, games); + } catch (error) { + handleError(error, socket, domain, data); + } + } + }); + + 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}`); + } catch (error) { + logger.error(`Failed to connect, retrying in ${config.reconnectDelay} seconds: ${error.message}`); + + await delay(config.reconnectDelay * 1000); + socket.connect(); + } + }; + + socket.transmit = (domain, data) => { + socket.ws.send(JSON.stringify([domain, data])); + }; + + socket.connect(); + + return socket; +} + +async function init() { + const bot = { + rooms: [], + users: [], + }; + + const games = await getGames(bot, instance); + + bot.socket = await connect(bot, games); +} + +module.exports = init; diff --git a/src/utils/style.js b/src/utils/style.js index b3212c7..7fb05bc 100644 --- a/src/utils/style.js +++ b/src/utils/style.js @@ -21,14 +21,18 @@ module.exports = (() => { } if (config.platform === 'schat') { - return { + const methods = { bold: schatBold, italic: schatItalic, - red: bypass, - getter(...args) { - console.log(args); + }; + + const handler = { + get(target, prop) { + return target[prop] || bypass; }, }; + + return new Proxy(methods, handler); } return null;