Skip to content

Commit 642e7ff

Browse files
committed
feat: implement new repel command options + logging
1 parent 552cac6 commit 642e7ff

3 files changed

Lines changed: 184 additions & 72 deletions

File tree

.env.example

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,7 @@ ONBOARDING_CHANNEL=
4040
JOIN_LOG_CHANNEL=
4141
INTRO_CHANNEL=
4242
INTRO_ROLE=
43-
REPEL_ROLE_ID=MiniMod # The ID of the role that is used for MiniMods
44-
REPEL_DELETE_COUNT=2 # The number of messages to delete when using the repel command
43+
REPEL_ROLE_ID=1002411741776461844 # The ID of the role that is used for MiniMods
44+
REPEL_DEFAULT_DELETE_COUNT=20 # The number of messages to delete when using the repel command
45+
REPEL_LOG_CHANNEL_ID=1403558160144531589 # The channel where the repel command logs are sent
46+
REPEL_DEFAULT_TIMEOUT=6 # Default timeout for the repel command in HOURS

src/env.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export const { INTRO_CHANNEL } = process.env;
4444
export const { INTRO_ROLE } = process.env;
4545

4646
export const { REPEL_ROLE_ID } = process.env;
47-
export const REPEL_DELETE_COUNT =
48-
Number.parseInt(process.env.REPEL_DELETE_COUNT) || 2;
47+
export const REPEL_DEFAULT_DELETE_COUNT =
48+
Number.parseInt(process.env.REPEL_DEFAULT_DELETE_COUNT) || 20;
4949
export const { REPEL_LOG_CHANNEL_ID } = process.env;
50+
export const REPEL_DEFAULT_TIMEOUT =
51+
Number.parseInt(process.env.REPEL_DEFAULT_TIMEOUT) || 6;

src/v2/commands/repel/index.ts

Lines changed: 176 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
ApplicationCommandOptionType,
33
ChannelType,
4+
EmbedBuilder,
45
PermissionFlagsBits,
56
User,
67
type Client,
@@ -9,53 +10,113 @@ import {
910
type TextChannel,
1011
} from 'discord.js';
1112
import type { CommandDataWithHandler } from '../../../types';
12-
import { REPEL_DELETE_COUNT, REPEL_ROLE_ID } from '../../env';
13+
import {
14+
REPEL_DEFAULT_DELETE_COUNT,
15+
REPEL_ROLE_ID,
16+
REPEL_LOG_CHANNEL_ID,
17+
REPEL_DEFAULT_TIMEOUT,
18+
} from '../../env';
19+
import { DiscordAPIErrorCode } from '../../../enums';
20+
import { logEmbed } from '../../utils/channel-logger';
1321

14-
const TARGET_KEY = 'target';
15-
const MESSAGE_LINK_KEY = 'message_link';
22+
enum RepelCommandOptions {
23+
TARGET = 'target',
24+
MESSAGE_LINK = 'message_link',
25+
DELETE_COUNT = 'delete_count',
26+
TIMEOUT = 'timeout',
27+
REASON = 'reason',
28+
}
1629
const DAY = 24 * 60 * 60 * 1000;
17-
const TIMEOUT_DURATION = 6 * 60 * 60 * 1000; // 6 hours in milliseconds
1830

1931
const reply = (
2032
interaction: CommandInteraction,
2133
content: string,
2234
ephemeral = true,
2335
) => interaction.reply({ content, ephemeral });
2436

25-
const getTargetFromMessage = async (
26-
client: Client,
27-
guild: any,
28-
messageLink: string,
29-
) => {
30-
const match = messageLink.match(/(?:channels|@me)\/(?:(\d+)\/)?(\d+)\/(\d+)/);
31-
if (!match) throw new Error('Invalid message link format.');
32-
const messageId = match[3];
33-
const channelId = match[2];
34-
35-
const channel = channelId ? await client.channels.fetch(channelId) : null;
36-
if (channel?.type !== ChannelType.GuildText)
37-
throw new Error('Invalid channel for message link.');
38-
39-
const message = await (channel as TextChannel).messages.fetch(messageId);
40-
return await guild.members.fetch(message.author.id);
41-
};
42-
4337
export const repelInteraction: CommandDataWithHandler = {
4438
name: 'repel',
45-
description:
46-
'Remove recent messages and timeout a user (requires timeout permissions)',
39+
description: 'Remove recent messages and timeout a user',
4740
options: [
4841
{
49-
name: TARGET_KEY,
42+
name: RepelCommandOptions.TARGET,
5043
description: 'The user to repel',
5144
type: ApplicationCommandOptionType.User,
52-
required: false,
45+
required: true,
5346
},
5447
{
55-
name: MESSAGE_LINK_KEY,
56-
description: 'Message link to identify the user to repel',
48+
name: RepelCommandOptions.REASON,
49+
description: 'Reason for repelling the user',
5750
type: ApplicationCommandOptionType.String,
51+
required: true,
52+
},
53+
{
54+
name: RepelCommandOptions.DELETE_COUNT,
55+
description: `Number of messages to delete from the user (default: ${REPEL_DEFAULT_DELETE_COUNT})`,
56+
type: ApplicationCommandOptionType.Integer,
5857
required: false,
58+
choices: [
59+
{
60+
name: '5 messages',
61+
value: 5,
62+
},
63+
{
64+
name: '10 messages',
65+
value: 10,
66+
},
67+
{
68+
name: '20 messages',
69+
value: 20,
70+
},
71+
{
72+
name: '50 messages',
73+
value: 50,
74+
},
75+
{
76+
name: '100 messages',
77+
value: 100,
78+
},
79+
{
80+
name: '200 messages',
81+
value: 200,
82+
},
83+
],
84+
},
85+
{
86+
name: RepelCommandOptions.TIMEOUT,
87+
description: `Timeout duration in hours (default: ${REPEL_DEFAULT_TIMEOUT} hours)`,
88+
type: ApplicationCommandOptionType.Integer,
89+
required: false,
90+
choices: [
91+
{
92+
name: 'No timeout',
93+
value: 0,
94+
},
95+
{
96+
name: '1 hour',
97+
value: 1,
98+
},
99+
{
100+
name: '2 hours',
101+
value: 2,
102+
},
103+
{
104+
name: '3 hours',
105+
value: 3,
106+
},
107+
{
108+
name: '6 hours',
109+
value: 6,
110+
},
111+
{
112+
name: '12 hours',
113+
value: 12,
114+
},
115+
{
116+
name: '1 day',
117+
value: 24,
118+
},
119+
],
59120
},
60121
],
61122

@@ -97,38 +158,35 @@ export const repelInteraction: CommandDataWithHandler = {
97158
return;
98159
}
99160

100-
const targetUser = interaction.options.get(TARGET_KEY, false)?.user as
101-
| User
102-
| undefined;
103-
const messageLink = interaction.options.get(MESSAGE_LINK_KEY, false)
104-
?.value as string | undefined;
161+
const targetUser = interaction.options.get(
162+
RepelCommandOptions.TARGET,
163+
false,
164+
)?.user as User;
165+
console.log('Target User:', targetUser);
105166

106-
if (!targetUser && !messageLink) {
107-
await reply(
108-
interaction,
109-
'You must specify either a user or a message link.',
110-
);
111-
}
167+
let targetGuildMember: GuildMember | null = null;
168+
let userNotInServer = false;
112169

113170
try {
114-
let targetMember: GuildMember;
115-
116-
if (targetUser) {
117-
targetMember = await interaction.guild.members.fetch(targetUser.id);
118-
} else if (messageLink) {
119-
targetMember = await getTargetFromMessage(
120-
client,
121-
interaction.guild,
122-
messageLink!,
123-
);
171+
targetGuildMember = await interaction.guild.members.fetch(targetUser.id);
172+
} catch (error: any) {
173+
if (
174+
error.code === DiscordAPIErrorCode.UnknownMember ||
175+
error.code === DiscordAPIErrorCode.UnknownUser
176+
) {
177+
userNotInServer = true;
178+
} else {
179+
throw error;
124180
}
181+
}
125182

126-
if (targetMember.id === member.id) {
183+
if (targetGuildMember !== null) {
184+
if (targetGuildMember.id === member.id) {
127185
await reply(interaction, 'You cannot repel yourself.');
128186
return;
129187
}
130188

131-
if (targetMember.roles.cache.has(repelRole.id)) {
189+
if (targetGuildMember.roles.cache.has(repelRole.id)) {
132190
await reply(
133191
interaction,
134192
`You cannot repel a user with the ${roleName} role.`,
@@ -139,13 +197,14 @@ export const repelInteraction: CommandDataWithHandler = {
139197
const botMember = await interaction.guild.members.fetch(client.user!.id);
140198
const isOwner = interaction.guild.ownerId === member.id;
141199

142-
if (targetMember.id === interaction.guild.ownerId) {
200+
if (targetGuildMember.id === interaction.guild.ownerId) {
143201
await reply(interaction, 'Cannot moderate the server owner.');
144202
}
145203

146204
if (
147205
!isOwner &&
148-
targetMember.roles.highest.position >= member.roles.highest.position
206+
targetGuildMember.roles.highest.position >=
207+
member.roles.highest.position
149208
) {
150209
await reply(
151210
interaction,
@@ -154,23 +213,35 @@ export const repelInteraction: CommandDataWithHandler = {
154213
}
155214

156215
if (
157-
targetMember.roles.highest.position >= botMember.roles.highest.position
216+
targetGuildMember.roles.highest.position >=
217+
botMember.roles.highest.position
158218
) {
159219
await reply(
160220
interaction,
161221
'I cannot moderate this user due to role hierarchy.',
162222
);
163223
}
224+
}
164225

165-
await interaction.deferReply({ ephemeral: true });
226+
const targetId = userNotInServer ? targetUser.id : targetGuildMember!.id;
227+
const targetTag = userNotInServer
228+
? targetUser.tag
229+
: targetGuildMember!.user.tag;
166230

231+
try {
232+
await interaction.deferReply({ ephemeral: true });
233+
const messagesToDelete =
234+
interaction.options.getInteger(
235+
RepelCommandOptions.DELETE_COUNT,
236+
false,
237+
) ?? REPEL_DEFAULT_DELETE_COUNT;
167238
let deletedCount = 0;
168239
const textChannels = interaction.guild.channels.cache.filter(
169240
ch => ch.type === ChannelType.GuildText,
170241
);
171242

172243
for (const [, channel] of textChannels) {
173-
if (deletedCount >= REPEL_DELETE_COUNT) break;
244+
if (deletedCount >= messagesToDelete) break;
174245

175246
try {
176247
const messages = await channel.messages.fetch({
@@ -179,12 +250,10 @@ export const repelInteraction: CommandDataWithHandler = {
179250
const userMessages = messages
180251
.filter(
181252
m =>
182-
m.author.id === targetMember.id &&
253+
m.author.id === targetId &&
183254
Date.now() - m.createdTimestamp < 14 * DAY,
184255
)
185-
.first(
186-
Math.min(REPEL_DELETE_COUNT - deletedCount, REPEL_DELETE_COUNT),
187-
);
256+
.first(Math.min(messagesToDelete - deletedCount, messagesToDelete));
188257
if (userMessages.length > 0) {
189258
userMessages.length === 1
190259
? await userMessages[0].delete()
@@ -194,23 +263,62 @@ export const repelInteraction: CommandDataWithHandler = {
194263
} catch {}
195264
}
196265

197-
const isUserTimedOut = targetMember.communicationDisabledUntilTimestamp
198-
? targetMember.communicationDisabledUntilTimestamp > Date.now()
199-
: false;
266+
const isUserTimedOut =
267+
targetGuildMember?.communicationDisabledUntilTimestamp
268+
? targetGuildMember.communicationDisabledUntilTimestamp > Date.now()
269+
: false;
200270

201-
if (!isUserTimedOut) {
202-
await targetMember.timeout(
203-
TIMEOUT_DURATION,
271+
const timeoutDurationInHours =
272+
interaction.options.getInteger(RepelCommandOptions.TIMEOUT, false) ??
273+
REPEL_DEFAULT_TIMEOUT;
274+
if (
275+
!isUserTimedOut &&
276+
timeoutDurationInHours > 0 &&
277+
targetGuildMember !== null
278+
) {
279+
await targetGuildMember.timeout(
280+
timeoutDurationInHours * 60 * 60 * 1000,
204281
`Repel command used by ${member.user.tag}`,
205282
);
206283
await interaction.editReply({
207-
content: `Successfully repelled ${targetMember.user.tag}. Removed ${deletedCount} messages and timed out for 6 hours.`,
284+
content: `Successfully repelled ${targetTag}. Removed ${deletedCount} messages and timed out for ${timeoutDurationInHours} hours.`,
208285
});
209286
} else {
210287
await interaction.editReply({
211-
content: `Successfully repelled ${targetMember.user.tag}. Removed ${deletedCount} messages.`,
288+
content: `Successfully repelled ${targetTag}. Removed ${deletedCount} messages.`,
212289
});
213290
}
291+
292+
const embed = new EmbedBuilder()
293+
.setTitle('Repel Action')
294+
.setDescription(
295+
`<@${targetId}> has been repelled by <@${member.id}> in <#${interaction.channelId}>.`,
296+
)
297+
.addFields(
298+
{
299+
name: 'Reason',
300+
value: interaction.options.getString(
301+
RepelCommandOptions.REASON,
302+
true,
303+
),
304+
},
305+
{
306+
name: 'Deleted Messages',
307+
value: deletedCount.toString(),
308+
},
309+
{
310+
name: 'Timeout Duration',
311+
value:
312+
isUserTimedOut || userNotInServer
313+
? 'No Timeout'
314+
: timeoutDurationInHours === 0
315+
? 'No Timeout'
316+
: `${timeoutDurationInHours} hours`,
317+
},
318+
)
319+
.setColor(0x00ff00)
320+
.setTimestamp();
321+
await logEmbed(client, REPEL_LOG_CHANNEL_ID, embed, undefined, true);
214322
} catch (error: any) {
215323
const errorMsg =
216324
error.message || 'An error occurred while executing this command.';

0 commit comments

Comments
 (0)