'use strict'; const config = require('config'); const bhttp = require('bhttp'); const WebSocket = require('ws'); const fs = require('fs').promises; const logger = require('simple-node-logger').createSimpleLogger(); const points = {}; async function auth() { const httpSession = bhttp.session(); const res = await httpSession.post(`${config.api}/session`, config.user, { encodeJSON: true, }); if (res.statusCode !== 200) { throw new Error(`Failed to authenticate: ${res.body.toString()}`); } logger.info(`Authenticated 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 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}`; 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.json', JSON.stringify(points, null, 4)); } function getPoints(game, { user, room }) { const userPoints = points[game.key]?.[`${user.id}:${user.username}`]; game.sendMessage(`You have scored **${userPoints || 0}** points in ${game.name}, @${user.username}`, room.id); } function getLeaderboard(game, { user, room }) { const leaderboard = 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(-10) .join(', '); game.sendMessage(`The top ${Math.min(Object.keys(curatedLeaderboard).length, 10)} ${game.name} players are ${curatedLeaderboard}`, room.id); } function onConnect(data, bot) { bot.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; bot.users = { ...bot.users, ...users }; /* eslint-enable no-param-reassign */ rooms.forEach((room) => { bot.transmit('message', { roomId: room.id, body: `Hi, I am ${config.user.username}, your game host!`, style: config.style, }); }); } function onMessage(message, bot, games) { const [, command, subcommand] = message.body?.match(new RegExp(`^${config.prefix}(\\w+)(?:\\:(\\w+))?`)) || []; const user = bot.users[message.userId]; const room = bot.rooms.find((roomX) => roomX.id === message.roomId); if (['leaderboard', 'lead', 'leader', 'leaders', 'scoreboard', 'best'].includes(subcommand) && games[command]) { getLeaderboard(games[command], { user, room }); return; } if (['points', 'score'].includes(subcommand) && games[command]) { getPoints(games[command], { user, room }); return; } if (command) { const args = message.body.split(/\s+/).slice(1); const game = games[command]; if (game) { if (user) { user.points = points[game.key]?.[`${user.id}:${user.username}`] || 0; } games[command].onCommand(args, { ...game, subcommand, bot, message, user, room, points: points[game.key] || {}, logger, }); } } Object.values(games).forEach((game) => game.onMessage?.(message, { ...game, bot, message, user, room, logger, })); } const messageHandlers = { connect: onConnect, rooms: onRooms, message: onMessage, }; async function initPoints() { try { const pointsFile = await fs.readFile('./points.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.json', '{}'); 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, }; 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', { roomId, body: `[${game.name || key}] ${body}`, style: config.style, }); }; const setGamePoints = (userId, score, mode) => setPoints(key, userId, score, mode); return { ...acc, [key]: { ...game, name: game.name || key, key, sendMessage, setPoints: setGamePoints, }, }; }, {}); ws.on('message', (msg) => { const [domain, data] = JSON.parse(msg); if (messageHandlers[domain]) { messageHandlers[domain](data, bot, games); } }); await initPoints(); } init();