all repos — stealth-developers @ 81453c0542d093cf0faf523d510a6f6aa0eacd54

bot: we're fixing in #Prod
vi did:web:vt3e.cat
Mon, 29 Jun 2026 20:21:24 +0100
commit

81453c0542d093cf0faf523d510a6f6aa0eacd54

parent

daca13a6ef949c27e8b29443201b8a5f86a34942

M apps/bot/src/feats/tickets/actions/assignment.tsapps/bot/src/feats/tickets/actions/assignment.ts

@@ -4,7 +4,7 @@ addTicketComment,

addTicketParticipant, editTicket, } from "@stealth-developers/db"; -import { ActionRowBuilder, ButtonBuilder, ChannelType } from "discord.js"; +import { ActionRowBuilder, ButtonBuilder } from "discord.js"; import { getGuildActor } from "f/guild-actor"; import { PublicError, logger } from "@/lib";

@@ -13,7 +13,7 @@ import type { TicketContext } from "../types";

import { TicketButtonPresets } from "../buttons"; import { hasAccessToTicket } from "../lib"; -import { channelName, messageChannel } from "./helpers"; +import { messageChannel } from "./helpers"; export async function claimTicket(ctx: TicketContext) { const { ticket, actor, ticketChannel, guild } = ctx;

@@ -52,16 +52,6 @@ components: [actionRow],

}).catch((err) => logger.error(err, `failed to send claim message to channel ${ticketChannel.id}`), ); - - if (ticketChannel.type === ChannelType.GuildText) { - ticketChannel - .edit({ - name: channelName(ticket.localId, "claimed", actor.displayName || actor.username!), - }) - .catch((err) => - logger.error(err, `failed to update channel name on claim for ${ticketChannel.id}`), - ); - } } return newTicket;

@@ -94,16 +84,6 @@ components: [actionRow],

}).catch((err) => logger.error(err, `Failed to send unclaim message to channel ${ticketChannel.id}`), ); - - if (ticketChannel.type === ChannelType.GuildText) { - ticketChannel - .edit({ - name: channelName(ticket.localId, "open"), - }) - .catch((err) => - logger.error(err, `Failed to update channel name on unclaim for ${ticketChannel.id}`), - ); - } } return newTicket;

@@ -117,6 +97,7 @@ ticketId: string;

role: "participant" | "moderator" | "subject"; reason?: string; }, + redactUser = false, ) { const botActor = getGuildActor(guildId); if (!botActor) throw new PublicError("Failed to close ticket: Bot actor not found.");

@@ -131,6 +112,8 @@

await addTicketComment({ actorId: botActor.id, ticketId: data.ticketId, - content: `Added participant: ${data.actor.displayName || data.actor.username!} - ${data.reason ?? "no reason provided"}`, + content: redactUser + ? `Added participant - ${data.reason ?? "no reason provided"}` + : `Added participant: ${data.actor.displayName || data.actor.username!} - ${data.reason ?? "no reason provided"}`, }); }
M apps/bot/src/feats/tickets/actions/lifecycle.tsapps/bot/src/feats/tickets/actions/lifecycle.ts

@@ -37,7 +37,7 @@ import { TicketModalPresets } from "../modals";

import { addParticipant } from "./assignment"; import { getCommentContainer } from "./comments"; import { getTicketContainer } from "./helpers"; -import { canDmUser, channelName, messageChannel, messageUser } from "./helpers"; +import { canDmUser, messageChannel, messageUser } from "./helpers"; export async function createTicket( opener: AnyUser,

@@ -119,9 +119,9 @@ ticketId = ticket.id;

const setupResult = await Result.fromAsync(async () => { const channel = await guild.channels.create({ - name: channelName(localId, "provisioning"), type: ChannelType.GuildText, parent: ticketCategory, + name: `ticket-${String(localId).padStart(4, "0")}`, permissionOverwrites: [ { id: guild.id,

@@ -140,7 +140,11 @@ })),

{ id: client.user!.id, type: OverwriteType.Member, - allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels], + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.ManageChannels, + PermissionFlagsBits.CreatePrivateThreads, + ], }, { id: subjectUser.discordId!,

@@ -249,14 +253,6 @@ `Failed to create private moderator thread for ticket ${ticket.id}`,

); } - channel - .edit({ - name: channelName(localId, "open"), - }) - .catch((err) => { - logger.error(err, `failed to update channel name to open for ticket ${ticket.id}`); - }); - return channel; });

@@ -349,6 +345,9 @@ channel

.delete() .catch((err) => logger.error(err, `Failed to delete channel ${channel.id} during cleanup`)); } else { + const moderators = await getModerators(dbGuild.id); + if (!moderators) throw new PublicError("Failed to close ticket: Moderators not found"); + const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents( TicketButtonPresets.reopenTicketButton(ticketId), TicketButtonPresets.cleanTicketButton(ticketId),

@@ -362,13 +361,38 @@ }).catch((err) => logger.error(err, `Failed to message closed channel ${channel.id}`));

channel .edit({ - name: channelName(result.localId, result.status), permissionOverwrites: [ { + id: guild.id, + type: OverwriteType.Role, + deny: [PermissionFlagsBits.ViewChannel], + }, + ...moderators.map((moderator) => ({ + id: moderator.discordId!, + type: OverwriteType.Member, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.AttachFiles, + ], + })), + { + id: client.user!.id, + type: OverwriteType.Member, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.ManageChannels, + PermissionFlagsBits.CreatePrivateThreads, + ], + }, + { id: dbSubject.discordId!, - deny: [PermissionFlagsBits.ViewChannel], - allow: [], type: OverwriteType.Member, + deny: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.AttachFiles, + ], }, ], })

@@ -469,23 +493,49 @@ }).catch((err) =>

logger.error(err, `Failed to send reopen message to channel ${ticketChannel.id}`), ); + if (!ticket.subjectActor) + throw new PublicError("Failed to reopen ticket: Subject actor not found"); + + const moderators = await getModerators(guild.id); + if (!moderators) throw new PublicError("Failed to reopen ticket: Moderators not found"); + if (ticketChannel.type === ChannelType.GuildText) { ticketChannel .edit({ - name: channelName(ticket.localId, "claimed", actor.displayName || actor.username!), - permissionOverwrites: ticket.subjectActor - ? [ - { - id: ticket.subjectActor.discordId!, - type: OverwriteType.Member, - allow: [ - PermissionFlagsBits.ViewChannel, - PermissionFlagsBits.SendMessages, - PermissionFlagsBits.AttachFiles, - ], - }, - ] - : undefined, + permissionOverwrites: [ + { + id: guild.discordId, + type: OverwriteType.Role, + deny: [PermissionFlagsBits.ViewChannel], + }, + ...moderators.map((moderator) => ({ + id: moderator.discordId!, + type: OverwriteType.Member, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.AttachFiles, + ], + })), + { + id: client.user!.id, + type: OverwriteType.Member, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.ManageChannels, + PermissionFlagsBits.CreatePrivateThreads, + ], + }, + { + id: ticket.subjectActor.discordId!, + type: OverwriteType.Member, + deny: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.AttachFiles, + ], + }, + ], }) .catch((err) => logger.error(
M apps/bot/src/feats/tickets/buttons.tsapps/bot/src/feats/tickets/buttons.ts

@@ -67,8 +67,11 @@ options: {

ticketId: option.string({ required: true }), }, async run(interaction, _, { actor, ticket, ticketChannel, guild }) { - if (!ticket || !ticketChannel || !actor || !guild) - return errorMessage(interaction, "Failed to find ticket"); + if (!ticket) return errorMessage(interaction, "Failed to find ticket in the database"); + else if (!ticketChannel) + return errorMessage(interaction, "Failed to find ticket channel, may not have access"); + else if (!actor) return errorMessage(interaction, "Failed to find actor"); + else if (!guild) return errorMessage(interaction, "Failed to find guild"); const result = await Result.fromAsync( actions.reopenTicket({ ticket, ticketChannel, actor, guild }),

@@ -111,8 +114,11 @@ options: {

ticketId: option.string({ required: true }), }, async run(interaction, _, { actor, ticket, ticketChannel, guild }) { - if (!ticket || !ticketChannel || !actor || !guild) - return errorMessage(interaction, "Failed to find ticket"); + if (!ticket) return errorMessage(interaction, "Failed to find ticket in the database"); + else if (!ticketChannel) + return errorMessage(interaction, "Failed to find ticket channel, may not have access"); + else if (!actor) return errorMessage(interaction, "Failed to find actor"); + else if (!guild) return errorMessage(interaction, "Failed to find guild"); const result = await Result.fromAsync( actions.unclaimTicket({ ticket, ticketChannel, actor, guild }),

@@ -132,8 +138,11 @@ options: {

ticketId: option.string({ required: true }), }, async run(interaction, _, { actor, ticket, ticketChannel, guild }) { - if (!ticket || !ticketChannel || !actor || !guild) - return errorMessage(interaction, "Failed to find ticket"); + if (!ticket) return errorMessage(interaction, "Failed to find ticket in the database"); + else if (!ticketChannel) + return errorMessage(interaction, "Failed to find ticket channel, may not have access"); + else if (!actor) return errorMessage(interaction, "Failed to find actor"); + else if (!guild) return errorMessage(interaction, "Failed to find guild"); if (!actor.moderator) return errorMessage(interaction, "You must be a moderator to use this command");

@@ -191,8 +200,11 @@ ticketId: option.string({ required: true }),

}, async run(interaction, _, ctx) { const { actor, ticket, ticketChannel, guild } = ctx; - if (!ticket || !ticketChannel || !actor || !guild) - return errorMessage(interaction, "Failed to find ticket"); + if (!ticket) return errorMessage(interaction, "Failed to find ticket in the database"); + else if (!ticketChannel) + return errorMessage(interaction, "Failed to find ticket channel, may not have access"); + else if (!actor) return errorMessage(interaction, "Failed to find actor"); + else if (!guild) return errorMessage(interaction, "Failed to find guild"); const result = await Result.fromAsync(actions.cleanTicket(ctx)); if (result.isErr()) return errorMessage(interaction, "Failed to delete ticket channel");
M apps/bot/src/feats/tickets/commands.tsapps/bot/src/feats/tickets/commands.ts

@@ -147,8 +147,11 @@

group.subcommand("reopen", { description: "Reopen a ticket", async run(interaction, _, { actor, ticket, ticketChannel, guild }) { - if (!ticket || !ticketChannel || !actor || !guild) - return errorMessage(interaction, "Failed to find ticket"); + if (!ticket) return errorMessage(interaction, "Failed to find ticket in the database"); + else if (!ticketChannel) + return errorMessage(interaction, "Failed to find ticket channel, may not have access"); + else if (!actor) return errorMessage(interaction, "Failed to find actor"); + else if (!guild) return errorMessage(interaction, "Failed to find guild"); const result = await Result.fromAsync( actions.reopenTicket({ ticket, ticketChannel, actor, guild }),

@@ -165,8 +168,11 @@

group.subcommand("claim", { description: "Claim the ticket", async run(interaction, _, { actor, ticket, ticketChannel, guild }) { - if (!ticket || !ticketChannel || !actor || !guild) - return errorMessage(interaction, "Failed to find ticket"); + if (!ticket) return errorMessage(interaction, "Failed to find ticket in the database"); + else if (!ticketChannel) + return errorMessage(interaction, "Failed to find ticket channel, may not have access"); + else if (!actor) return errorMessage(interaction, "Failed to find actor"); + else if (!guild) return errorMessage(interaction, "Failed to find guild"); const result = await Result.fromAsync( actions.claimTicket({ ticket, ticketChannel, actor, guild }),

@@ -183,8 +189,11 @@

group.subcommand("unclaim", { description: "Unclaim a ticket", async run(interaction, _, { actor, ticket, ticketChannel, guild }) { - if (!ticket || !ticketChannel || !actor || !guild) - return errorMessage(interaction, "Failed to find ticket"); + if (!ticket) return errorMessage(interaction, "Failed to find ticket in the database"); + else if (!ticketChannel) + return errorMessage(interaction, "Failed to find ticket channel, may not have access"); + else if (!actor) return errorMessage(interaction, "Failed to find actor"); + else if (!guild) return errorMessage(interaction, "Failed to find guild"); const result = await Result.fromAsync( actions.unclaimTicket({ ticket, ticketChannel, actor, guild }),

@@ -205,8 +214,11 @@ ticket: option.number("The ID of the ticket", { required: false }),

comment: option.string("The comment to add", { required: false }), }, async run(interaction, { comment }, { actor, ticket, ticketChannel, guild }) { - if (!ticket || !ticketChannel || !actor || !guild) - return errorMessage(interaction, "Failed to find ticket"); + if (!ticket) return errorMessage(interaction, "Failed to find ticket in the database"); + else if (!ticketChannel) + return errorMessage(interaction, "Failed to find ticket channel, may not have access"); + else if (!actor) return errorMessage(interaction, "Failed to find actor"); + else if (!guild) return errorMessage(interaction, "Failed to find guild"); if (!actor.moderator) return errorMessage(interaction, "You must be a moderator to use this command");

@@ -255,8 +267,11 @@ group.subcommand("delete", {

description: "Delete the ticket channel", async run(interaction, _, ctx) { const { actor, ticket, ticketChannel, guild } = ctx; - if (!ticket || !ticketChannel || !actor || !guild) - return errorMessage(interaction, "Failed to find ticket"); + if (!ticket) return errorMessage(interaction, "Failed to find ticket in the database"); + else if (!ticketChannel) + return errorMessage(interaction, "Failed to find ticket channel, may not have access"); + else if (!actor) return errorMessage(interaction, "Failed to find actor"); + else if (!guild) return errorMessage(interaction, "Failed to find guild"); const result = await Result.fromAsync(actions.cleanTicket(ctx)); if (result.isErr()) return errorMessage(interaction, "Failed to delete ticket channel");

@@ -332,12 +347,16 @@ if (!participant) {

return errorMessage(interaction, "Failed to add participant"); } - await actions.addParticipant(interaction.guild.id, { - actor: participant, - ticketId: ticket.id, - role: "participant", - reason: args.reason, - }); + await actions.addParticipant( + interaction.guild.id, + { + actor: participant, + ticketId: ticket.id, + role: "participant", + reason: args.reason, + }, + true, + ); await interaction.reply({ content: `Added participant <@${args.user.id}> to the ticket - ${args.reason ?? "no reason provided"}`,
M apps/bot/src/feats/tickets/modals.tsapps/bot/src/feats/tickets/modals.ts

@@ -10,6 +10,7 @@ getTicket,

guilds, } from "@stealth-developers/db"; import { + CheckboxBuilder, LabelBuilder, MessageFlags, ModalBuilder,

@@ -19,8 +20,10 @@ } from "discord.js";

import { kitten } from "@/client"; import { errorMessage, handleError, upsertUser } from "@/lib"; +import { useGetTicketData } from "@/middleware"; import * as actions from "./actions"; +import { hasAccessToTicket } from "./lib"; export const configTextModal = kitten.modal("config-text", { options: {

@@ -51,27 +54,36 @@ });

}, }); -export const closeTicketModal = kitten.modal("close-ticket", { +export const closeTicketModal = useGetTicketData.modal("close-ticket", { options: { ticketId: option.string({ required: true }), }, - run: async (interaction, { ticketId }) => { + run: async (interaction, _, { guild, actor, ticket }) => { await interaction.deferReply({ flags: ["Ephemeral"] }); - if (!interaction.guild) + if (!interaction.guild) { return errorMessage(interaction, "You must run this command in a guild"); + } else if (!ticket) { + return errorMessage(interaction, "Failed to find the ticket"); + } else if (!actor) { + return errorMessage(interaction, "Failed to find your actor in the database"); + } else if (!guild) { + return errorMessage(interaction, "Failed to find the guild"); + } - const ticket = await getTicket(ticketId); - if (!ticket) return errorMessage(interaction, "Ticket not found"); + if (!hasAccessToTicket(actor, guild, ticket)) { + return errorMessage(interaction, "You do not have access to this ticket"); + } - const user = await upsertUser(interaction.user, interaction.guild); - if (!user) return errorMessage(interaction, "Failed to upsert user"); + const shouldDelete = actor.moderator ? interaction.fields.getCheckbox("delete") : false; const reasons = { public: interaction.fields.getTextInputValue("reason:public"), - private: user.moderator ? interaction.fields.getTextInputValue("reason:private") : undefined, + private: actor.moderator ? interaction.fields.getTextInputValue("reason:private") : undefined, }; - const result = await Result.fromAsync(actions.closeTicket(user, ticket.id, reasons)); + const result = await Result.fromAsync( + actions.closeTicket(actor, ticket.id, reasons, shouldDelete), + ); if (result.isErr()) return handleError(interaction, result.unwrapErr(), "Failed to close ticket.");

@@ -162,6 +174,11 @@ .setCustomId("reason:private")

.setRequired(isModerator), ); + const deleteCheckbox = new LabelBuilder() + .setLabel("Delete ticket channel") + .setDescription("Delete the ticket channel after closing") + .setCheckboxComponent(new CheckboxBuilder().setCustomId("delete").setDefault(false)); + const labels = isModerator ? [reasonLabel, privateReasonLabel] : [reasonLabel]; const modal = new ModalBuilder()

@@ -169,6 +186,7 @@ .setCustomId(closeTicketModal.id({ ticketId: id }))

.setTitle("Close ticket") .addLabelComponents(...labels); + if (isModerator) modal.addLabelComponents(deleteCheckbox); return modal; }, edit: (id: string, reasons: { public?: string | null; private?: string | null }) => {
M apps/bot/src/feats/tickets/timers.tsapps/bot/src/feats/tickets/timers.ts

@@ -4,11 +4,11 @@ import { getTextChannel } from "@/lib";

import { addTimer } from "@/timers"; import { getGuildActor } from "../guild-actor"; -import { channelName, closeTicket, messageChannel } from "./actions"; +import { closeTicket, messageChannel } from "./actions"; // time in minutes before a ticket is considered inactive -const CLAIMANT_INACTIVITY_THRESHOLD = 0.25; -const SUBJECT_INACTIVITY_THRESHOLD = 0.25; +const CLAIMANT_INACTIVITY_THRESHOLD = 15; +const SUBJECT_INACTIVITY_THRESHOLD = 15; addTimer({ name: "Check if tickets are inactive",

@@ -33,12 +33,6 @@

messageChannel(channel.id, { content: `This ticket has been automatically unclaimed due to inactivity, someone else will be with you shortly.`, }); - - channel - .edit({ - name: channelName(ticket.localId, "open"), - }) - .catch(); } continue;
M apps/bot/src/middleware/index.tsapps/bot/src/middleware/index.ts

@@ -77,10 +77,16 @@ }

} } - if (!interaction.channel) return {}; + if (!interaction.channel) { + logger.warn({ interaction }, "No channel found in interaction"); + return {}; + } const ticket = await getTicketByChannelId(interaction.channel.id); - if (!ticket) return {}; + if (!ticket) { + logger.warn({ interaction }, `No ticket found in channel ${interaction.channel.id}`); + return {}; + } if (!ticket.channelId) return { ticket }; const ticketChannel = await getTextChannel(ticket.channelId, true);
M apps/migration/src/4.save-users.tsapps/migration/src/4.save-users.ts

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

import type { GuildMemberResponse, UserResponse } from "@purrkit/types"; -import db, { actors, users } from "@stealth-developers/db"; +import db, { actors, eq, users } from "@stealth-developers/db"; import fs from "node:fs"; import { GUILD_ID } from "./constants";

@@ -13,80 +13,117 @@ fs.readFileSync("members/all-members.json", "utf8"),

) as GuildMemberResponse[]; for (const member of allMembers) { - const [user] = await db - .insert(users) - .values({ - discordId: member.user.id, - displayName: member.nick ?? member.user.global_name, - username: member.user.username, - avatar: member.user.avatar, - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoNothing() - .returning(); + const userSelect = await db + .select() + .from(users) + .where(eq(users.discordId, member.user.id)) + .limit(1); + + let dbUser = Array.isArray(userSelect) ? userSelect[0] : undefined; + + if (!dbUser) { + await db + .insert(users) + .values({ + discordId: member.user.id, + displayName: member.nick ?? member.user.global_name, + username: member.user.username, + avatar: member.user.avatar, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoNothing(); + + const reSelect = await db + .select() + .from(users) + .where(eq(users.discordId, member.user.id)) + .limit(1); + + dbUser = Array.isArray(reSelect) ? reSelect[0] : undefined; + } - if (!user) { + if (!dbUser) { console.error(`failed to save user for member ${member.user.id}`); continue; } - await db - .insert(actors) - .values({ - userId: user.id, - discordId: member.user.id, - displayName: member.nick ?? member.user.global_name, - username: member.user.username, - avatar: member.user.avatar, - guildId: GUILD_ID, + const actorSelect = await db + .select() + .from(actors) + .where(eq(actors.discordId, member.user.id)) + .limit(1); + + const existingActor = Array.isArray(actorSelect) ? actorSelect[0] : undefined; - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoNothing(); + if (!existingActor) { + await db + .insert(actors) + .values({ + userId: dbUser.id, + discordId: member.user.id, + displayName: member.nick ?? member.user.global_name, + username: member.user.username, + avatar: member.user.avatar, + guildId: GUILD_ID, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoNothing(); + } } for (const user of allUsers) { if (!user) { - console.error(`failed to save user ${user.id}`); + console.error("found empty user entry in fetched-users.json"); continue; } - const [dbUser] = await db - .insert(users) - .values({ - discordId: user.id, - displayName: user.global_name, - username: user.username, - avatar: user.avatar, - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoNothing() - .returning(); + const userSelect = await db.select().from(users).where(eq(users.discordId, user.id)).limit(1); + console.log(userSelect); + + let dbUser = Array.isArray(userSelect) ? userSelect[0] : undefined; + + if (!dbUser) { + await db + .insert(users) + .values({ + discordId: user.id, + displayName: user.global_name, + username: user.username, + avatar: user.avatar, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoNothing(); + + const reSelect = await db.select().from(users).where(eq(users.discordId, user.id)).limit(1); + + dbUser = Array.isArray(reSelect) ? reSelect[0] : undefined; + } if (!dbUser) { console.error(`failed to save user ${user.id}`); continue; } - const [dbActor] = await db - .insert(actors) - .values({ - userId: dbUser.id, - discordId: user.id, - displayName: user.global_name, - username: user.username, - avatar: user.avatar, - guildId: GUILD_ID, - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoNothing() - .returning(); + const actorSelect = await db.select().from(actors).where(eq(actors.discordId, user.id)).limit(1); + + const existingActor = Array.isArray(actorSelect) ? actorSelect[0] : undefined; - if (!dbActor) { - console.error(`failed to save actor for user ${user.id}`); + if (!existingActor) { + await db + .insert(actors) + .values({ + userId: dbUser.id, + discordId: user.id, + displayName: user.global_name, + username: user.username, + avatar: user.avatar, + guildId: GUILD_ID, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoNothing(); } }
M apps/migration/src/old-db/index.tsapps/migration/src/old-db/index.ts

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

import { Database } from "bun:sqlite"; import { drizzle } from "drizzle-orm/bun-sqlite"; -const sqlite = new Database("./data/old.sqlite"); +const sqlite = new Database("./data/meow.sqlite"); export const db = drizzle({ client: sqlite }); export * from "./schema";