all repos — stealth-developers @ d816a1cab318fbc6d5205897c2902c49574710fa

feat: add a /codes command
willow hai@wlo.moe
Mon, 07 Jul 2025 23:31:23 +0100
commit

d816a1cab318fbc6d5205897c2902c49574710fa

parent

2b8e1cb8074eaf0e6088ab69e8de711b416bb7ef

M src/config.tssrc/config.ts

@@ -16,6 +16,16 @@ z.object({

name: z.string(), displayName: z.string(), iconURL: z.string().optional(), + codes: z + .array( + z.object({ + code: z.string(), + expired: z.boolean().optional(), + expiredAt: z.string().datetime().optional(), + addedAt: z.string().datetime().optional(), + }), + ) + .optional(), }), );

@@ -35,6 +45,7 @@ bloxlink: bloxlinkSchema.optional(),

roblox: robloxSchema.optional(), trelloBoardId: z.string().optional(), developerId: z.string(), + terminology: z.string().default("project"), }); function validateConfig() {
M src/interactions/commands/bug/button.tssrc/interactions/commands/bug/button.ts

@@ -1,3 +1,4 @@

+import config from "@/config.ts"; import { ActionRowBuilder, type ChatInputCommandInteraction,

@@ -21,18 +22,19 @@ );

const selectMenu = new StringSelectMenuBuilder() .setCustomId("bug:project-select") - .setPlaceholder("select a project to report a bug") + .setPlaceholder(`select a ${config.data.terminology} to report a bug`) .addOptions(...options); const selectRow = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu); await interaction.reply({ - content: `📝 <@${tgtUser.id}>, use the dropdown below to select the project + content: + `📝 <@${tgtUser.id}>, use the dropdown below to select the ${config.data.terminology} you want to report a bug for. in the future, you can also use the </bug report:${interaction.commandId}> command to report bugs.` - .replace(/\s+/g, " ") - .trim(), + .replace(/\s+/g, " ") + .trim(), components: [selectRow], }); }
M src/interactions/commands/bug/index.tssrc/interactions/commands/bug/index.ts

@@ -1,3 +1,4 @@

+import config from "@/config.ts"; import { type ApplicationCommandData, SlashCommandBuilder } from "discord.js"; import type { ICommand } from "../../../types.ts"; import { buttonCommand } from "./button.ts";

@@ -12,8 +13,8 @@ .setName("report")

.setDescription("report a new bug") .addStringOption((option) => option - .setName("project") - .setDescription("which project this bug affects") + .setName(config.data.terminology) + .setDescription(`which ${config.data.terminology} this bug affects`) .setRequired(true) .addChoices(...reportCommand.getProjectChoices()), ),
M src/interactions/commands/bug/report.tssrc/interactions/commands/bug/report.ts

@@ -40,7 +40,7 @@ export async function execute(

_client: Client, interaction: ChatInputCommandInteraction, ) { - const project = interaction.options.getString("project", true); + const project = interaction.options.getString(config.data.terminology, true); const modal = new ModalBuilder() .setCustomId(`bug:${project}`)

@@ -281,7 +281,7 @@ );

const selectMenu = new StringSelectMenuBuilder() .setCustomId("bug:project-select") - .setPlaceholder("select a project") + .setPlaceholder(`select a ${config.data.terminology}`) .addOptions(...options); const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(

@@ -289,7 +289,7 @@ selectMenu,

); await interaction.reply({ - content: "select a project to report a bug for:", + content: `select a ${config.data.terminology} to report a bug for:`, components: [row], flags: ["Ephemeral"], });
M src/interactions/commands/bug/shared.tssrc/interactions/commands/bug/shared.ts

@@ -14,7 +14,7 @@ export const PROJECT_MAP = config.data.projects;

export function getProjectName(value: string): string { const project = PROJECT_MAP[value as keyof typeof PROJECT_MAP]; - if (!project) return "unknown project"; + if (!project) return `unknown ${config.data.terminology}`; return project.name; }
A src/interactions/commands/codes.ts

@@ -0,0 +1,172 @@

+import config from "@/config"; +import { + ActionRowBuilder, + ButtonBuilder, + type ButtonInteraction, + ButtonStyle, + type ChatInputCommandInteraction, + type Client, + ContainerBuilder, + SlashCommandBuilder, + TextDisplayBuilder, +} from "discord.js"; + +import { getProjectChoices } from "@/utils/choices"; +import { formatTime } from "@/utils/time"; + +const commandData = new SlashCommandBuilder() + .setName("codes") + .setDescription(`get the codes for the associated ${config.data.terminology}`) + .addStringOption((option) => + option + .setName(config.data.terminology) + .setDescription("the project to get the codes for") + .setRequired(true) + .addChoices(getProjectChoices()), + ) + .addUserOption((option) => + option + .setName("user") + .setDescription("the user to send the codes to") + .setRequired(false), + ); + +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { + const projectKey = interaction.options.getString( + config.data.terminology, + true, + ); + const project = config.data.projects[projectKey]; + + if (!project) { + return interaction.reply({ + content: `couldn't find data for ${config.data.terminology} "${projectKey}"`, + flags: ["Ephemeral"], + }); + } + + const codes = project.codes || []; + const container = new ContainerBuilder(); + const user = interaction.options.getUser("user"); + const p2 = user ? `• <@${user.id}>` : ""; + + if (codes.length === 0) { + const textComponents = [ + new TextDisplayBuilder().setContent( + `# no codes for ${project.displayName}`, + ), + new TextDisplayBuilder().setContent( + `there aren't any known codes for this ${config.data.terminology} at the moment`, + ), + ]; + + const footerComponent = new TextDisplayBuilder().setContent( + `please tell <@${config.data.developerId}> if this is incorrect ${p2}`, + ); + + container.addTextDisplayComponents(textComponents); + container.addTextDisplayComponents(footerComponent); + return interaction.reply({ + components: [container], + flags: ["IsComponentsV2"], + ...(user + ? { allowedMentions: { users: [user?.id] } } + : { allowedMentions: { users: [] } }), + }); + } + + { + const codeButtons = codes.map((code) => + new ButtonBuilder() + .setCustomId(`codes:${projectKey}:${code.code}`) + .setLabel(code.code) + .setStyle(code.expired ? ButtonStyle.Danger : ButtonStyle.Success), + ); + + const buttonRows: ActionRowBuilder<ButtonBuilder>[] = []; + for (let i = 0; i < codeButtons.length; i += 4) { + const row = new ActionRowBuilder<ButtonBuilder>().addComponents( + codeButtons.slice(i, i + 4), + ); + buttonRows.push(row); + } + + const textComponent = new TextDisplayBuilder().setContent( + `# codes for ${project.displayName}`, + ); + + const footerComponent = new TextDisplayBuilder().setContent( + `please tell <@${config.data.developerId}> if any any missing or expired codes ${p2}`, + ); + + container.addTextDisplayComponents(textComponent); + container.addActionRowComponents(buttonRows); + container.addTextDisplayComponents(footerComponent); + } + + return interaction.reply({ + components: [container], + flags: ["IsComponentsV2"], + ...(user + ? { allowedMentions: { users: [user?.id] } } + : { allowedMentions: { users: [] } }), + }); +} + +async function buttonExecute(client: Client, interaction: ButtonInteraction) { + const [, project, code] = interaction.customId.split(":"); + if (!project || !code) { + return interaction.reply({ + content: `invalid code button interaction, missing either ${config.data.terminology} or code`, + flags: ["Ephemeral"], + }); + } + + const projectData = config.data.projects[project]; + if (!projectData) { + return interaction.reply({ + content: `couldn't find data for ${config.data.terminology} "${project}"`, + flags: ["Ephemeral"], + }); + } + + const codeData = projectData.codes?.find((c) => c.code === code); + if (!codeData) { + return interaction.reply({ + content: `couldn't find code "${code}" for ${config.data.terminology} "${project}"`, + flags: ["Ephemeral"], + }); + } + + return interaction.reply({ + content: `\`${codeData.code}\``, + flags: ["Ephemeral"], + components: [ + new ActionRowBuilder<ButtonBuilder>().addComponents( + new ButtonBuilder() + .setCustomId("disabled:0") + .setLabel(codeData.expired ? "expired" : "active") + .setStyle(codeData.expired ? ButtonStyle.Danger : ButtonStyle.Success) + .setDisabled(true), + new ButtonBuilder() + .setCustomId("disabled:1") + .setLabel( + codeData.addedAt + ? `added at ${formatTime(codeData.addedAt)}` + : "added at unknown time", + ) + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + ), + ], + }); +} + +export default { + data: commandData, + execute, + buttonExecute, +};
A src/utils/choices.ts

@@ -0,0 +1,8 @@

+import config from "@/config"; + +export function getProjectChoices() { + return Object.entries(config.data.projects).map(([key, project]) => ({ + name: project.displayName, + value: key, + })); +}
A src/utils/time.ts

@@ -0,0 +1,11 @@

+export function formatTime(date: Date | string | number): string { + const d = new Date(date); + const options: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "short", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }; + return d.toLocaleString("en-GB", options); +}