all repos — stealth-developers @ 9a9249f1d097b77cab179b6dfb3935267c4b09a9

bot: refactor to use new db schema
vi did:web:vt3e.cat
Sat, 06 Jun 2026 01:05:14 +0100
commit

9a9249f1d097b77cab179b6dfb3935267c4b09a9

parent

eb45bf91d07a587e564849ea7fe7e87c12de47c4

M apps/bot/.sapphirerc.jsonapps/bot/.sapphirerc.json

@@ -1,16 +1,16 @@

{ - "projectLanguage": "ts", - "locations": { - "base": "src", - "arguments": "arguments", - "commands": "commands", - "listeners": "listeners", - "preconditions": "preconditions", - "interaction-handlers": "interaction-handlers", - "routes": "" - }, - "customFileTemplates": { - "enabled": false, - "location": "" - } -}+ "projectLanguage": "ts", + "locations": { + "base": "src", + "arguments": "arguments", + "commands": "commands", + "listeners": "listeners", + "preconditions": "preconditions", + "interaction-handlers": "interaction-handlers", + "routes": "" + }, + "customFileTemplates": { + "enabled": false, + "location": "" + } +}
M apps/bot/src/commands/tickets/command.tsapps/bot/src/commands/tickets/command.ts

@@ -5,10 +5,11 @@ import { Subcommand } from "@sapphire/plugin-subcommands";

import { ApplyOptions } from "@sapphire/decorators"; import { container, Result } from "@sapphire/framework"; -import db, { eq, tickets } from "@stealth-developers/db"; +import db, { eq, desc, tickets } from "@stealth-developers/db"; import config from "@stealth-developers/config"; import { getCategory, PublicError } from "@/lib"; import { upsertUser } from "$/db"; +import { errorMessage } from "@/lib/interactions"; @ApplyOptions<Subcommand.Options>({ description: "ticket-related commands",

@@ -98,46 +99,49 @@ );

} private async doCreateTicket(interaction: Subcommand.ChatInputCommandInteraction) { + if (!interaction.guild) + return errorMessage(interaction, "Failed to create ticket: Guild not found"); + const isOnBehalf = interaction.options.getSubcommand(true) === "for"; const publicId = randomUUID(); - if (!interaction.member) { - return interaction.reply({ - content: "Failed to create ticket: Member not found", - flags: ["Ephemeral"], - }); - } + if (!interaction.member) + return errorMessage(interaction, "Failed to create ticket: Member not found"); - const creator = await upsertUser(interaction.member); - if (!creator) - return interaction.reply({ - content: "Failed to create ticket: User not found", - flags: ["Ephemeral"], - }); + const creator = await upsertUser(interaction.member, interaction.guild); + if (!creator) return errorMessage(interaction, "Failed to create ticket: Creator not found"); const subject = isOnBehalf ? interaction.options.getUser("user") : interaction.user; - const subjectUser = subject ? await upsertUser(subject) : creator; + const subjectUser = subject ? await upsertUser(subject, interaction.guild!) : creator; if (!subjectUser) - return interaction.reply({ - content: "Failed to create ticket: Subject not found", - flags: ["Ephemeral"], - }); + return errorMessage(interaction, "Failed to create ticket: Subject user not found"); if (isOnBehalf && !creator.moderator) - return interaction.reply({ - content: "You don't have permission to create a ticket for another user.", - flags: ["Ephemeral"], - }); + return errorMessage( + interaction, + "You don't have permission to create a ticket for another user.", + ); const ticketCategory = await getCategory(config.discord.server.ticket_category); let createdChannel: TextChannel | undefined = undefined; const txResult = await Result.fromAsync(async () => { return await db.transaction(async (tx) => { + const [maxTicket] = await tx + .select({ localId: tickets.localId }) + .from(tickets) + .where(eq(tickets.guildId, creator.guildId)) + .orderBy(desc(tickets.localId)) + .limit(1); + + const localId = (maxTicket?.localId ?? 0) + 1; + const [ticket] = await tx .insert(tickets) .values({ - anonymousId: publicId, + id: publicId, + guildId: creator.guildId, + localId: localId, status: "open", openedAt: new Date(), openedBy: creator.id,

@@ -147,7 +151,7 @@ .returning();

if (!ticket) throw new PublicError("failed to add ticket to database."); - const ticketNumber = ticket.id; + const ticketNumber = ticket.localId; const paddedTicketNumber = String(ticketNumber).padStart(4, "0"); const channel = await interaction.guild?.channels.create({

@@ -177,10 +181,7 @@ : "There was an error creating the ticket.";

if (error instanceof Error) this.container.logger.error(error.message, error.stack); - return interaction.reply({ - content: message, - flags: ["Ephemeral"], - }); + return errorMessage(interaction, message); } const channelId = txResult.unwrap();
M apps/bot/src/lib/constants.tsapps/bot/src/lib/constants.ts

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

-import { join } from 'path'; +import { join } from "path"; -export const rootDir = join(__dirname, '..', '..'); -export const srcDir = join(rootDir, 'src'); +export const rootDir = join(__dirname, "..", ".."); +export const srcDir = join(rootDir, "src");
M apps/bot/src/lib/db.tsapps/bot/src/lib/db.ts

@@ -1,28 +1,59 @@

-import { User, type APIInteractionGuildMember } from "discord.js"; -import { GuildMember } from "discord.js"; -import { upsertUser as _upsertUser } from "@stealth-developers/db"; +import { User, type APIInteractionGuildMember, GuildMember, Guild } from "discord.js"; +import { upsertActor } from "@stealth-developers/db"; + +export async function upsertUser( + input: APIInteractionGuildMember | GuildMember | User, + guild: Guild, +) { + let discordId: string; + let username: string; + let displayName: string | null = null; + let avatar: string | null = null; + let isModerator = false; -export async function upsertUser(input: APIInteractionGuildMember | GuildMember | User) { if ("user" in input) { const member = input; const user = member.user; + discordId = user.id; + username = user.username; const nick = member instanceof GuildMember ? member.nickname : member.nick; - const displayName = user instanceof User ? user.globalName : user.global_name; + const globalName = user instanceof User ? user.globalName : user.global_name; + displayName = nick || globalName || user.username; + avatar = member.avatar || user.avatar; + + if (member instanceof GuildMember) { + isModerator = member.permissions.has("Administrator"); + } else if (member.permissions) { + const { PermissionsBitField } = await import("discord.js"); + const perms = new PermissionsBitField(BigInt(member.permissions)); + isModerator = perms.has("Administrator"); + } + } else { + const user = input; + discordId = user.id; + username = user.username; + displayName = user.displayName; + avatar = user.avatar; - return _upsertUser({ - id: user.id, - avatar: member.avatar || member.user.avatar, - username: user.username, - displayName: nick || displayName, - }); + try { + const member = await guild.members.fetch(user.id); + isModerator = member.permissions.has("ModerateMembers"); + } catch {} } - const user = input; - return _upsertUser({ - id: user.id, - avatar: user.avatar, - username: user.username, - displayName: user.displayName, + return upsertActor({ + guild: { + discordId: guild.id, + name: guild.name, + icon: guild.iconURL() || guild.icon, + }, + user: { + discordId, + username, + displayName, + avatar, + }, + moderator: isModerator, }); }
A apps/bot/src/lib/interactions.ts

@@ -0,0 +1,12 @@

+import type { BaseInteraction, InteractionReplyOptions } from "discord.js"; + +type Flags = InteractionReplyOptions["flags"]; + +export function errorMessage(interaction: BaseInteraction, message: string, ephemeral = false) { + const flags: Flags = ephemeral ? ["Ephemeral"] : []; + if (!interaction.isCommand()) return; + + if (interaction.deferred) return interaction.editReply({ content: message }); + if (!interaction.replied) return interaction.reply({ content: message, flags }); + else return interaction.followUp({ content: message, flags }); +}
M apps/bot/src/lib/permissions.tsapps/bot/src/lib/permissions.ts

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

-import { getUserByDiscordId } from "@stealth-developers/db"; +import { getActorByDiscordId } from "@stealth-developers/db"; -export const isModerator = async (id: string) => { - const user = await getUserByDiscordId(id); - return user?.moderator; +export const isModerator = async (id: string, guildId: number) => { + const actor = await getActorByDiscordId(id, guildId); + return !!actor?.moderator; };
M apps/bot/src/lib/tickets.tsapps/bot/src/lib/tickets.ts

@@ -1,12 +1,12 @@

-import db, { tickets, eq, getUser, getUserByDiscordId } from "@stealth-developers/db"; +import db, { tickets, eq, getActor, getActorByDiscordId } from "@stealth-developers/db"; -export async function hasAccessToTicket(userId: string, ticketId: number) { +export async function hasAccessToTicket(userId: string, ticketId: string) { const [ticket] = await db.select().from(tickets).where(eq(tickets.id, ticketId)).limit(1); if (!ticket) return false; - const userPromise = getUserByDiscordId(userId); - const subjectPromise = getUser(ticket.subject); - const openedByPromise = ticket.subject === ticket.openedBy ? null : getUser(ticket.openedBy); + const userPromise = getActorByDiscordId(userId, ticket.guildId); + const subjectPromise = getActor(ticket.subject); + const openedByPromise = ticket.subject === ticket.openedBy ? null : getActor(ticket.openedBy); const [user, subject, openedByResult] = await Promise.all([ userPromise,
M apps/bot/tsconfig.jsonapps/bot/tsconfig.json

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

{ - "extends": ["../../tsconfig.json"], - "compilerOptions": { - "paths": { - "@/*": ["./src/*"], - "$/*": ["./src/lib/*"], - }, - "rootDir": "./src", - }, - "include": ["src/**/*"], + "extends": ["../../tsconfig.json"], + "compilerOptions": { + "paths": { + "@/*": ["./src/*"], + "$/*": ["./src/lib/*"] + }, + "rootDir": "./src" + }, + "include": ["src/**/*"] }