bot: button equivalents for all appropriate implemented commands
jump to
@@ -1,10 +1,11 @@
{ + "language_servers": ["vtsls", "oxlint", "oxfmt"], "lsp": { "oxlint": { "initialization_options": { "settings": { "configPath": null, - "run": "onType", + "run": "onSave", "disableNestedConfig": false, "fixKind": "safe_fix", "unusedDisableDirectives": "deny",@@ -15,7 +16,7 @@ "oxfmt": {
"initialization_options": { "settings": { "fmt.configPath": null, - "run": "onSave", + "run": "onType", }, }, },
@@ -7,23 +7,23 @@ "dev": "bun src/index.ts | pino-pretty",
"run": "bun src/index.ts" }, "dependencies": { - "@purrkit/router": "^1.1.0", - "@sapphire/decorators": "^6.1.1", + "@purrkit/router": "^1.3.0", + "@sapphire/decorators": "^6.2.0", "@sapphire/discord.js-utilities": "7.3.2", - "@sapphire/framework": "^5.3.2", - "@sapphire/plugin-logger": "^4.0.2", + "@sapphire/framework": "^5.5.0", + "@sapphire/plugin-logger": "^4.1.0", "@sapphire/plugin-subcommands": "^7.0.1", - "@sapphire/utilities": "^3.18.1", + "@sapphire/utilities": "^3.18.2", "@stealth-developers/config": "workspace:*", "@stealth-developers/db": "workspace:*", - "discord.js": "^14.17.3", + "discord.js": "^14.26.4", "pino": "^10.3.1", "pino-pretty": "^13.1.3" }, "devDependencies": { "@sapphire/cli": "^1.9.3", - "@types/node": "^22.10.7", - "@types/ws": "^8.5.13", + "@types/node": "^22.20.0", + "@types/ws": "^8.18.1", "typescript": "~5.4.5" } }
@@ -4,9 +4,9 @@ - [x] for
- [x] view [ticketId] - [ ] actions - [x] close + - [x] reopen - [x] claim - [x] unclaim - - [x] reopen - [x] comment [ticketId?] - [x] edit [ticketId?] - [ ] clean - /ticket delete
@@ -123,8 +123,8 @@ }),
) .addActionRowComponents( new ActionRowBuilder<ButtonBuilder>().addComponents( - TicketButtonPresets.claim(publicId), - TicketButtonPresets.close(publicId), + TicketButtonPresets.claimTicketButton(publicId), + TicketButtonPresets.closeTicketButton(publicId), ), ), );@@ -160,7 +160,7 @@ }),
) .addActionRowComponents( new ActionRowBuilder<ButtonBuilder>().addComponents( - TicketButtonPresets.upload(publicId), + TicketButtonPresets.uploadLink(publicId), ), ), );@@ -246,10 +246,12 @@
container.addTextDisplayComponents(text(`-# ${ticket.id}`)); const actionRow = new ActionRowBuilder<ButtonBuilder>(); - if (limitedDetails) actionRow.addComponents(TicketButtonPresets.thank(ticket.id)); + if (limitedDetails) actionRow.addComponents(TicketButtonPresets.thankButton(ticket.id)); if (!limitedDetails) - actionRow.addComponents(TicketButtonPresets.comments(ticket.id, ticket.comments.length)); - actionRow.addComponents(TicketButtonPresets.transcript(ticket.id)); + actionRow.addComponents( + TicketButtonPresets.viewCommentsButton(ticket.id, ticket.comments.length), + ); + actionRow.addComponents(TicketButtonPresets.transcriptsLink(ticket.id)); container.addActionRowComponents(actionRow); return container;@@ -302,8 +304,16 @@ } else {
channel.edit({ name: channelName(result.localId, result.status), }); + + const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents( + TicketButtonPresets.reopenTicketButton(ticketId), + TicketButtonPresets.cleanTicketButton(ticketId), + ); + messageChannel(channel, { content: `This ticket has been closed by <@${user.discordId}>.`, + components: [actionRow], + flags: ["SuppressNotifications"], }); }@@ -312,7 +322,7 @@ if (!botActor) throw new PublicError("Failed to close ticket: Bot actor not found in database");
const subject = await client.users.fetch(dbSubject.discordId); - const container = await getTicketContainer(result, true); + const container = getTicketContainer(result, true); const messageRes = await Result.fromAsync( messageUser(subject, {@@ -341,7 +351,7 @@ const container = getTicketContainer(result, false);
const message = await Result.fromAsync( messageChannel(logChannel, { components: [container], - flags: ["IsComponentsV2"], + flags: ["SuppressNotifications", "IsComponentsV2"], }), );@@ -375,9 +385,15 @@ lastClaimantActivityAt: new Date(),
}); if (ticketChannel) { + const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents( + TicketButtonPresets.claimTicketButton(ticket.id), + TicketButtonPresets.closeTicketButton(ticket.id), + ); + messageChannel(ticketChannel, { content: `<@${actor.discordId}> reopened this ticket.`, flags: ["SuppressNotifications"], + components: [actionRow], }); if (ticketChannel.type === ChannelType.GuildText) {@@ -416,9 +432,14 @@ lastClaimantActivityAt: new Date(),
}); if (ticketChannel) { + const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents( + TicketButtonPresets.unclaimTicketButton(ticket.id), + ); + messageChannel(ticketChannel, { content: `<@${actor.discordId}> has claimed this ticket, they will be with you shortly.`, flags: ["SuppressNotifications"], + components: [actionRow], }); if (ticketChannel.type === ChannelType.GuildText) {@@ -464,9 +485,13 @@ lastClaimantActivityAt: null,
}); if (ticketChannel) { + const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents( + TicketButtonPresets.claimTicketButton(ticket.id), + ); messageChannel(ticketChannel, { content: `<@${actor.discordId}> unclaimed this ticket, someone else will be with you shortly.`, flags: ["SuppressNotifications"], + components: [actionRow], }); if (ticketChannel.type === ChannelType.GuildText) {
@@ -1,37 +1,16 @@
import { option } from "@purrkit/router"; import { Result } from "@sapphire/result"; import { ButtonBuilder, ContainerBuilder, TextDisplayBuilder, ActionRowBuilder } from "discord.js"; -import { getTicketByChannelId, getTicketByGuild, getTicketByUser } from "@stealth-developers/db"; +import { getTicketByGuild, getTicketByUser } from "@stealth-developers/db"; import { errorMessage, getTextChannel, handleError, logger, PublicError } from "@/lib"; import { hasAccessToTicket } from "@/lib/tickets"; -import { useGetData } from "@/middleware"; +import { useGetTicketData } from "@/middleware"; import { TicketButtonPresets } from "./buttons"; import * as actions from "./actions"; import { manageConfig } from "./config"; import { TicketModalPresets } from "./modals"; - -const useGetTicketData = useGetData.use(async (interaction, { guild }) => { - if (interaction.isChatInputCommand()) { - const ticketId = interaction.options.getNumber("ticket", false); - if (ticketId && guild) { - const ticket = await getTicketByGuild(guild.id, ticketId); - if (!ticket) return {}; - return { ticket }; - } - } - - if (!interaction.channel) return {}; - - const ticket = await getTicketByChannelId(interaction.channel.id); - if (!ticket) return {}; - - if (!ticket.channelId) return { ticket }; - const ticketChannel = await getTextChannel(ticket.channelId, true); - - return { ticket, ticketChannel }; -}); const ticketCommand = useGetTicketData.command("ticket", { description: "Ticket-related commands",@@ -368,7 +347,9 @@
const container = new ContainerBuilder() .addTextDisplayComponents(new TextDisplayBuilder().setContent(promptText)) .addActionRowComponents( - new ActionRowBuilder<ButtonBuilder>().addComponents(TicketButtonPresets.create()), + new ActionRowBuilder<ButtonBuilder>().addComponents( + TicketButtonPresets.newTicketButton(), + ), ); await channel.send({ flags: ["IsComponentsV2"], components: [container] });
@@ -35,6 +35,7 @@ interaction: Interaction,
error: unknown, fallbackMessage?: string, ) { + console.log("error", error); const context = getInteractionContext(interaction); if (error instanceof PublicError) return await errorMessage(interaction, error.message);
@@ -4,7 +4,11 @@ type Flags = InteractionReplyOptions["flags"];
export function errorMessage(interaction: BaseInteraction, message: string, ephemeral = true) { const flags: Flags = ephemeral ? ["Ephemeral"] : []; - if (!interaction.isCommand()) return; + if (interaction.isButton()) { + return interaction.reply({ content: message, flags }); + } else if (!interaction.isCommand()) { + return; + } if (interaction.deferred) return interaction.editReply({ content: message }); if (!interaction.replied) return interaction.reply({ content: message, flags });
@@ -1,7 +1,12 @@
import { HaltExecution } from "@purrkit/router"; import { kitten } from "@/client"; -import { upsertGuild } from "@stealth-developers/db"; -import { upsertUser } from "@/lib"; +import { + getTicketByChannelId, + getTicketByGuild, + getTicketById, + upsertGuild, +} from "@stealth-developers/db"; +import { getTextChannel, upsertUser } from "@/lib"; const base = kitten.builder();@@ -36,3 +41,41 @@ throw new HaltExecution("Failed to find your guild in the database. Please try again.");
return { guild }; }); + +export const useGetTicketData = useGetData.use(async (interaction, { guild }) => { + if (interaction.isChatInputCommand()) { + const ticketId = interaction.options.getNumber("ticket", false); + if (ticketId && guild) { + const ticket = await getTicketByGuild(guild.id, ticketId); + if (!ticket) return {}; + return { ticket }; + } + } + + if (interaction.isButton()) { + const customId = interaction.customId; + if (customId.includes("ticket:")) { + const ticketId = customId.split(":")[1]; + if (ticketId) { + const ticket = await getTicketById(ticketId); + if (!ticket) return {}; + + if (!ticket.channelId) return { ticket }; + const ticketChannel = await getTextChannel(ticket.channelId, true); + if (!ticketChannel) return { ticket }; + + return { ticket, ticketChannel }; + } + } + } + + if (!interaction.channel) return {}; + + const ticket = await getTicketByChannelId(interaction.channel.id); + if (!ticket) return {}; + + if (!ticket.channelId) return { ticket }; + const ticketChannel = await getTextChannel(ticket.channelId, true); + + return { ticket, ticketChannel }; +});
@@ -320,7 +320,7 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="], + "@types/node": ["@types/node@22.20.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],@@ -354,7 +354,7 @@ "drizzle-kit": ["drizzle-kit@1.0.0-rc.3", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-lwGIi6GTlEYzrtac1Szecns+zrAxjaOoNysfBCKRQOXQE7nMJ8IU2I2S6ek3FHEm1N30dQb5YrTdqV7zNH6n7A=="],
"drizzle-orm": ["drizzle-orm@1.0.0-rc.3", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql-pg": ">=4.0.0-beta.58 || >=4.0.0", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.58 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-akZOa5UxapFbdBG8IDkfBRpSZJpMHaOJtGgp7oi1oHaiU8S3KN92waHo2l5aRuv1D9tMGYpv3BQFOsiGcNjLTQ=="], - "elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="], + "elysia": ["elysia@1.4.29", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-GwMRGGwSdjfPt+w3LA0fqTuYJtS8uVRJicvoar98/HrO5qdFKDc9CwjIb6Kja+v39lkY+58hr2JvdR9jQzlUuA=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],@@ -527,6 +527,8 @@
"@sapphire/framework/@sapphire/discord.js-utilities": ["@sapphire/discord.js-utilities@7.3.3", "", { "dependencies": { "@sapphire/discord-utilities": "^3.5.0", "@sapphire/duration": "^1.2.0", "@sapphire/utilities": "^3.18.2", "tslib": "^2.8.1" } }, "sha512-WDj+zjWgNCUSvzYDD0wY3TVeTUseHq0Nhk0wVWxSDjY8z2gFEVcpY7wF8/fbTDWP44LUG5sUQ4haIrIj2OjmkQ=="], "@sapphire/pieces/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@stealth-developers/bot/@purrkit/router": ["@purrkit/router@1.3.0", "", { "peerDependencies": { "discord.js": "^14.26.4", "typescript": "^5" } }, "sha512-exP3od2+Stw86Z8sA/8Oetk/u/cNNU7zu2K64eOavC1qpqZZ1+x/++egLXZ1PZnDy9+QSQrv1/lL6TkUyqGXqQ=="], "@stealth-developers/bot/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="],