all repos — stealth-developers @ daca13a6ef949c27e8b29443201b8a5f86a34942

bot: bug reports
vi did:web:vt3e.cat
Mon, 29 Jun 2026 18:21:26 +0100
commit

daca13a6ef949c27e8b29443201b8a5f86a34942

parent

11ae44fd13ccdbcc13f9599ffeb859e5016664ee

A apps/bot/src/feats/bugs/commands.ts

@@ -0,0 +1,15 @@

+import { kitten } from "@/client"; + +import { buildReportModal } from "./shared"; + +export const bugCommand = kitten.command("bug", { + description: "Make a bug report", +}); + +bugCommand.subcommand("report", { + description: "Report a new bug", + async run(interaction) { + const modal = buildReportModal(); + await interaction.showModal(modal); + }, +});
A apps/bot/src/feats/bugs/index.ts

@@ -0,0 +1,2 @@

+export { bugCommand } from "./commands"; +export { bugModal } from "./modals";
A apps/bot/src/feats/bugs/modals.ts

@@ -0,0 +1,117 @@

+import db, { eq, sql } from "@stealth-developers/db"; +import { bugs } from "@stealth-developers/db/src/schemas/bugs"; +import { AttachmentBuilder } from "discord.js"; + +import { errorMessage, getTextChannel } from "@/lib"; +import { useGetData } from "@/middleware"; + +import { constructBugContainer } from "./shared"; + +export const bugModal = useGetData.modal("bug-report", { + run: async (interaction, _, { actor, guild }) => { + if (!actor || !guild) + return errorMessage(interaction, "You must be in a server to report a bug."); + + const { fields: _fields } = interaction; + const fields = { + affected: _fields.getStringSelectValues("affected"), + title: _fields.getTextInputValue("title"), + description: _fields.getTextInputValue("description"), + media: _fields.getUploadedFiles("media", false), + }; + + await interaction.deferReply({ flags: ["Ephemeral"] }); + + const bugRes = await db.transaction(async (tx) => { + const [res] = await tx + .select({ count: sql<number>`COUNT(*)`.mapWith(Number) }) + .from(bugs) + .where(eq(bugs.guildId, guild.id)); + + if (!res) return null; + const { count } = res; + + const bugNumber = count + 1; + + return await tx + .insert(bugs) + .values({ + guildId: guild.id, + actorId: actor.id, + + bugNumber: bugNumber, + status: "open", + + title: fields.title, + description: fields.description, + affectedProjects: [...fields.affected], + + sent: false, + messageId: null, + threadId: null, + + createdAt: new Date(), + updatedAt: new Date(), + closedAt: null, + }) + .returning(); + }); + if (!bugRes) return errorMessage(interaction, "Failed to create bug report."); + + const bug = bugRes[0]; + if (!bug) return errorMessage(interaction, "Failed to create bug report."); + + let attachment: AttachmentBuilder | undefined; + if (fields.media && fields.media.size > 0) { + const [_, _attachment] = Array.from(fields.media)[0]!; + try { + const fileUrl = _attachment.url; + const fileName = _attachment.name; + if (fileUrl) { + const res = await fetch(fileUrl); + if (res.ok) { + const arrayBuffer = await res.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + attachment = new AttachmentBuilder(buffer, { name: fileName }); + } + } + } catch (err) { + console.error("error processing uploaded file:", err); + } + } + + const container = await constructBugContainer(bug, actor, attachment); + if (!container) return; + + const channel = await getTextChannel("1512924775842840767"); + if (!channel) return errorMessage(interaction, "Failed to send bug report."); + + const message = await channel.send({ + components: [container], + flags: ["IsComponentsV2"], + ...(attachment ? { files: [attachment] } : {}), + }); + + function truncateTitle(title: string, maxLength: number): string { + return title.length > maxLength ? `${title.substring(0, maxLength)}...` : title; + } + const paddedNumber = bug.bugNumber.toString().padStart(4, "0"); + const threadName = `#${paddedNumber} ${truncateTitle(fields.title, 150)}`; + const thread = await message.startThread({ + name: threadName, + autoArchiveDuration: 1440, + }); + + await db + .update(bugs) + .set({ threadId: thread.id, messageId: message.id }) + .where(eq(bugs.id, bug.id)); + + thread.send({ + content: "Use this space to discuss the bug, provide additional details, or ask questions.", + }); + interaction.editReply( + `Bug #${paddedNumber} created successfully! Find it here: ${message.url}`, + ); + }, +});
A apps/bot/src/feats/bugs/shared.ts

@@ -0,0 +1,236 @@

+import type { Actor, Bug } from "@stealth-developers/db"; + +import { + ActionRowBuilder, + AttachmentBuilder, + ButtonBuilder, + ButtonStyle, + ContainerBuilder, + FileUploadBuilder, + LabelBuilder, + MediaGalleryBuilder, + MediaGalleryItemBuilder, + ModalBuilder, + SectionBuilder, + StringSelectMenuBuilder, + TextDisplayBuilder, + TextInputBuilder, + TextInputStyle, + ThumbnailBuilder, +} from "discord.js"; + +import { bugModal } from "./modals"; + +export const Projects = { + gw: { + universe: "6583326485", + name: "ground war", + displayName: "Ground War", + iconURL: + "https://tr.rbxcdn.com/180DAY-08f5294f7b48367cccbe389e705a4060/256/256/Image/Webp/noFilter", + }, + wft: { + universe: "21449357", + name: "warfare tycoon", + displayName: "Warfare Tycoon", + iconURL: + "https://tr.rbxcdn.com/180DAY-3fd1cc01fe04259c77a635662f096c9e/256/256/Image/Webp/noFilter", + codes: [ + { + code: "FIRSTCODE", + expired: true, + addedAt: "2022-09-03T12:12:00Z", + }, + { + code: "40KLIKES", + expired: false, + addedAt: "2022-09-29T18:31:00Z", + }, + { + code: "HAPPYHOLIDAYS", + expired: true, + addedAt: "2022-12-24T21:27:00Z", + }, + { + code: "50KLIKES", + expired: false, + addedAt: "2022-12-24T21:34:00Z", + }, + { + code: "BLACKHAWKISHERE", + expired: false, + addedAt: "2022-12-24T21:32:00Z", + }, + { + code: "60KLIKES", + expired: false, + addedAt: "2023-03-09T21:19:00Z", + }, + { + code: "70KLIKES", + expired: false, + addedAt: "2023-07-07T20:43:00Z", + }, + ], + }, + ab: { + universe: "5611522097", + name: "airsoft battles", + displayName: "Airsoft Battles", + iconURL: + "https://tr.rbxcdn.com/180DAY-704ac49e2129f50ca709751588cdd45b/256/256/Image/Webp/noFilter", + }, +} as const; + +type ProjectChoice<T extends "name" | "label" = "name"> = T extends "name" + ? { name: string; value: string } + : { label: string; value: string; default: boolean }; + +export function getProjectChoices<T extends "name" | "label" = "name">( + label: T = "name" as T, + defaultProjectKey?: string, +): ProjectChoice<T>[] { + return Object.entries(Projects).map(([key, project]) => + label === "name" + ? { name: project.displayName, value: key } + : { + label: project.displayName, + value: key, + default: key === defaultProjectKey, + }, + ) as ProjectChoice<T>[]; +} + +export function buildReportModal() { + const terminology = "game"; + const capitalizedTerminology = terminology.charAt(0).toUpperCase() + terminology.slice(1); + + const projectOptions = getProjectChoices("label"); + + const gameLabel = new LabelBuilder() + .setLabel(capitalizedTerminology) + .setDescription(`The ${terminology} affected by this bug`) + .setStringSelectMenuComponent( + new StringSelectMenuBuilder() + .setMaxValues(1) + .setMinValues(1) + .setCustomId("affected") + .setPlaceholder(`Select the ${terminology}`) + .addOptions(projectOptions), + ); + + const titleLabel = new LabelBuilder() + .setLabel("Bug Title") + .setDescription("A short summary of the bug") + .setTextInputComponent( + (() => { + const input = new TextInputBuilder() + .setCustomId("title") + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setMaxLength(128); + + return input; + })(), + ); + + const descriptionLabel = new LabelBuilder() + .setLabel("Bug Description") + .setDescription("Describe the bug in detail") + .setTextInputComponent( + (() => { + const input = new TextInputBuilder() + .setCustomId("description") + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + .setMaxLength(1024); + + return input; + })(), + ); + + const modal = new ModalBuilder().setCustomId(bugModal.id()).setTitle("Report a Bug"); + + const mediaLabel = new LabelBuilder() + .setLabel("Media") + .setDescription("Provide any screenshots or videos that demonstrate the bug") + .setFileUploadComponent(new FileUploadBuilder().setCustomId("media").setRequired(false)); + modal.setLabelComponents(gameLabel, titleLabel, descriptionLabel, mediaLabel); + + return modal; +} + +export async function constructButtons(bug: Bug) { + const row = new ActionRowBuilder<ButtonBuilder>(); + + const isOpen = bug.status === "open"; + const statusId = isOpen ? `bug:close:${bug.id}` : `bug:open:${bug.id}`; + const statusLabel = isOpen ? "Close" : "Reopen"; + + const statusToggle = new ButtonBuilder() + .setCustomId(statusId) + .setLabel(statusLabel) + .setStyle(isOpen ? ButtonStyle.Danger : ButtonStyle.Success); + const editButton = new ButtonBuilder() + .setCustomId(`bug:edit:${bug.id}`) + .setLabel("Edit") + .setStyle(ButtonStyle.Secondary); + const deleteButton = new ButtonBuilder() + .setCustomId(`bug:delete:${bug.id}`) + .setLabel("Delete") + .setStyle(ButtonStyle.Secondary); + const trelloButton = new ButtonBuilder() + .setCustomId(`bug:trello:${bug.id}`) + .setLabel("Trello") + .setStyle(ButtonStyle.Secondary); + + row.addComponents(statusToggle, editButton, deleteButton, trelloButton); + return row; +} + +export function text(content: string) { + return new TextDisplayBuilder().setContent(content); +} +export function thumbnail(url?: string) { + return url ? new ThumbnailBuilder().setURL(url) : undefined; +} + +export async function constructBugContainer( + bug: Bug, + actor: Actor, + file?: AttachmentBuilder, +): Promise<ContainerBuilder | undefined> { + const affected = bug.affectedProjects[0]; + if (!affected) return undefined; + + // @ts-expect-error: shudup + const project = Projects[affected]; + if (!project) return undefined; + + const { title, description, bugNumber } = bug; + const bodyText = text(`### ${title}\n${description}`); + const footerText = text( + [`-# #${bugNumber}`, project.displayName, `Reported by <@${actor.discordId!}>`].join(" • "), + ); + + const icon = thumbnail(project.iconURL); + + const section = new SectionBuilder().addTextDisplayComponents(bodyText); + if (icon) section.setThumbnailAccessory(icon); + + const buttons = await constructButtons(bug); + + const gallery = file + ? new MediaGalleryBuilder().addItems( + new MediaGalleryItemBuilder().setURL(`attachment://${file.name}`), + ) + : undefined; + + const container = new ContainerBuilder(); + container.addSectionComponents(section); + if (gallery) container.addMediaGalleryComponents(gallery); + container.addTextDisplayComponents(footerText); + container.addActionRowComponents(buttons); + + return container; +}
M apps/bot/src/feats/index.tsapps/bot/src/feats/index.ts

@@ -1,5 +1,6 @@

+import { bugCommand, bugModal } from "./bugs"; import { miscCommands } from "./misc"; import { ticketCommands, ticketComponents } from "./tickets"; -export const commands = [...ticketCommands, ...miscCommands]; -export const components = [...ticketComponents]; +export const commands = [...ticketCommands, ...miscCommands, bugCommand]; +export const components = [...ticketComponents, bugModal];
A pkgs/db/drizzle/20260629171950_dizzy_expediter/migration.sql

@@ -0,0 +1,18 @@

+CREATE TABLE `bugs` ( + `id` integer PRIMARY KEY AUTOINCREMENT, + `guild_id` integer NOT NULL, + `actor_id` integer NOT NULL, + `bug_number` integer NOT NULL, + `status` text, + `title` text NOT NULL, + `description` text NOT NULL, + `affected_projects` text DEFAULT '[]' NOT NULL, + `sent` integer DEFAULT false NOT NULL, + `message_id` text, + `thread_id` text, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + `closed_at` integer, + CONSTRAINT `fk_bugs_guild_id_guilds_id_fk` FOREIGN KEY (`guild_id`) REFERENCES `guilds`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_bugs_actor_id_actors_id_fk` FOREIGN KEY (`actor_id`) REFERENCES `actors`(`id`) ON DELETE CASCADE +);
A pkgs/db/drizzle/20260629171950_dizzy_expediter/snapshot.json

@@ -0,0 +1,1728 @@

+{ + "version": "7", + "dialect": "sqlite", + "id": "7307595b-c82d-458e-81d5-05f82bed2390", + "prevIds": [ + "35559d0e-f33e-4ca5-8f75-37ac46e70ec0" + ], + "ddl": [ + { + "name": "sessions", + "entityType": "tables" + }, + { + "name": "users", + "entityType": "tables" + }, + { + "name": "actors", + "entityType": "tables" + }, + { + "name": "guilds", + "entityType": "tables" + }, + { + "name": "ticket_attachments", + "entityType": "tables" + }, + { + "name": "ticket_comments", + "entityType": "tables" + }, + { + "name": "ticket_messages", + "entityType": "tables" + }, + { + "name": "ticket_participants", + "entityType": "tables" + }, + { + "name": "tickets", + "entityType": "tables" + }, + { + "name": "bugs", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "scope", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_type", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "discord_id", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "display_name", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "username", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "avatar", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "accepted_privacy_policy", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "accepted_terms_of_service", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "guild_id", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "actors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "discord_id", + "entityType": "columns", + "table": "actors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "display_name", + "entityType": "columns", + "table": "actors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "username", + "entityType": "columns", + "table": "actors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "avatar", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "moderator", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "deleted_at", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "discord_id", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_category", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_log_channel", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_greeting", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_prompt_channel", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_prompt", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_id", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'watever'", + "generated": null, + "name": "key", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "file_name", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "file_type", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "file_size", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_id", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_id", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_role", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "is_private", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "embeds", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "components", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "edited_at", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "deleted_at", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_id", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reason", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "guild_id", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "local_id", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "channel_id", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "thread_id", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "close_reason", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "private_reason", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "opened_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "closed_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "closed_by", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_subject_activity_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_moderator_activity_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "opened_by", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "open_reason", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "subject", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "claimed_by", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "claimed_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "thanked_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "warned_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_message_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "has_moderator_message", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "has_author_message", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "guild_id", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "bug_number", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'[]'", + "generated": null, + "name": "affected_projects", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "sent", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "thread_id", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "closed_at", + "entityType": "columns", + "table": "bugs" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_sessions_user_id_users_id_fk", + "entityType": "fks", + "table": "sessions" + }, + { + "columns": [ + "guild_id" + ], + "tableTo": "guilds", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_actors_guild_id_guilds_id_fk", + "entityType": "fks", + "table": "actors" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_actors_user_id_users_id_fk", + "entityType": "fks", + "table": "actors" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "ticket_messages", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_attachments_message_id_ticket_messages_id_fk", + "entityType": "fks", + "table": "ticket_attachments" + }, + { + "columns": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_attachments_ticket_id_tickets_id_fk", + "entityType": "fks", + "table": "ticket_attachments" + }, + { + "columns": [ + "actor_id" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_attachments_actor_id_actors_id_fk", + "entityType": "fks", + "table": "ticket_attachments" + }, + { + "columns": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_comments_ticket_id_tickets_id_fk", + "entityType": "fks", + "table": "ticket_comments" + }, + { + "columns": [ + "actor_id" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_comments_actor_id_actors_id_fk", + "entityType": "fks", + "table": "ticket_comments" + }, + { + "columns": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_messages_ticket_id_tickets_id_fk", + "entityType": "fks", + "table": "ticket_messages" + }, + { + "columns": [ + "actor_id" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_messages_actor_id_actors_id_fk", + "entityType": "fks", + "table": "ticket_messages" + }, + { + "columns": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_participants_ticket_id_tickets_id_fk", + "entityType": "fks", + "table": "ticket_participants" + }, + { + "columns": [ + "actor_id" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_participants_actor_id_actors_id_fk", + "entityType": "fks", + "table": "ticket_participants" + }, + { + "columns": [ + "guild_id" + ], + "tableTo": "guilds", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_tickets_guild_id_guilds_id_fk", + "entityType": "fks", + "table": "tickets" + }, + { + "columns": [ + "closed_by" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_tickets_closed_by_actors_id_fk", + "entityType": "fks", + "table": "tickets" + }, + { + "columns": [ + "opened_by" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_tickets_opened_by_actors_id_fk", + "entityType": "fks", + "table": "tickets" + }, + { + "columns": [ + "subject" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_tickets_subject_actors_id_fk", + "entityType": "fks", + "table": "tickets" + }, + { + "columns": [ + "claimed_by" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_tickets_claimed_by_actors_id_fk", + "entityType": "fks", + "table": "tickets" + }, + { + "columns": [ + "guild_id" + ], + "tableTo": "guilds", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_bugs_guild_id_guilds_id_fk", + "entityType": "fks", + "table": "bugs" + }, + { + "columns": [ + "actor_id" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_bugs_actor_id_actors_id_fk", + "entityType": "fks", + "table": "bugs" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sessions_pk", + "table": "sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "users_pk", + "table": "users", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "actors_pk", + "table": "actors", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "guilds_pk", + "table": "guilds", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ticket_attachments_pk", + "table": "ticket_attachments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ticket_comments_pk", + "table": "ticket_comments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ticket_messages_pk", + "table": "ticket_messages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ticket_participants_pk", + "table": "ticket_participants", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tickets_pk", + "table": "tickets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "bugs_pk", + "table": "bugs", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "guild_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "actors_guild_user_idx", + "entityType": "indexes", + "table": "actors" + }, + { + "columns": [ + { + "value": "ticket_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_attachments_ticket_idx", + "entityType": "indexes", + "table": "ticket_attachments" + }, + { + "columns": [ + { + "value": "actor_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_attachments_actor_idx", + "entityType": "indexes", + "table": "ticket_attachments" + }, + { + "columns": [ + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_attachments_public_id_idx", + "entityType": "indexes", + "table": "ticket_attachments" + }, + { + "columns": [ + { + "value": "key", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_attachments_key_idx", + "entityType": "indexes", + "table": "ticket_attachments" + }, + { + "columns": [ + { + "value": "ticket_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_comments_ticket_idx", + "entityType": "indexes", + "table": "ticket_comments" + }, + { + "columns": [ + { + "value": "actor_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_comments_actor_idx", + "entityType": "indexes", + "table": "ticket_comments" + }, + { + "columns": [ + { + "value": "ticket_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_messages_ticket_idx", + "entityType": "indexes", + "table": "ticket_messages" + }, + { + "columns": [ + { + "value": "actor_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_messages_actor_idx", + "entityType": "indexes", + "table": "ticket_messages" + }, + { + "columns": [ + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_messages_public_id_idx", + "entityType": "indexes", + "table": "ticket_messages" + }, + { + "columns": [ + { + "value": "ticket_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_participants_ticket_idx", + "entityType": "indexes", + "table": "ticket_participants" + }, + { + "columns": [ + { + "value": "actor_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_participants_actor_idx", + "entityType": "indexes", + "table": "ticket_participants" + }, + { + "columns": [ + { + "value": "role", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_participants_role_idx", + "entityType": "indexes", + "table": "ticket_participants" + }, + { + "columns": [ + { + "value": "guild_id", + "isExpression": false + }, + { + "value": "local_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "tickets_guild_local_idx", + "entityType": "indexes", + "table": "tickets" + }, + { + "columns": [ + "discord_id" + ], + "nameExplicit": false, + "name": "users_discord_id_unique", + "entityType": "uniques", + "table": "users" + }, + { + "columns": [ + "discord_id" + ], + "nameExplicit": false, + "name": "guilds_discord_id_unique", + "entityType": "uniques", + "table": "guilds" + } + ], + "renames": [] +}
A pkgs/db/src/schemas/bugs.ts

@@ -0,0 +1,46 @@

+import { sql } from "drizzle-orm"; +import * as s from "drizzle-orm/sqlite-core"; + +import { actors } from "./actors"; +import { guilds } from "./guilds"; + +export const bugs = s.sqliteTable("bugs", { + id: s.integer("id").primaryKey({ autoIncrement: true }), + guildId: s + .integer("guild_id") + .notNull() + .references(() => guilds.id, { onDelete: "cascade" }), + actorId: s + .integer("actor_id") + .notNull() + .references(() => actors.id, { onDelete: "cascade" }), + + bugNumber: s.integer("bug_number").notNull(), + status: s.text("status", { enum: ["open", "closed", "resolved"] }), + + title: s.text("title").notNull(), + description: s.text("description").notNull(), + + affectedProjects: s + .text("affected_projects", { mode: "json" }) + .$type<string[]>() + .default([]) + .notNull(), + + sent: s.integer("sent", { mode: "boolean" }).default(false).notNull(), + messageId: s.text("message_id"), + threadId: s.text("thread_id"), + + createdAt: s + .integer("created_at", { mode: "timestamp" }) + .default(sql`(unixepoch())`) + .notNull(), + updatedAt: s + .integer("updated_at", { mode: "timestamp" }) + .default(sql`(unixepoch())`) + .notNull(), + closedAt: s.integer("closed_at", { mode: "timestamp" }), +}); + +export type Bug = typeof bugs.$inferSelect; +export type NewBug = typeof bugs.$inferInsert;
M pkgs/db/src/schemas/index.tspkgs/db/src/schemas/index.ts

@@ -3,3 +3,5 @@ export * from "./actors";

export * from "./guilds"; export * from "./tickets"; + +export * from "./bugs";
M pkgs/db/src/schemas/relations.tspkgs/db/src/schemas/relations.ts

@@ -1,6 +1,7 @@

import { defineRelations } from "drizzle-orm"; import { actors } from "./actors"; +import { bugs } from "./bugs"; import { guilds } from "./guilds"; import { ticketAttachments,

@@ -10,7 +11,6 @@ ticketParticipants,

tickets, } from "./tickets"; import { sessions, users } from "./users"; - export default defineRelations( { guilds,

@@ -22,6 +22,7 @@ ticketParticipants,

ticketMessages, ticketAttachments, ticketComments, + bugs, }, (r) => ({ guilds: {

@@ -41,6 +42,7 @@

actors: { guild: r.one.guilds({ from: r.actors.guildId, to: r.guilds.id }), user: r.one.users({ from: r.actors.userId, to: r.users.id }), + bugs: r.many.bugs({ from: r.actors.id, to: r.bugs.actorId }), openedTickets: r.many.tickets({ from: r.actors.id,

@@ -134,6 +136,10 @@ message: r.one.ticketMessages({

from: r.ticketAttachments.messageId, to: r.ticketMessages.id, }), + }, + + bugs: { + actor: r.one.actors({ from: r.bugs.actorId, to: r.actors.id }), }, }), );