bot: adding, removing, listing participants
vi did:web:vt3e.cat
Sun, 28 Jun 2026 03:27:48 +0100
4 files changed,
147 insertions(+),
8 deletions(-)
M
apps/bot/src/feats/tickets/README.md
→
apps/bot/src/feats/tickets/README.md
@@ -1,4 +1,4 @@
-- [ ] /ticket +- [x] /ticket - [x] new - [x] for - [x] view [ticketId]@@ -10,10 +10,10 @@ - [x] unclaim
- [x] comment [ticketId?] - [x] edit [ticketId?] - [x] clean - /ticket delete - - [ ] participants - - [ ] add - - [ ] remove - - [ ] list + - [x] participants + - [x] add + - [x] remove + - [x] list - [x] manage - [x] list - [x] export@@ -27,8 +27,8 @@ - [x] prompt
- [x] greeting - [x] send-prompt -- [ ] other ticket stuff +- [x] other ticket stuff - [x] permissions - - [ ] add participants + - [x] add participants - [x] log messages - [x] private thread too
M
apps/bot/src/feats/tickets/actions/assignment.ts
→
apps/bot/src/feats/tickets/actions/assignment.ts
@@ -122,7 +122,7 @@ const botActor = getGuildActor(guildId);
if (!botActor) throw new PublicError("Failed to close ticket: Bot actor not found."); await addTicketParticipant({ - actorId: botActor.id, + actorId: data.actor.id, ticketId: data.ticketId, role: data.role, reason: data.reason,
M
apps/bot/src/feats/tickets/actions/lifecycle.ts
→
apps/bot/src/feats/tickets/actions/lifecycle.ts
@@ -285,6 +285,8 @@ role: "subject",
reason: "subject of ticket", }); + console.log(subjectUser); + const channel = setupResult.unwrap(); return { channelId: channel.id, publicId }; }
M
apps/bot/src/feats/tickets/commands.ts
→
apps/bot/src/feats/tickets/commands.ts
@@ -7,8 +7,12 @@ getTicketsByUser,
getTicketsByUserGDPR, purgeTicketData, purgeUserTicketData, + removeTicketParticipant, + upsertActor, } from "@stealth-developers/db"; import { ActionRowBuilder, ButtonBuilder, ContainerBuilder, TextDisplayBuilder } from "discord.js"; +import { ChannelType } from "discord.js"; +import { OverwriteType } from "discord.js"; import { PublicError, errorMessage, getTextChannel, handleError, logger } from "@/lib"; import { useGetTicketData } from "@/middleware";@@ -256,6 +260,139 @@ return errorMessage(interaction, "Failed to find ticket");
const result = await Result.fromAsync(actions.cleanTicket(ctx)); if (result.isErr()) return errorMessage(interaction, "Failed to delete ticket channel"); + }, + }); + }, +); + +ticketCommand.group( + "participants", + { + description: "Manage your ticket data", + }, + (group) => { + group.subcommand("list", { + description: "List participants of the ticket", + options: { + ticket: option.number("The ticket ID", { required: true }), + }, + async run(interaction, _, { ticket }) { + if (!ticket) return errorMessage(interaction, "No ticket found"); + + const { participants } = ticket; + if (participants.length === 0) return errorMessage(interaction, "No participants found"); + + const container = new ContainerBuilder(); + for (const participant of participants) { + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `<@${participant.actor?.discordId}> (${participant.role}) - ${participant.reason ?? "No reason provided"}`, + ), + ); + } + + return interaction.reply({ + components: [container], + flags: ["Ephemeral", "IsComponentsV2"], + allowedMentions: { parse: [] }, + }); + }, + }); + + group.subcommand("add", { + description: "Add a participant to the ticket", + options: { + user: option.user("The user to add", { required: true }), + reason: option.string("The reason for adding the user", { required: false }), + }, + async run(interaction, args, { actor, ticket, ticketChannel }) { + if (!actor?.moderator) { + return errorMessage(interaction, "You must be a moderator to run this command"); + } else if (!ticket) { + return errorMessage(interaction, "You must be in a ticket to run this command"); + } else if (!interaction.guild) { + return errorMessage(interaction, "This command can only be used in a guild"); + } + + const participant = await upsertActor({ + guild: { + discordId: interaction.guild.id, + name: interaction.guild.name, + icon: interaction.guild.icon, + }, + user: { + discordId: args.user.id, + username: args.user.username, + displayName: args.user.displayName, + avatar: args.user.avatar, + }, + moderator: false, + }); + 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 interaction.reply({ + content: `Added participant <@${args.user.id}> to the ticket - ${args.reason ?? "no reason provided"}`, + }); + + if (ticketChannel?.type === ChannelType.GuildText) { + ticketChannel.edit({ + permissionOverwrites: [ + { + id: args.user.id, + type: OverwriteType.Member, + allow: ["ViewChannel", "SendVoiceMessages", "AttachFiles"], + }, + ], + }); + } + }, + }); + + group.subcommand("remove", { + description: "Remove a participant from the ticket", + options: { + user: option.user("The user to add", { required: true }), + }, + async run(interaction, args, { actor, ticket, ticketChannel }) { + if (!actor?.moderator) { + return errorMessage(interaction, "You must be a moderator to run this command"); + } else if (!ticket) { + return errorMessage(interaction, "You must be in a ticket to run this command"); + } else if (!interaction.guild) { + return errorMessage(interaction, "This command can only be used in a guild"); + } + + const { participants } = ticket; + if (!participants) return errorMessage(interaction, "No participants in the ticket"); + + const participant = participants.find((p) => p.actor?.discordId === args.user.id); + if (!participant) return errorMessage(interaction, "Participant not found"); + if (participant.role === "subject") + return errorMessage(interaction, "You cannot remove the subject from the ticket"); + + await removeTicketParticipant(ticket.id, participant.id); + await interaction.reply({ content: `<@${args.user.id}> was removed from the ticket` }); + + if (ticketChannel?.type === ChannelType.GuildText) { + ticketChannel.edit({ + permissionOverwrites: [ + { + id: args.user.id, + type: OverwriteType.Member, + deny: ["ViewChannel", "SendVoiceMessages", "AttachFiles"], + }, + ], + }); + } }, }); },