import { CacheType, Client, EmbedBuilder, GatewayIntentBits, Interaction, REST, Routes, SlashCommandBuilder, SlashCommandStringOption, } from "npm:discord.js@14.14.1"; import { channel, log_from, SimpleResult, split_promise } from "./utils.ts"; const log = log_from(import.meta.url); /** * Wraps a discord bot and implements required actions. */ export class EpitlsBot { private bot; private token; private rest; private assoc_channel; public constructor(bot_token: string) { this.token = bot_token; const intents = [GatewayIntentBits.Guilds]; this.bot = new Client({ intents }); this.bot.on("interactionCreate", (interaction) => this.handle_interaction(interaction)); this.rest = new REST().setToken(bot_token); this.assoc_channel = channel< { cri_login: string; discord_user_id: string; callback: (success: SimpleResult) => void } >(); } /** * Connects to discord API server and registers slash commands.\ * Needs to be run after construction as it is an asynchronous operation. */ public async start() { const { promise, resolver } = split_promise(); this.bot.on("ready", () => resolver()); this.bot.login(this.token); await promise; await this.register_commands(); } /** * Assigns a discord role to a discord user. */ public async assign_role(user_id: string, guild_id: string, role_id: string) { const guild = await this.bot.guilds.fetch(guild_id); const member = await guild.members.fetch({ user: user_id }); const role = await guild.roles.fetch(role_id); if (role === null) return console.error("Role", role_id, "not found in guild", guild_id); member.roles.add(role_id); log(`Assigned role '${role.name}' to user '${member.displayName}'.`); } /** * Pull-style API to wait for and receive account association requests. */ public async *receive_associations() { while (true) yield await this.assoc_channel.receive(); } /** * Connected bot name accessor. */ public bot_name() { return this.bot.user?.displayName; } public async get_username(user_id: string) { try { const user = await this.bot.users.fetch(user_id); return user.displayName; } catch (_) { return undefined; } } private async register_commands() { const cmd = new SlashCommandBuilder() .setName("associate") .setDescription("Associates a cri account to your discord account.") .addStringOption( new SlashCommandStringOption() .setName("cri_email") .setDescription("claude.nougaro@epita.fr") .setRequired(true), ).toJSON(); for (const guild_id of this.bot.guilds.cache.keys()) { await this.rest.put( Routes.applicationGuildCommands(this.bot.application!.id, guild_id), { body: [cmd] }, ); } } private async handle_interaction(interaction: Interaction) { if (!interaction.isChatInputCommand()) return; if (interaction.commandName != "associate") return; const email = interaction.options.getString("cri_email")!; if (!email.endsWith("@epita.fr")) { await interaction.reply(message_command_response_error("invalid cri email " + email)); return; } const [cri_login] = email.split("@epita.fr"); const discord_user_id = interaction.user.id; const { promise, resolver } = split_promise(); this.assoc_channel.send({ cri_login, discord_user_id, callback: resolver }); await interaction.reply(message_command_response_sending_email(email)); log(`Started verification for discord id '${discord_user_id}' with cri login '${cri_login}'.`); const result = await promise; if (result === true) interaction.editReply(message_command_response_success()); else interaction.editReply(message_command_response_error(result)); } } function message_command_response_sending_email(email: string) { const embed = new EmbedBuilder() .setTitle("`🟡`| Vérification") .setDescription(` Un lien de vérification a été envoyé par e-mail à \`${email}\`. Le lien expirera dans 5 minutes. **Il pourraît être arrivé dans \`Courrier indésirable\`.** `.trim()); return { embeds: [embed] }; } function message_command_response_error(msg: string) { const embed = new EmbedBuilder() .setTitle("`❌`| Vérification") .setDescription(`Échec de la vérification :\n\`${msg}\``); return { embeds: [embed] }; } function message_command_response_success() { const embed = new EmbedBuilder() .setTitle("`✅`| Vérification") .setDescription("Votre e-mail CRI a bien été associé à ce compte Discord."); return { embeds: [embed] }; }