Skip to content

Commit 157ac72

Browse files
committed
make it so custom roles delete after 3 days, fix booster logic so it actually detects if youre already a server booster and removed role id env
1 parent e772ff1 commit 157ac72

17 files changed

Lines changed: 414 additions & 140 deletions

.env.example

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ DEBUG=true
33
BOT_TOKEN=
44
CLIENT_ID=
55
GUILD_ID=
6-
BOOSTER_ROLE_ID=
76
GREET_CHANNEL_ID=
87
LOG_CHANNEL_ID=
98
DATABASE_URL=

prisma/schema.prisma

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,14 @@ model Booster {
4242
}
4343

4444
model CustomRole {
45-
id String @id @default(cuid())
46-
boosterId String @unique
47-
booster Booster @relation(fields: [boosterId], references: [id], onDelete: Cascade)
48-
discordRoleId String @unique
49-
createdAt DateTime @default(now())
50-
updatedAt DateTime @updatedAt
45+
id String @id @default(cuid())
46+
boosterId String @unique
47+
booster Booster @relation(fields: [boosterId], references: [id], onDelete: Cascade)
48+
discordRoleId String @unique
49+
name String @default("")
50+
deleteScheduledAt DateTime?
51+
createdAt DateTime @default(now())
52+
updatedAt DateTime @updatedAt
53+
54+
@@index([deleteScheduledAt])
5155
}

src/commands/booster.ts

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import {
33
ChatInputCommandInteraction,
44
EmbedBuilder,
55
PermissionFlagsBits,
6+
ContainerBuilder,
7+
TextDisplayBuilder,
8+
MessageFlags,
69
} from "discord.js";
710
import { Command } from "../libs/loadCommands.js";
811
import {
@@ -13,7 +16,6 @@ import {
1316
getActiveBoosters,
1417
getTotalBoosts,
1518
registerBoost,
16-
ensureGuild,
1719
} from "../services/boosterService.js";
1820

1921
const boosterCommand: Command = {
@@ -66,21 +68,59 @@ const boosterCommand: Command = {
6668
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
6769
const sub = interaction.options.getSubcommand();
6870

69-
// All subcommands need a guild — bail early if missing
7071
const discordGuild = interaction.guild;
7172
if (!discordGuild) {
72-
await interaction.reply({ content: "This command can only be used in a server.", ephemeral: true });
73+
await interaction.reply({
74+
content: "This command can only be used in a server.",
75+
flags: MessageFlags.Ephemeral,
76+
});
7377
return;
7478
}
7579

76-
await interaction.deferReply({ ephemeral: true });
80+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
7781

7882
if (sub === "check") {
7983
const user = interaction.options.getUser("user", true);
8084
const result = await getBooster(user.id, interaction);
85+
const member = await discordGuild.members.fetch(user.id).catch(() => null);
86+
const isBoostingServer = member?.premiumSince !== null;
87+
const avatarUrl =
88+
member?.displayAvatarURL({ size: 256 }) ?? user.displayAvatarURL({ size: 256 });
8189

82-
if (!result?.success || !result.data) {
83-
await interaction.editReply({ content: `No booster record found for ${user.tag}.` });
90+
if (!result?.success) {
91+
await interaction.editReply({ content: "Could not load booster info for this server." });
92+
return;
93+
}
94+
95+
if (!result.data) {
96+
if (isBoostingServer && member && member.premiumSince) {
97+
const discordBoost = `🟢 Boosting since <t:${Math.floor(member.premiumSince.getTime() / 1000)}:D>`;
98+
const embed = new EmbedBuilder()
99+
.setColor(0xff73fa)
100+
.setTitle(`Here is the Booster Information for ${user.id}!`)
101+
.setThumbnail(avatarUrl)
102+
.addFields(
103+
{ name: "Status", value: "🟢 Active (Nitro Boost)", inline: true },
104+
{ name: "Discord boost", value: discordBoost, inline: true },
105+
{ name: "Custom Role", value: "None", inline: true }
106+
)
107+
.setTimestamp();
108+
109+
await interaction.editReply({ embeds: [embed] });
110+
return;
111+
}
112+
113+
const container = new ContainerBuilder()
114+
.setAccentColor(0xe642a4)
115+
.addTextDisplayComponents(
116+
new TextDisplayBuilder().setContent("**Uh oh!**"),
117+
new TextDisplayBuilder().setContent(`It looks like ${user} is not a Booster.`)
118+
);
119+
120+
await interaction.editReply({
121+
flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2,
122+
components: [container],
123+
});
84124
return;
85125
}
86126

@@ -89,16 +129,14 @@ const boosterCommand: Command = {
89129
const embed = new EmbedBuilder()
90130
.setColor(booster.active ? 0xf47fff : 0x99aab5)
91131
.setTitle(`Booster Info: ${user.username}`)
132+
.setThumbnail(avatarUrl)
92133
.addFields(
93134
{ name: "Status", value: booster.active ? "🟢 Active" : "🔴 Inactive", inline: true },
94-
{ name: "Boost Count", value: String(booster.boostCounts ?? 0), inline: true },
95135
{
96136
name: "Custom Role",
97137
value: booster.customRole ? `<@&${booster.customRole.discordRoleId}>` : "None",
98138
inline: true,
99-
},
100-
{ name: "First Boosted", value: `<t:${Math.floor(booster.boostedAt.getTime() / 1000)}:D>`, inline: true },
101-
{ name: "Last Updated", value: `<t:${Math.floor(booster.updatedAt.getTime() / 1000)}:R>`, inline: true }
139+
}
102140
)
103141
.setTimestamp();
104142

@@ -110,6 +148,14 @@ const boosterCommand: Command = {
110148
const user = interaction.options.getUser("user", true);
111149
const amount = interaction.options.getInteger("amount", true);
112150

151+
const targetMember = await discordGuild.members.fetch(user.id).catch(() => null);
152+
if (!targetMember?.premiumSince) {
153+
await interaction.editReply({
154+
content: `${user} is not currently boosting this server with Nitro, so boosts can't be added for them.`,
155+
});
156+
return;
157+
}
158+
113159
await registerBoost(user.id, discordGuild.id, discordGuild.name, discordGuild.iconURL());
114160

115161
const updated = await addBoostCount(user.id, discordGuild.id, amount);
@@ -118,8 +164,22 @@ const boosterCommand: Command = {
118164
return;
119165
}
120166

167+
const boostWord = amount === 1 ? "boost" : "boosts";
168+
const countWord = updated.boostCounts === 1 ? "boost" : "boosts";
169+
const container = new ContainerBuilder()
170+
.setAccentColor(0xe642a4)
171+
.addTextDisplayComponents(
172+
new TextDisplayBuilder().setContent(
173+
`**Boost successfully added!**`
174+
),
175+
new TextDisplayBuilder().setContent(
176+
`We've successfully added ${amount} ${boostWord} to ${user}'s profile.`
177+
)
178+
);
179+
121180
await interaction.editReply({
122-
content: `Added **${amount}** boost(s) to ${user.tag}. New count: **${updated.boostCounts}**.`,
181+
flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2,
182+
components: [container],
123183
});
124184
return;
125185
}

src/commands/reload.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
PermissionFlagsBits,
55
Client,
66
Collection,
7+
MessageFlags,
78
} from "discord.js";
89
import { Command } from "../libs/loadCommands.js";
910
import { loadCommands } from "../libs/loadCommands.js";
@@ -16,7 +17,7 @@ const reloadCommand: Command = {
1617
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
1718

1819
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
19-
await interaction.deferReply({ ephemeral: true });
20+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
2021

2122
try {
2223
const config = loadVariables();

src/commands/role.ts

Lines changed: 86 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
import {
22
SlashCommandBuilder,
33
ChatInputCommandInteraction,
4-
PermissionFlagsBits,
54
ColorResolvable,
5+
ContainerBuilder,
6+
TextDisplayBuilder,
7+
MessageFlags,
68
} from "discord.js";
79
import { Command } from "../libs/loadCommands.js";
8-
import { getBooster } from "../services/boosterService.js";
10+
import { ensureBoosterWhileBoosting, getBooster } from "../services/boosterService.js";
911
import {
1012
createCustomRole,
1113
updateCustomRole,
1214
deleteCustomRole,
1315
} from "../services/roleService.js";
1416

17+
const ACCENT = 0xe642a4;
18+
19+
function componentsV2(lines: string[]) {
20+
const container = new ContainerBuilder()
21+
.setAccentColor(ACCENT)
22+
.addTextDisplayComponents(...lines.map((content) => new TextDisplayBuilder().setContent(content)));
23+
return {
24+
flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2,
25+
components: [container],
26+
};
27+
}
28+
1529
const roleCommand: Command = {
1630
data: new SlashCommandBuilder()
1731
.setName("role")
@@ -43,95 +57,126 @@ const roleCommand: Command = {
4357
),
4458

4559
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
46-
const record = await getBooster(interaction.user.id, interaction);
60+
const guild = interaction.guild;
61+
if (!guild) {
62+
await interaction.reply(componentsV2(["This command can only be used in a server."]));
63+
return;
64+
}
65+
66+
const member = await guild.members.fetch(interaction.user.id);
67+
const resolved = await getBooster(interaction.user.id, interaction);
4768

48-
if (!record?.data?.active) {
49-
await interaction.reply({
50-
content: "You must be an active booster to use this command.",
51-
ephemeral: true,
52-
});
69+
if (!resolved?.success) {
70+
await interaction.reply(
71+
componentsV2(["Could not load your guild data for this server."])
72+
);
5373
return;
5474
}
5575

76+
const nitroBoosting = member.premiumSince !== null;
77+
78+
if (!nitroBoosting) {
79+
await interaction.reply(
80+
componentsV2([
81+
"**Uh oh!**",
82+
"You must be boosting this server with Nitro to use this command.",
83+
])
84+
);
85+
return;
86+
}
87+
88+
const boosterRecord =
89+
resolved.data ??
90+
(await ensureBoosterWhileBoosting(
91+
interaction.user.id,
92+
guild.id,
93+
guild.name,
94+
guild.iconURL()
95+
));
96+
5697
const sub = interaction.options.getSubcommand();
57-
const guild = interaction.guild!;
58-
const member = await guild.members.fetch(interaction.user.id);
5998

6099
if (sub === "create") {
61-
if (record.data.customRole) {
62-
await interaction.reply({
63-
content: "You already have a custom role. Use /role edit to modify it.",
64-
ephemeral: true,
65-
});
100+
if (boosterRecord.customRole) {
101+
await interaction.reply(
102+
componentsV2([
103+
"You already have a custom role.",
104+
"Use `/role edit` to change it.",
105+
])
106+
);
66107
return;
67108
}
68109

69110
const name = interaction.options.getString("name", true);
70111
const color = interaction.options.getString("color", true);
71112

72113
if (!/^#[0-9a-fA-F]{6}$/.test(color)) {
73-
await interaction.reply({
74-
content: "Invalid color. Please use a hex color like #ff0000.",
75-
ephemeral: true,
76-
});
114+
await interaction.reply(
115+
componentsV2([
116+
"Invalid color.",
117+
"Use a hex color like `#ff0000`.",
118+
])
119+
);
77120
return;
78121
}
79122

80-
await interaction.deferReply({ ephemeral: true });
123+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
81124

82125
const role = await createCustomRole(guild, member, name, color as ColorResolvable);
83126

84-
await interaction.editReply(`Custom role ${role} created successfully.`);
127+
await interaction.editReply(
128+
componentsV2([`**Custom role created!**`, `${role} is ready to use.`])
129+
);
85130
return;
86131
}
87132

88133
if (sub === "edit") {
89-
if (!record.data.customRole?.discordRoleId) {
90-
await interaction.reply({
91-
content: "You do not have a custom role. Use /role create first.",
92-
ephemeral: true,
93-
});
134+
if (!boosterRecord.customRole?.discordRoleId) {
135+
await interaction.reply(
136+
componentsV2(["You don't have a custom role yet.", "Use `/role create` first."])
137+
);
94138
return;
95139
}
96140

97141
const name = interaction.options.getString("name") ?? undefined;
98142
const color = (interaction.options.getString("color") ?? undefined) as ColorResolvable | undefined;
99143

100144
if (color && !/^#[0-9a-fA-F]{6}$/.test(color as string)) {
101-
await interaction.reply({
102-
content: "Invalid color. Please use a hex color like #ff0000.",
103-
ephemeral: true,
104-
});
145+
await interaction.reply(
146+
componentsV2([
147+
"Invalid color.",
148+
"Use a hex color like `#ff0000`.",
149+
])
150+
);
105151
return;
106152
}
107153

108-
await interaction.deferReply({ ephemeral: true });
154+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
109155

110156
const role = await updateCustomRole(guild, interaction.user.id, name, color);
111157
if (!role) {
112-
await interaction.editReply("Failed to update role. It may have been deleted.");
158+
await interaction.editReply(
159+
componentsV2(["Couldn't update that role.", "It may have been deleted manually."])
160+
);
113161
return;
114162
}
115163

116-
await interaction.editReply(`Custom role updated successfully.`);
164+
await interaction.editReply(componentsV2(["**Custom role updated.**"]));
117165
return;
118166
}
119167

120168
if (sub === "delete") {
121-
if (!record.data.customRole?.discordRoleId) {
122-
await interaction.reply({
123-
content: "You do not have a custom role.",
124-
ephemeral: true,
125-
});
169+
if (!boosterRecord.customRole?.discordRoleId) {
170+
await interaction.reply(componentsV2(["You don't have a custom role to delete."]));
126171
return;
127172
}
128173

129-
await interaction.deferReply({ ephemeral: true });
174+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
130175
await deleteCustomRole(guild, interaction.user.id);
131-
await interaction.editReply("Custom role deleted.");
176+
await interaction.editReply(componentsV2(["**Custom role deleted.**"]));
132177
return;
133178
}
134179
},
135180
};
136181

137-
export default roleCommand;
182+
export default roleCommand;

0 commit comments

Comments
 (0)