FemScoreboard/discord/bot.ts

164 lines
5.5 KiB
TypeScript
Raw Normal View History

2023-10-07 22:46:02 -07:00
/**
* bot.ts
* Scans the chat for reactions and updates the leaderboard database.
*/
2023-10-08 18:59:04 -07:00
import {
Client,
2024-02-06 16:45:55 -08:00
Collection,
2023-10-08 18:59:04 -07:00
Events,
GatewayIntentBits,
Interaction,
2023-10-08 18:59:04 -07:00
MessageReaction,
PartialMessageReaction,
Partials, SlashCommandBuilder,
2023-10-08 18:59:04 -07:00
TextChannel,
User
} from 'discord.js';
import fs = require('node:fs');
import path = require('node:path');
2024-02-06 15:23:26 -08:00
import fetch from 'node-fetch';
import { JSDOM } from 'jsdom';
import {logError, logInfo, logWarn} from '../logging';
2023-10-08 19:10:47 -07:00
import {
db,
openDb,
reactionEmojis,
recordReaction,
sync
} from './util';
2023-10-07 22:46:02 -07:00
interface CommandClient extends Client {
commands?: Collection<string, { data: SlashCommandBuilder, execute: (interaction: Interaction) => Promise<void> }>
}
const client: CommandClient = new Client({
2023-10-07 22:46:02 -07:00
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessageReactions],
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
});
client.commands = new Collection();
2024-02-06 16:45:55 -08:00
2023-10-07 22:46:02 -07:00
client.once(Events.ClientReady, async () => {
2023-10-08 19:10:47 -07:00
logInfo('[bot] Ready.');
2023-10-07 22:46:02 -07:00
for (let i = 0; i < reactionEmojis.length; ++i)
2023-10-08 19:10:47 -07:00
logInfo(`[bot] config: reaction_${i + 1} = ${reactionEmojis[i]}`);
2023-10-07 22:46:02 -07:00
});
2023-10-08 19:10:47 -07:00
2023-10-07 22:46:02 -07:00
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) {
2023-10-08 19:10:47 -07:00
logError('[bot] Something went wrong when fetching the reaction:', error);
2023-10-07 22:46:02 -07:00
// 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) {
2023-10-08 19:10:47 -07:00
logError('[bot] Something went wrong when fetching the message:', error);
2023-10-07 22:46:02 -07:00
// Return as `reaction.message.author` may be undefined/null
return;
}
}
// Now the message has been cached and is fully available
2023-10-08 19:10:47 -07:00
logInfo(`[bot] ${reaction.message.author.id}'s message reaction count changed: ${reaction.emoji.name}x${reaction.count}`);
2023-10-07 22:46:02 -07:00
await recordReaction(<MessageReaction> reaction);
}
2023-10-08 18:59:04 -07:00
async function fetchMotd()
{
const res = await fetch(process.env.MOTD_HREF);
const xml = await res.text();
2024-02-06 15:23:26 -08:00
const parser = new JSDOM(xml);
const doc = parser.window.document;
2023-10-08 18:59:04 -07:00
return doc.querySelector(process.env.MOTD_QUERY).textContent;
}
async function scheduleRandomMessage(firstTime = false)
{
if (!firstTime) {
const channel = <TextChannel> await client.channels.fetch(process.env.MOTD_CHANNEL);
if (!channel) {
logWarn(`[bot] Channel ${process.env.MOTD_CHANNEL} not found, disabling MOTD.`);
return;
}
2023-10-08 18:59:04 -07:00
const randomMessage = await fetchMotd();
await channel.send(randomMessage);
2023-10-08 19:10:47 -07:00
logInfo(`[bot] Sent MOTD: ${randomMessage}`);
2023-10-08 18:59:04 -07:00
}
// wait between 2-8 hours
const timeoutMins = Math.random() * 360 + 120;
const scheduledTime = new Date();
scheduledTime.setMinutes(scheduledTime.getMinutes() + timeoutMins);
2023-10-08 19:15:00 -07:00
logInfo(`[bot] Next MOTD: ${scheduledTime.toLocaleTimeString()}`);
2023-10-08 18:59:04 -07:00
setTimeout(scheduleRandomMessage, timeoutMins * 60 * 1000);
}
2024-02-06 16:45:55 -08:00
client.on(Events.InteractionCreate, async interaction => {
if (!interaction.isChatInputCommand()) return;
});
2023-10-07 22:46:02 -07:00
client.on(Events.MessageReactionAdd, onMessageReactionChanged);
client.on(Events.MessageReactionRemove, onMessageReactionChanged);
client.on(Events.InteractionCreate, async interaction => {
if (!interaction.isChatInputCommand()) return;
2023-10-07 22:46:02 -07:00
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 () => {
2023-10-08 19:10:47 -07:00
logInfo("[db] Opening...");
2023-10-07 22:46:02 -07:00
await openDb();
2023-10-08 19:10:47 -07:00
logInfo("[db] Migrating...");
2023-10-07 22:46:02 -07:00
await db.migrate();
2023-10-08 19:10:47 -07:00
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);
}
}
2023-10-08 19:10:47 -07:00
logInfo("[bot] Logging in...");
2023-10-07 22:46:02 -07:00
await client.login(process.env.TOKEN);
await sync(client.guilds);
2023-10-08 18:59:04 -07:00
if (process.env.ENABLE_MOTD) {
await scheduleRandomMessage(true);
}
})();