epitoulouse-bot/src/bot.ts

145 lines
4.4 KiB
TypeScript

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<void>();
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<CacheType>) {
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<SimpleResult>();
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] };
}