Skip to content

Commit 402ac95

Browse files
feat: remove roles if no longer role menu access granted
1 parent f7d022c commit 402ac95

File tree

7 files changed

+113
-13
lines changed

7 files changed

+113
-13
lines changed

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.3.3/schema.json",
33
"vcs": {
44
"enabled": true,
55
"clientKind": "git",

src/deploy-commands.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ const commands = Array.from(commandManager.getCommands().values()).map(
1010
(command) => command.data.toJSON(),
1111
);
1212

13-
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN!);
13+
const token = process.env.DISCORD_TOKEN;
14+
if (!token) {
15+
throw new Error("DISCORD_TOKEN is not set");
16+
}
17+
18+
const rest = new REST({ version: "10" }).setToken(token);
1419

1520
(async () => {
1621
try {

src/events/ready.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ChannelType,
55
type Client,
66
EmbedBuilder,
7+
PermissionsBitField,
78
} from "discord.js";
89
import config from "../config/config";
910
import { botHasRecentMessages, getServerData } from "../utils/helpers";
@@ -92,6 +93,74 @@ export const onReady = async (client: Client) => {
9293
await sendRoleMenu(roleChannel);
9394
}
9495

96+
async function cleanRoleMenuRoles() {
97+
cronLog("CleanRoleMenuRoles", "Fetching guild");
98+
const guild = await client.guilds.fetch(config.guildId);
99+
if (!guild) {
100+
cronLog("CleanRoleMenuRoles", "Guild not found, skipping");
101+
return;
102+
}
103+
104+
try {
105+
await guild.members.fetch();
106+
} catch (error) {
107+
console.error(
108+
"[Cron][CleanRoleMenuRoles] Failed to fetch guild members:",
109+
error,
110+
);
111+
return;
112+
}
113+
114+
const memberIdsWithMenuRoles = new Set<string>();
115+
116+
for (const roleId of config.roleMenuRoleIds) {
117+
try {
118+
const role =
119+
guild.roles.cache.get(roleId) ?? (await guild.roles.fetch(roleId));
120+
if (!role) {
121+
cronLog("CleanRoleMenuRoles", `Role ${roleId} not found, skipping`);
122+
continue;
123+
}
124+
125+
role.members.forEach((member) => {
126+
memberIdsWithMenuRoles.add(member.id);
127+
});
128+
} catch (error) {
129+
console.error(
130+
`[Cron][CleanRoleMenuRoles] Failed to inspect role ${roleId}:`,
131+
error,
132+
);
133+
}
134+
}
135+
136+
let removedCount = 0;
137+
for (const memberId of memberIdsWithMenuRoles) {
138+
const member = guild.members.cache.get(memberId);
139+
if (!member) continue;
140+
141+
const hasAccess =
142+
member.roles.cache.has(config.roleMenuRequiredRoleId) ||
143+
member.permissions.has(PermissionsBitField.Flags.Administrator);
144+
145+
if (hasAccess) continue;
146+
147+
try {
148+
await member.roles.remove(config.roleMenuRoleIds);
149+
removedCount += 1;
150+
} catch (error) {
151+
console.error(
152+
`[Cron][CleanRoleMenuRoles] Failed to remove role menu roles from ${member.id}:`,
153+
error,
154+
);
155+
}
156+
}
157+
158+
cronLog(
159+
"CleanRoleMenuRoles",
160+
`Removed role menu roles from ${removedCount} member(s)`,
161+
);
162+
}
163+
95164
async function sendReactionRoleMenus() {
96165
const reactionRoleChannel = await client.channels.fetch(
97166
config.reactionRoleMenuId,
@@ -181,6 +250,13 @@ Select your notifications.
181250

182251
await updateStatus();
183252
new cron.CronJob("*/5 * * * *", updateStatus, null, true, "Europe/Berlin");
253+
new cron.CronJob(
254+
"*/5 * * * *",
255+
cleanRoleMenuRoles,
256+
null,
257+
true,
258+
"Europe/Berlin",
259+
);
184260

185261
new cron.CronJob("0 10 * * *", sendReminder, null, true, "Europe/Berlin");
186262

@@ -202,6 +278,7 @@ Select your notifications.
202278
);
203279

204280
void sendRoleMenuMsg();
281+
void cleanRoleMenuRoles();
205282
void sendReactionRoleMenus();
206283
void runSync();
207284
};

src/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@ async function initializeBot() {
4444
console.log("Loading commands...");
4545
await commandManager.loadCommands();
4646

47-
const rest = new REST({ version: "10" }).setToken(
48-
process.env.DISCORD_TOKEN!,
49-
);
47+
const token = process.env.DISCORD_TOKEN;
48+
if (!token) {
49+
throw new Error("DISCORD_TOKEN is not set");
50+
}
51+
52+
const rest = new REST({ version: "10" }).setToken(token);
5053
const commands = commandManager.getCommandsJSON();
5154
console.log(
5255
`Registering ${commands.length} commands to guild ${config.guildId}...`,
@@ -57,7 +60,7 @@ async function initializeBot() {
5760
);
5861

5962
console.log("Initializing bot...");
60-
await client.login(process.env.DISCORD_TOKEN!);
63+
await client.login(token);
6164

6265
console.log(
6366
`Bot initialized and ready to serve in guild: ${config.guildId}`,

src/types/command.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@ import type {
22
ButtonInteraction,
33
ChatInputCommandInteraction,
44
ModalSubmitInteraction,
5+
SlashCommandBuilder,
6+
SlashCommandOptionsOnlyBuilder,
7+
SlashCommandSubcommandsOnlyBuilder,
58
} from "discord.js";
69

710
export interface Command {
8-
data: any;
11+
data:
12+
| SlashCommandBuilder
13+
| SlashCommandSubcommandsOnlyBuilder
14+
| SlashCommandOptionsOnlyBuilder;
915
cooldown?: number;
1016
admin?: boolean;
1117
execute: (interaction: ChatInputCommandInteraction) => Promise<void>;

src/types/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import type {
22
ChatInputCommandInteraction,
33
SlashCommandBuilder,
4+
SlashCommandOptionsOnlyBuilder,
5+
SlashCommandSubcommandsOnlyBuilder,
46
} from "discord.js";
57

68
export interface Command {
7-
data: SlashCommandBuilder;
9+
data:
10+
| SlashCommandBuilder
11+
| SlashCommandSubcommandsOnlyBuilder
12+
| SlashCommandOptionsOnlyBuilder;
813
execute: (interaction: ChatInputCommandInteraction) => Promise<void>;
914
}
1015

1116
export interface Event {
1217
name: string;
13-
execute: (...args: any[]) => void;
18+
execute: (...args: unknown[]) => void;
1419
}

src/utils/commandManager.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,19 @@ export class CommandManager {
5353
userId: string,
5454
cooldownSeconds: number,
5555
): number {
56-
if (!this.cooldowns.has(commandName)) {
57-
this.cooldowns.set(commandName, new Collection());
56+
let timestamps = this.cooldowns.get(commandName);
57+
if (!timestamps) {
58+
timestamps = new Collection();
59+
this.cooldowns.set(commandName, timestamps);
5860
}
5961

6062
const now = Date.now();
61-
const timestamps = this.cooldowns.get(commandName)!;
6263

6364
if (timestamps.has(userId)) {
64-
const expirationTime = timestamps.get(userId)!;
65+
const expirationTime = timestamps.get(userId);
66+
if (!expirationTime) {
67+
return 0;
68+
}
6569
if (now < expirationTime) {
6670
return Math.ceil((expirationTime - now) / 1000);
6771
}

0 commit comments

Comments
 (0)