From 8629cf246b0555ed6642a92bb4a29fb7c336e7d6 Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Tue, 12 May 2026 16:01:14 +0200 Subject: [PATCH] feat(discord-bot): change bot prefix and improve quest results interaction --- apps/discord-bot/src/env.ts | 2 +- apps/discord-bot/src/modes/bot.ts | 14 ++-- apps/discord-bot/src/quests.ts | 113 ++++++++++++++---------------- 3 files changed, 59 insertions(+), 70 deletions(-) diff --git a/apps/discord-bot/src/env.ts b/apps/discord-bot/src/env.ts index 90cb15d..a1656a0 100644 --- a/apps/discord-bot/src/env.ts +++ b/apps/discord-bot/src/env.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { parseEnv, logger } from "@lbf-bot/utils"; +import { parseEnv } from "@lbf-bot/utils"; // TODO: use parseEnv from utils diff --git a/apps/discord-bot/src/modes/bot.ts b/apps/discord-bot/src/modes/bot.ts index 87a2da6..e0b988d 100644 --- a/apps/discord-bot/src/modes/bot.ts +++ b/apps/discord-bot/src/modes/bot.ts @@ -16,14 +16,12 @@ const onReady = async (client: Client) => { setInterval(() => void trackingCron(client), env.WOV_TRACKING_INTERVAL); }; -const onMessage = async (message: OmitPartialGroupDMChannel, client: Client) => { +const onMessage = async (message: OmitPartialGroupDMChannel) => { if (message.author.bot) return; - if (message.content.startsWith(`<@${client.user!.id}>`)) { - const [commandName, ...args] = message.content - .replace(`<@${client.user!.id}>`, "") - .trim() - .split(" "); + const parts = message.content.trim().split(/\s+/); + if (parts[0]?.toLowerCase() === "lbf") { + const [commandName, ...args] = parts.slice(1); const command = commands[commandName]; if (!command) return; @@ -40,6 +38,6 @@ const onMessage = async (message: OmitPartialGroupDMChannel, client: Cl }; export const setupBotMode = (client: Client) => { - client.on("clientReady", (client) => { void onReady(client); }); - client.on("messageCreate", (message) => { void onMessage(message, client); }); + client.on("clientReady", (client) => { void onReady(client); }); + client.on("messageCreate", (message) => { void onMessage(message); }); }; diff --git a/apps/discord-bot/src/quests.ts b/apps/discord-bot/src/quests.ts index e990623..b75e443 100644 --- a/apps/discord-bot/src/quests.ts +++ b/apps/discord-bot/src/quests.ts @@ -1,4 +1,4 @@ -import { ChannelType, EmbedBuilder, type Client, type Message, type MessageCreateOptions } from "discord.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ComponentType, EmbedBuilder, StringSelectMenuBuilder, type Client, type MessageCreateOptions } from "discord.js"; import { createLogger } from "@lbf-bot/utils"; import { env } from "~/env"; import { checkForNewQuest, type QuestResult } from "~/wov"; @@ -6,8 +6,6 @@ import { getAccountBalance, setAccountBalance } from "~/economy"; const logger = createLogger({ prefix: "quests" }); -// --- rewards --- - export const makeResultEmbed = async (result: QuestResult, exclude: Array): Promise => { const imageUrl = result.quest.promoImageUrl; const color = parseInt(result.quest.promoImagePrimaryColor.substring(1), 16); @@ -34,7 +32,7 @@ export const makeResultEmbed = async (result: QuestResult, exclude: Array !exclude.includes(x.username)) - .filter((_, i) => i < 8) - .map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`) - .join("\n"), + .setDescription(participants + .filter((x) => !exclude.includes(x.username) && x.xp > 0) + .filter((_, i) => i < 8) + .map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`) + .join("\n"), ), ], }; @@ -68,16 +65,42 @@ export const askForGrinders = async (quest: QuestResult, client: Client) => { let exclude: string[] = []; if (env.QUEST_REWARDS) { + const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16); + const top10 = quest.participants - .filter((x) => !env.QUEST_EXCLUDE.includes(x.username)) + .filter((x) => !env.QUEST_EXCLUDE.includes(x.username) && x.xp > 0) .sort((a, b) => b.xp - a.xp) - .slice(0, 10) + .slice(0, 10); + + if (!top10.length) { + logger.error("No eligible participants for grinder selection"); + return; + } + + const selectOptions = top10 + .map((p) => ({ label: `${p.username} (${p.xp}xp)`, value: p.username })); + + const row1 = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId("grinders") + .setPlaceholder("Sélectionne les tricheurs") + .setMinValues(0) + .setMaxValues(selectOptions.length) + .addOptions(selectOptions), + ); + + const row2 = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("validate") + .setLabel("Valider") + .setStyle(ButtonStyle.Primary), + ); + + const top10Text = top10 .map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`) .join("\n"); - const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16); - - await adminChannel.send({ + const msg = await adminChannel.send({ content: `-# ||${env.DISCORD_ADMIN_MENTION}||`, embeds: [ new EmbedBuilder() @@ -85,64 +108,32 @@ export const askForGrinders = async (quest: QuestResult, client: Client) => { .setColor(color), new EmbedBuilder() .setTitle("Top 10 XP") - .setDescription(top10) + .setDescription(top10Text) .setColor(color), new EmbedBuilder() .setTitle("Qui a grind ?") - .setDescription("Merci d'entrer les pseudos des joueurs qui ont grind.\n\nFormat:```@LBF onion,Yuno,...```\n**Attention les majuscules comptent**\nPour entrer la liste des joueurs, il faut __mentionner le bot__, si personne n'a grind, `@LBF tg`") + .setDescription("Sélectionne les joueurs qui ont grind (tricheurs en gros), puis clique sur **Valider**.") .setColor(color), ], + components: [row1, row2], }); - const filter = (msg: Message) => - msg.channel.id === adminChannel.id && - !msg.author.bot && - msg.content.startsWith(`<@${client.user!.id}>`); + let grinders: string[] = []; - let confirmed = false; - let answer: string | null = null; - while (!confirmed) { - const collected = await adminChannel.awaitMessages({ filter, max: 1 }); - answer = collected.first()?.content || null; - if (!answer) continue; + let done = false; + while (!done) { + const interaction = await msg.awaitMessageComponent({ filter: (i) => !i.user.bot }); - answer = answer.replace(`<@${client.user!.id}>`, "").trim(); - if (answer.toLowerCase() === "tg") { - answer = ""; - break; - } - - const players = answer.split(",").map((x) => x.trim()).filter(Boolean); - await adminChannel.send({ - content: `Est-ce correct ? (oui/non)`, - embeds: [ - new EmbedBuilder() - .setTitle("Joueurs entrés") - .setDescription(players.length ? players.map((name) => `- ${name}`).join("\n") : "*Aucun joueur entré*") - .setColor(color), - ], - }); - - const confirmFilter = (msg: Message) => - msg.channel.id === adminChannel.id && - !msg.author.bot && - ["oui", "non", "yes", "no"].includes(msg.content.toLowerCase()); - - const confirmCollected = await adminChannel.awaitMessages({ filter: confirmFilter, max: 1 }); - const confirmation = confirmCollected.first()?.content.toLowerCase(); - if (confirmation === "oui" || confirmation === "yes") { - confirmed = true; - await adminChannel.send({ content: "Ok" }); - } else { - await adminChannel.send({ content: "D'accord, veuillez réessayer. Qui a grind ?" }); + if (interaction.componentType === ComponentType.StringSelect) { + grinders = interaction.values; + await interaction.deferUpdate(); + } else if (interaction.componentType === ComponentType.Button && interaction.customId === "validate") { + await interaction.update({ components: [] }); + done = true; } } - if (answer === null) { - return logger.fatal("Answer was 'null', this should be unreachable"); - } - - exclude = answer.split(",").map((x) => x.trim()).filter(Boolean); + exclude = grinders; } const embed = await makeResultEmbed(quest, [...env.QUEST_EXCLUDE, ...exclude]);