diff --git a/src/bot.ts b/src/bot.ts index 4a5ef8a..b65e462 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -13,6 +13,7 @@ import { SlashCommandRoleOption, SlashCommandStringOption, SlashCommandSubcommandBuilder, + SlashCommandUserOption, } from "npm:discord.js@14.14.1"; import { RuleSet } from "./rules.ts"; import { channel, log_from, SimpleResult, split_promise, try_ } from "./utils.ts"; @@ -35,9 +36,7 @@ export class EpitlsBot { this.bot.on("interactionCreate", (interaction) => this.handle_interaction(interaction)); this.rest = new REST().setToken(bot_token); this.rules = rules; - this.assoc_channel = channel< - { cri_login: string; discord_user_id: string; callback: (success: SimpleResult) => void } - >(); + this.assoc_channel = channel(); } /** @@ -116,7 +115,7 @@ export class EpitlsBot { private async register_commands() { const assoc_cmd = new SlashCommandBuilder() .setName("associate") - .setDescription("Associates a cri account to your discord account.") + .setDescription("Associes un utilisateur cri avec votre compte discord.") .addStringOption( new SlashCommandStringOption() .setName("cri_email") @@ -124,10 +123,21 @@ export class EpitlsBot { .setRequired(true), ).toJSON(); + const ADMIN_PERMISSIONS = 0x0000000000000008n; + const dissoc_cmd = new SlashCommandBuilder() + .setName("dissociate") + .setDescription("Dissocies un utilisateur cri de votre compte discord.") + .setDefaultMemberPermissions(ADMIN_PERMISSIONS) + .addUserOption( + new SlashCommandUserOption() + .setName("user") + .setRequired(true), + ).toJSON(); + const rule_cmd = new SlashCommandBuilder() .setName("rule") .setDescription("Gestion des règles d'associations groupes cri / rôles.") - .setDefaultMemberPermissions(0x0000000000000008n) + .setDefaultMemberPermissions(ADMIN_PERMISSIONS) .addSubcommand( new SlashCommandSubcommandBuilder() .setName("list") @@ -172,7 +182,7 @@ export class EpitlsBot { for (const guild_id of this.bot.guilds.cache.keys()) { await this.rest.put( Routes.applicationGuildCommands(this.bot.application!.id, guild_id), - { body: [assoc_cmd, rule_cmd] }, + { body: [assoc_cmd, dissoc_cmd, rule_cmd] }, ); } } @@ -180,6 +190,7 @@ export class EpitlsBot { private async handle_interaction(interaction: Interaction) { if (!interaction.isChatInputCommand()) return; if (interaction.commandName === "associate") await this.handle_command_associate(interaction); + if (interaction.commandName === "dissociate") await this.handle_command_dissociate(interaction); if (interaction.commandName === "rule") { const subcommand = interaction.options.getSubcommand(); if (subcommand === "list") await this.handle_command_rule_list(interaction); @@ -198,7 +209,7 @@ export class EpitlsBot { const discord_user_id = interaction.user.id; const { promise, resolver } = split_promise(); - this.assoc_channel.send({ cri_login, discord_user_id, callback: resolver }); + this.assoc_channel.send({ kind: "associate", cri_login, discord_user_id, callback: resolver }); await interaction.reply(message_command_response_assoc_pending(email)); log(`Started verification for discord id '${discord_user_id}' with cri login '${cri_login}'.`); const result = await promise; @@ -206,6 +217,16 @@ export class EpitlsBot { else interaction.editReply(message_command_response_assoc_error(result)); } + private async handle_command_dissociate(interaction: ChatInputCommandInteraction) { + const user = interaction.options.getUser("user"); + if (user === null) throw new Error("Unreachable."); + const discord_user_id = user.id; + const { promise, resolver } = split_promise(); + this.assoc_channel.send({ kind: "dissociate", discord_user_id, callback: resolver }); + await promise; + await interaction.reply(message_command_response_dissoc_success()); + } + private async handle_command_rule_list(interaction: ChatInputCommandInteraction) { const guild_id = interaction.guildId; if (guild_id === null) { @@ -261,6 +282,19 @@ export class EpitlsBot { } } +export type Association = { + kind: "associate"; + cri_login: string; + discord_user_id: string; + callback: (success: SimpleResult) => void; +}; + +export type Dissociation = { + kind: "dissociate"; + discord_user_id: string; + callback: (success: SimpleResult) => void; +}; + function message_command_response_assoc_pending(email: string) { const embed = new EmbedBuilder() .setTitle("`🟡`| Association") @@ -328,6 +362,13 @@ function to_comparable(input: string) { .replace(/\p{Diacritic}/gu, ""); // remotes accents } +function message_command_response_dissoc_success() { + const embed = new EmbedBuilder() + .setTitle("`✅`| Dissociation") + .setDescription("Votre compte Discord a bien été dissocié de votre compte CRI."); + return { embeds: [embed] }; +} + function display_rule(rule: GuildRule) { if (rule.include_historical) return `- \`${rule.group_id}\` [H] → \`@${rule.role_name}\``; else return `- \`${rule.group_id}\` → \`@${rule.role_name}\``; diff --git a/src/main.ts b/src/main.ts index c6ffc86..58ee3e1 100755 --- a/src/main.ts +++ b/src/main.ts @@ -61,8 +61,9 @@ class Service { // for all received associations, trigger the association procedure. (async () => { - for await (const { discord_user_id, cri_login, callback } of this.bot.receive_associations()) { - this.association_procedure(discord_user_id, cri_login).then(callback); + for await (const req of this.bot.receive_associations()) { + if (req.kind === "associate") this.association_procedure(req.discord_user_id, req.cri_login).then(req.callback); + if (req.kind === "dissociate") this.dissociation_procedure(req.discord_user_id).then(req.callback); } })(); @@ -121,6 +122,11 @@ class Service { } return true; } + + async dissociation_procedure(discord_user_id: string): Promise { + await this.state.remove_user(discord_user_id); + return true; + } } // bot.assign_role_to_member("358338548174159873", "871777993922588712", "1202346358867238952"); diff --git a/src/state.ts b/src/state.ts index 3459188..938eac2 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,6 +1,6 @@ import { v1 as uuid } from "https://deno.land/std@0.213.0/uuid/mod.ts"; -export type StoredUser = { discord_user_id: string; cri_login: string }; +export type StoredUser = { discord_user_id: string; cri_login: string; uuid: string }; /** * Wraps the persistent state, containing user associaitons. */ @@ -44,32 +44,46 @@ export class State { /** * Get the association of a user given its discord user id. */ - public async get_user(discord_user_id: string) { + public async get_user_by_discord_id(discord_user_id: string) { const { value: user_uuid } = await this.kv.get(["users_by_discord_id/", discord_user_id]); if (user_uuid === null) return undefined; const { value: stored } = await this.kv.get(["users/", user_uuid as string]); return stored as StoredUser; } + /** + * Get the association of a user given its cri login. + */ + public async get_user_by_cri_login(cri_login: string) { + const { value: user_uuid } = await this.kv.get(["users_by_cri_login/", cri_login]); + if (user_uuid === null) return undefined; + const { value: stored } = await this.kv.get(["users/", user_uuid as string]); + return stored as StoredUser; + } + /** * Appends a new user association. */ public async set_user(discord_user_id: string, cri_login: string) { + const old_discord = await this.get_user_by_cri_login(cri_login); + if (old_discord !== undefined) await this.remove_user(old_discord.discord_user_id); await this.remove_user(discord_user_id); await this.add_user(discord_user_id, cri_login); } private async add_user(discord_user_id: string, cri_login: string) { const user_uuid = uuid.generate() as string; - const stored: StoredUser = { discord_user_id, cri_login }; + const stored: StoredUser = { discord_user_id, cri_login, uuid: user_uuid }; await this.kv.set(["users/", user_uuid], stored); await this.kv.set(["users_by_discord_id/", discord_user_id], user_uuid); + await this.kv.set(["users_by_cri_login/", cri_login], user_uuid); } - private async remove_user(discord_user_id: string) { - const { value: user_uuid } = await this.kv.get(["users_by_discord_id/", discord_user_id]); - if (user_uuid === null) return undefined; - await this.kv.delete(["users/", user_uuid as string]); + public async remove_user(discord_user_id: string) { + const user = await this.get_user_by_discord_id(discord_user_id); + if (user === undefined) return; + await this.kv.delete(["users/", user.uuid]); await this.kv.delete(["users_by_discord_id/", discord_user_id]); + await this.kv.delete(["users_by_cri_login/", user.cri_login]); } }