11import {
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' ;
1112import 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+ }
1629const DAY = 24 * 60 * 60 * 1000 ;
17- const TIMEOUT_DURATION = 6 * 60 * 60 * 1000 ; // 6 hours in milliseconds
1830
1931const 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 ( / (?: c h a n n e l s | @ m e ) \/ (?: ( \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-
4337export 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