/** * bot.ts * Scans the chat for reactions and updates the leaderboard database. */ import { Client, Collection, Events, GatewayIntentBits, Interaction, MessageReaction, PartialMessageReaction, Partials, SlashCommandBuilder, TextChannel, User } from 'discord.js'; import fs = require('node:fs'); import path = require('node:path'); import fetch from 'node-fetch'; import { JSDOM } from 'jsdom'; import {logError, logInfo, logWarn} from '../logging'; import { db, openDb, reactionEmojis, recordReaction, sync } from './util'; interface CommandClient extends Client { commands?: Collection Promise }> } const client: CommandClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessageReactions], partials: [Partials.Message, Partials.Channel, Partials.Reaction], }); client.commands = new Collection(); 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) { logWarn(`[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.InteractionCreate, async interaction => { if (!interaction.isChatInputCommand()) return; }); client.on(Events.MessageReactionAdd, onMessageReactionChanged); client.on(Events.MessageReactionRemove, onMessageReactionChanged); client.on(Events.InteractionCreate, async interaction => { if (!interaction.isChatInputCommand()) return; const client: CommandClient = interaction.client; const command = client.commands.get(interaction.commandName); if (!command) { logError(`[bot] No command matching ${interaction.commandName} was found.`); return; } try { await command.execute(interaction); } catch (error) { logError(error); if (interaction.replied || interaction.deferred) { await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true }); } else { await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); } } }); // startup (async () => { logInfo("[db] Opening..."); await openDb(); logInfo("[db] Migrating..."); await db.migrate(); logInfo("[db] Ready."); logInfo("[bot] Loading commands..."); const foldersPath = path.join(__dirname, 'commands'); const commandFolders = fs.readdirSync(foldersPath); for (const folder of commandFolders) { const commandsPath = path.join(foldersPath, folder); const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); for (const file of commandFiles) { const filePath = path.join(commandsPath, file); const command = require(filePath); client.commands.set(command.data.name, command); } } logInfo("[bot] Logging in..."); await client.login(process.env.TOKEN); await sync(client.guilds); if (process.env.ENABLE_MOTD) { await scheduleRandomMessage(true); } })();