import config from "@/config"; import { type Bug, attachments, db } from "@/database"; import { type Client, ContainerBuilder, FileUploadBuilder, LabelBuilder, ModalBuilder, SectionBuilder, type Snowflake, StringSelectMenuBuilder, TextDisplayBuilder, TextInputBuilder, TextInputStyle, ThumbnailBuilder, } from "discord.js"; import { and, eq } from "drizzle-orm"; // -- types ---------------------------------------------------------------------------------------- type ProjectChoice = T extends "name" ? { name: string; value: string } : { label: string; value: string; default: boolean }; // -- constants ------------------------------------------------------------------------------------ export const PROJECT_MAP = config.projects; export const INPUT_IDS = { AFFECTED: "affectedInput", TITLE: "titleInput", DESCRIPTION: "descriptionInput", MEDIA: "fileUploadInput", } as const; export const MODAL_IDS = { REPORT: "bug:report", EDIT: "bug:edit", } as const; export const VALIDATION = { TITLE_MAX_LENGTH: 100, DESCRIPTION_MAX_LENGTH: 1000, THREAD_NAME_MAX_LENGTH: 50, AUTO_ARCHIVE_DURATION: 1440, } as const; // -- local helpres -------------------------------------------------------------------------------- export function getProjectChoices( label: T = "name" as T, defaultProjectKey?: string, ): ProjectChoice[] { return Object.entries(PROJECT_MAP).map(([key, project]) => label === "name" ? { name: project.displayName, value: key } : { label: project.displayName, value: key, default: key === defaultProjectKey, }, ) as ProjectChoice[]; } // -- main ----------------------------------------------------------------------------------------- export async function constructContainer( bug: Bug, userId: Snowflake, ): Promise { const project = PROJECT_MAP[bug.projects[0]]; if (!project) return undefined; const text = { body: new TextDisplayBuilder().setContent( [`### ${bug.title}`, `${bug.description}`].join("\n"), ), footer: { text: new TextDisplayBuilder().setContent( [`-# #${bug.id}`, project.displayName, `Reported by <@${userId}>`].join( " • ", ), ), icon: project.iconURL ? new ThumbnailBuilder().setURL(project.iconURL) : undefined, }, }; const section = new SectionBuilder(); if (text.footer.icon) section.setThumbnailAccessory(text.footer.icon); section.addTextDisplayComponents(text.body); const media = await db .select() .from(attachments) .where( and(eq(attachments.ownerId, bug.id), eq(attachments.ownerType, "bug")), ); const urls = media.map((attachment) => attachment.url); section.addTextDisplayComponents(text.footer.text); const container = new ContainerBuilder().addSectionComponents(section); return container; } export function buildReportModal(bugId?: string, bug?: Bug) { const terminology = config.terminology; const capitalizedTerminology = terminology.charAt(0).toUpperCase() + terminology.slice(1); const projectOptions = bug ? getProjectChoices("label", bug.projects[0]) : getProjectChoices("label"); const selectedProject = bug ? projectOptions.find((opt) => opt.value === bug.projects[0]) : undefined; const gameLabel = new LabelBuilder() .setLabel(capitalizedTerminology) .setDescription(`The ${terminology} affected by this bug`) .setStringSelectMenuComponent( new StringSelectMenuBuilder() .setMaxValues(1) .setMinValues(1) .setCustomId(INPUT_IDS.AFFECTED) .setPlaceholder(selectedProject?.label || `Select the ${terminology}`) .addOptions(projectOptions), ); const titleLabel = new LabelBuilder() .setLabel("Bug Title") .setDescription(bug ? "Update the bug title" : "A short summary of the bug") .setTextInputComponent( (() => { const input = new TextInputBuilder() .setCustomId(INPUT_IDS.TITLE) .setStyle(TextInputStyle.Short) .setRequired(true) .setMaxLength(VALIDATION.TITLE_MAX_LENGTH); if (bug) input.setValue(bug.title); return input; })(), ); const descriptionLabel = new LabelBuilder() .setLabel("Bug Description") .setDescription( bug ? "Update the bug description" : "Describe the bug in detail", ) .setTextInputComponent( (() => { const input = new TextInputBuilder() .setCustomId(INPUT_IDS.DESCRIPTION) .setStyle(TextInputStyle.Paragraph) .setRequired(true) .setMaxLength(VALIDATION.DESCRIPTION_MAX_LENGTH); if (bug) input.setValue(bug.description); return input; })(), ); const modal = new ModalBuilder() .setCustomId(bug ? `${MODAL_IDS.EDIT}:${bugId}` : `${MODAL_IDS.REPORT}:NEW`) .setTitle(bug ? `Edit Bug #${bugId}` : "Report a Bug"); if (bug) { modal.setLabelComponents(gameLabel, titleLabel, descriptionLabel); } else { const mediaLabel = new LabelBuilder() .setLabel("Media") .setDescription( "Provide any screenshots or videos that demonstrate the bug", ) .setFileUploadComponent( new FileUploadBuilder().setCustomId(INPUT_IDS.MEDIA).setRequired(false), ); modal.setLabelComponents( gameLabel, titleLabel, descriptionLabel, mediaLabel, ); } return modal; } export async function updateBugEmbed( client: Client, bug: Bug, messageId: Snowflake, channelId: Snowflake, ) { const channel = await client.channels.fetch(channelId); if (!channel || !channel.isTextBased() || !channel.isSendable()) return; const message = await channel.messages.fetch(messageId); const project = PROJECT_MAP[bug.projects[0]]; if (!project) return; const container = await constructContainer(bug, message.author.id); if (!container) return; await message.edit({ components: [container], flags: ["IsComponentsV2"], }); }