From cbfed03f66a27d8d2c0e48c7966dc5ab6fd96be7 Mon Sep 17 00:00:00 2001 From: Matthieu Jolimaitre Date: Tue, 13 Aug 2024 19:49:25 +0200 Subject: [PATCH] add export command --- src/bot.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++-- src/main.ts | 14 ++++++++++-- src/utils.ts | 8 ++++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index a33260b..423104b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,4 +1,5 @@ import { + AttachmentBuilder, CacheType, ChatInputCommandInteraction, Client, @@ -17,6 +18,7 @@ import { } from "npm:discord.js@14.14.1"; import { RuleSet } from "./rules.ts"; import { channel, log_from, SimpleResult, split_promise, try_ } from "./utils.ts"; +import { StoredUser } from "./state.ts"; const log = log_from(import.meta.url); /** @@ -36,7 +38,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(); + this.assoc_channel = channel(); } /** @@ -135,6 +137,12 @@ export class EpitlsBot { .setRequired(true), ).toJSON(); + const export_cmd = new SlashCommandBuilder() + .setName("export") + .setDescription("Exporte les associations des login cri vers les compte discord.") + .setDefaultMemberPermissions(ADMIN_PERMISSIONS) + .toJSON(); + const rule_cmd = new SlashCommandBuilder() .setName("rule") .setDescription("Gestion des règles d'associations groupes cri / rôles.") @@ -183,7 +191,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, dissoc_cmd, rule_cmd] }, + { body: [assoc_cmd, dissoc_cmd, rule_cmd, export_cmd] }, ); } } @@ -192,6 +200,7 @@ export class EpitlsBot { 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 === "export") await this.handle_command_export(interaction); if (interaction.commandName === "rule") { const subcommand = interaction.options.getSubcommand(); if (subcommand === "list") await this.handle_command_rule_list(interaction); @@ -228,6 +237,17 @@ export class EpitlsBot { await interaction.reply(message_command_response_dissoc_success()); } + private async handle_command_export(interaction: ChatInputCommandInteraction) { + const { promise, resolver } = split_promise>(); + this.assoc_channel.send({ kind: "export", callback: resolver }); + const result = await promise; + if (Array.isArray(result)) { + const csv = await this.users_to_csv(result); + const response = message_command_response_export_success(result.length, csv); + await interaction.reply(response); + } else await interaction.reply(message_command_response_export_error(result)); + } + private async handle_command_rule_list(interaction: ChatInputCommandInteraction) { const guild_id = interaction.guildId; if (guild_id === null) { @@ -281,6 +301,15 @@ export class EpitlsBot { } await interaction.reply(message_command_response_rule_remove_success(rules)); } + + private async users_to_csv(users: StoredUser[]) { + const promises = users.map(async (user) => ({ ...user, username: await this.get_username(user.discord_user_id) })); + const results = await Promise.all(promises); + const values = results.map(( + { discord_user_id, cri_login, username }, + ) => [discord_user_id, cri_login, username ?? ""]); + return values.map((line) => line.map(csv_escape).join(",")).join("\n"); + } } export type Association = { @@ -296,6 +325,11 @@ export type Dissociation = { callback: (success: SimpleResult) => void; }; +export type Export = { + kind: "export"; + callback: (value: SimpleResult) => void; +}; + function message_command_response_assoc_pending(email: string) { const embed = new EmbedBuilder() .setTitle("`🟡`| Association") @@ -338,6 +372,21 @@ ${rules.map(display_rule).join("\n")} return { embeds: [embed] }; } +function message_command_response_export_success(count: number, csv: string) { + const embed = new EmbedBuilder() + .setTitle("`✅`| Export") + .setDescription(`\`${count}\` associations exportées.`); + const attachment = new AttachmentBuilder(csv).setName(`export-${Date.now()}.csv`); + return { embeds: [embed], files: [attachment] }; +} + +function message_command_response_export_error(msg: string) { + const embed = new EmbedBuilder() + .setTitle("`❌`| Export") + .setDescription(`Échec :\n\`${msg}\``); + return { embeds: [embed] }; +} + function message_command_response_rule_add_success(rule: GuildRule) { const embed = new EmbedBuilder() .setTitle("`✅`| Règle ajouté") @@ -374,3 +423,11 @@ 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}\``; } + +function csv_escape(text: string) { + const escaped = text + .replaceAll("\\", "\\\\") + .replaceAll("\n", "\\n") + .replaceAll('"', '\\"'); + return `"${escaped}"`; +} diff --git a/src/main.ts b/src/main.ts index a362256..a07fa28 100755 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ #!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --allow-env --unstable-kv -import { log_from, read_conf, root_path, SimpleResult } from "./utils.ts"; -import { State } from "./state.ts"; +import { collect, log_from, read_conf, root_path, SimpleResult } from "./utils.ts"; +import { State, StoredUser } from "./state.ts"; import { RuleSet, TargetRole } from "./rules.ts"; import { EpitlsBot } from "./bot.ts"; import { CriApi } from "./cri.ts"; @@ -64,6 +64,7 @@ class Service { 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); + if (req.kind === "export") this.export_procedure().then(req.callback); } })(); @@ -133,6 +134,15 @@ class Service { } return true; } + + async export_procedure(): Promise> { + try { + return await collect(this.state.users()); + } catch (error) { + console.error(error); + return `${error}`; + } + } } // bot.assign_role_to_member("358338548174159873", "871777993922588712", "1202346358867238952"); diff --git a/src/utils.ts b/src/utils.ts index 99086f8..b8f9a76 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -105,7 +105,7 @@ export function placeholder_conf() { return JSON.stringify(result, null, 4); } -export type SimpleResult = true | string; +export type SimpleResult = T | string; export async function wait(ms: number) { const { promise, resolver } = split_promise(); @@ -131,3 +131,9 @@ export async function try_(operation: () => Promise | T, recovery: () => P return undefined; } } + +export async function collect(generator: AsyncIterable) { + const result = [] as T[]; + for await (const item of generator) result.push(item); + return result; +}