Skip to content

Commit d378c33

Browse files
vcarlCopilot
andauthored
Make tickets more configurable (#122)
Co-authored-by: Copilot <[email protected]>
1 parent 4802f21 commit d378c33

File tree

3 files changed

+130
-25
lines changed

3 files changed

+130
-25
lines changed

app/commands/setupTickets.ts

+109-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ChatInputCommandInteraction } from "discord.js";
1+
import { TextChannel, type ChatInputCommandInteraction } from "discord.js";
22
import {
33
ChannelType,
44
ComponentType,
@@ -25,13 +25,38 @@ import type {
2525
SlashCommand,
2626
} from "#~/helpers/discord";
2727
import { quoteMessageContent } from "#~/helpers/discord";
28+
import db from "#~/db.server.js";
2829

2930
const rest = new REST({ version: "10" }).setToken(discordToken);
3031

32+
const DEFAULT_BUTTON_TEXT = "Open a private ticket with the moderators";
33+
3134
export default [
3235
{
3336
command: new SlashCommandBuilder()
3437
.setName("tickets-channel")
38+
.addRoleOption((o) => {
39+
o.setName("role");
40+
o.setDescription(
41+
"Which role (if any) should be pinged when a ticket is created?",
42+
);
43+
o.setRequired(false);
44+
return o;
45+
})
46+
.addStringOption((o) => {
47+
o.setName("button-text");
48+
o.setDescription(
49+
`What should the button say? If left blank, it will say "${DEFAULT_BUTTON_TEXT}"`,
50+
);
51+
return o;
52+
})
53+
.addChannelOption((o) => {
54+
o.setName("channel");
55+
o.setDescription(
56+
"Which channel (if not this one) should tickets be created in?",
57+
);
58+
return o;
59+
})
3560
.setDescription(
3661
"Set up a new button for creating private tickets with moderators",
3762
)
@@ -42,21 +67,56 @@ export default [
4267
handler: async (interaction: ChatInputCommandInteraction) => {
4368
if (!interaction.guild) throw new Error("Interaction has no guild");
4469

45-
await interaction.reply({
46-
components: [
47-
{
48-
type: ComponentType.ActionRow,
49-
components: [
50-
{
51-
type: ComponentType.Button,
52-
label: "Open a private ticket with the moderators",
53-
style: ButtonStyle.Primary,
54-
customId: "open-ticket",
55-
},
56-
],
57-
},
58-
],
59-
});
70+
const pingableRole = interaction.options.getRole("role");
71+
const ticketChannel = interaction.options.getChannel("channel");
72+
const buttonText =
73+
interaction.options.getString("button-text") || DEFAULT_BUTTON_TEXT;
74+
75+
if (ticketChannel && ticketChannel.type !== ChannelType.GuildText) {
76+
await interaction.reply({
77+
content: `The channel configured must be a text channel! Tickets will be created as private threads.`,
78+
});
79+
return;
80+
}
81+
82+
try {
83+
const interactionResponse = await interaction.reply({
84+
components: [
85+
{
86+
type: ComponentType.ActionRow,
87+
components: [
88+
{
89+
type: ComponentType.Button,
90+
label: buttonText,
91+
style: ButtonStyle.Primary,
92+
customId: "open-ticket",
93+
},
94+
],
95+
},
96+
],
97+
});
98+
const producedMessage = await interactionResponse.fetch();
99+
100+
let roleId = pingableRole?.id;
101+
if (!roleId) {
102+
const { [SETTINGS.moderator]: mod } = await fetchSettings(
103+
interaction.guild,
104+
[SETTINGS.moderator, SETTINGS.modLog],
105+
);
106+
roleId = mod;
107+
}
108+
109+
await db
110+
.insertInto("tickets_config")
111+
.values({
112+
message_id: producedMessage.id,
113+
channel_id: ticketChannel?.id,
114+
role_id: roleId,
115+
})
116+
.execute();
117+
} catch (e) {
118+
console.error(`error:`, e);
119+
}
60120
},
61121
} as SlashCommand,
62122
{
@@ -87,7 +147,8 @@ export default [
87147
!interaction.channel ||
88148
interaction.channel.type !== ChannelType.GuildText ||
89149
!interaction.user ||
90-
!interaction.guild
150+
!interaction.guild ||
151+
!interaction.message
91152
) {
92153
await interaction.reply({
93154
content: "Something went wrong while creating a ticket",
@@ -98,19 +159,43 @@ export default [
98159
const { channel, fields, user } = interaction;
99160
const concern = fields.getField("concern").value;
100161

101-
const { [SETTINGS.moderator]: mod } = await fetchSettings(
102-
interaction.guild,
103-
[SETTINGS.moderator, SETTINGS.modLog],
104-
);
105-
const thread = await channel.threads.create({
162+
let config = await db
163+
.selectFrom("tickets_config")
164+
.selectAll()
165+
.where("message_id", "=", interaction.message.id)
166+
.executeTakeFirst();
167+
// If there's no config, that means that the button was set up before the db was set up. Add one with default values
168+
if (!config) {
169+
const { [SETTINGS.moderator]: mod } = await fetchSettings(
170+
interaction.guild,
171+
[SETTINGS.moderator, SETTINGS.modLog],
172+
);
173+
config = await db
174+
.insertInto("tickets_config")
175+
.returningAll()
176+
.values({ message_id: interaction.message.id, role_id: mod })
177+
.executeTakeFirst();
178+
if (!config) {
179+
throw new Error("Something went wrong while fixing tickets config");
180+
}
181+
}
182+
183+
const ticketsChannel = config.channel_id
184+
? ((await interaction.guild.channels.fetch(
185+
config.channel_id,
186+
)) as TextChannel) || channel
187+
: channel;
188+
189+
const thread = await ticketsChannel.threads.create({
106190
name: `${user.username}${format(new Date(), "PP kk:mmX")}`,
107191
autoArchiveDuration: 60 * 24 * 7,
108192
type: ChannelType.PrivateThread,
109193
});
110194
await thread.send({
111-
content: `<@${user.id}>, this is a private space only visible to you and the <@&${mod}> role.`,
195+
content: `<@${user.id}>, this is a private space only visible to you and the <@&${config.role_id}> role.`,
112196
});
113-
await thread.send(quoteMessageContent(concern));
197+
await thread.send(`${user.displayName} said:
198+
${quoteMessageContent(concern)}`);
114199
await thread.send({
115200
content: "When you’ve finished, please close the ticket.",
116201
components: [
@@ -143,7 +228,6 @@ export default [
143228
command: { type: InteractionType.MessageComponent, name: "close-ticket" },
144229
handler: async (interaction) => {
145230
const [, ticketOpenerUserId, feedback] = interaction.customId.split("||");
146-
console.log(ticketOpenerUserId, feedback, interaction.customId);
147231
const threadId = interaction.channelId;
148232
if (!interaction.member || !interaction.guild) {
149233
console.error(

app/db.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export interface Sessions {
2929
id: string | null;
3030
}
3131

32+
export interface TicketsConfig {
33+
channel_id: string | null;
34+
message_id: string;
35+
role_id: string;
36+
}
37+
3238
export interface Users {
3339
authProvider: Generated<string | null>;
3440
email: string | null;
@@ -40,5 +46,6 @@ export interface DB {
4046
guilds: Guilds;
4147
message_stats: MessageStats;
4248
sessions: Sessions;
49+
tickets_config: TicketsConfig;
4350
users: Users;
4451
}

migrations/20250325193821_tickets.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { Kysely } from "kysely";
2+
3+
export async function up(db: Kysely<any>): Promise<void> {
4+
return db.schema
5+
.createTable("tickets_config")
6+
.addColumn("message_id", "text", (c) => c.primaryKey().notNull())
7+
.addColumn("channel_id", "text")
8+
.addColumn("role_id", "text", (c) => c.notNull())
9+
.execute();
10+
}
11+
12+
export async function down(db: Kysely<any>): Promise<void> {
13+
return db.schema.dropTable("tickets_config").execute();
14+
}

0 commit comments

Comments
 (0)