/** * bot.ts * Scans the chat for reactions and updates the leaderboard database. */ import { Client, Events, GatewayIntentBits, MessageReaction, PartialMessageReaction, Partials, TextChannel, User } from 'discord.js'; import fetch from 'node-fetch'; import { JSDOM } from 'jsdom'; import { logError, logInfo } from '../logging'; import { db, openDb, reactionEmojis, recordReaction, sync } from './util'; const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessageReactions], partials: [Partials.Message, Partials.Channel, Partials.Reaction], }); client.once(Events.ClientReady, async () => { logInfo('[bot] Ready.'); for (let i = 0; i < reactionEmojis.length; ++i) logInfo(`[bot] config: reaction_${i + 1} = ${reactionEmojis[i]}`); }); async function onMessageReactionChanged(reaction: MessageReaction | PartialMessageReaction, user: User) { // When a reaction is received, check if the structure is partial if (reaction.partial) { // If the message this reaction belongs to was removed, the fetching might result in an API error which should be handled try { await reaction.fetch(); } catch (error) { logError('[bot] Something went wrong when fetching the reaction:', error); // Return as `reaction.message.author` may be undefined/null return; } } if (reaction.message.partial) { // If the message this reaction belongs to was removed, the fetching might result in an API error which should be handled try { await reaction.message.fetch(); } catch (error) { logError('[bot] Something went wrong when fetching the message:', error); // Return as `reaction.message.author` may be undefined/null return; } } // Now the message has been cached and is fully available logInfo(`[bot] ${reaction.message.author.id}'s message reaction count changed: ${reaction.emoji.name}x${reaction.count}`); await recordReaction( reaction); } async function fetchMotd() { const res = await fetch(process.env.MOTD_HREF); const xml = await res.text(); const parser = new JSDOM(xml); const doc = parser.window.document; return doc.querySelector(process.env.MOTD_QUERY).textContent; } async function scheduleRandomMessage(firstTime = false) { if (!firstTime) { const channel = await client.channels.fetch(process.env.MOTD_CHANNEL); if (!channel) { console.warn(`[bot] Channel ${process.env.MOTD_CHANNEL} not found, disabling MOTD.`); return; } const randomMessage = await fetchMotd(); await channel.send(randomMessage); logInfo(`[bot] Sent MOTD: ${randomMessage}`); } // wait between 2-8 hours const timeoutMins = Math.random() * 360 + 120; const scheduledTime = new Date(); scheduledTime.setMinutes(scheduledTime.getMinutes() + timeoutMins); logInfo(`[bot] Next MOTD: ${scheduledTime.toLocaleTimeString()}`); setTimeout(scheduleRandomMessage, timeoutMins * 60 * 1000); } client.on(Events.MessageReactionAdd, onMessageReactionChanged); client.on(Events.MessageReactionRemove, onMessageReactionChanged); async function startup() { logInfo("[db] Opening..."); await openDb(); logInfo("[db] Migrating..."); await db.migrate(); logInfo("[db] Ready."); logInfo("[bot] Logging in..."); await client.login(process.env.TOKEN); await sync(client.guilds); if (process.env.ENABLE_MOTD) { await scheduleRandomMessage(true); } } startup();