From 262ac258dfaada32f4f7830e8e657295d0492386 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 9 Aug 2025 00:44:18 +0300 Subject: [PATCH 1/7] refractor: use repel role id instead of name --- .env.example | 2 +- src/env.ts | 3 ++- src/v2/commands/repel/index.ts | 23 ++++++++++++----------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 84f8f829..3b9e0490 100644 --- a/.env.example +++ b/.env.example @@ -40,5 +40,5 @@ ONBOARDING_CHANNEL= JOIN_LOG_CHANNEL= INTRO_CHANNEL= INTRO_ROLE= -REPEL_ROLE_NAME=MiniMod # The name of the role that is used for MiniMods +REPEL_ROLE_ID=MiniMod # The ID of the role that is used for MiniMods REPEL_DELETE_COUNT=2 # The number of messages to delete when using the repel command diff --git a/src/env.ts b/src/env.ts index 449ecaba..f87f40c5 100644 --- a/src/env.ts +++ b/src/env.ts @@ -43,6 +43,7 @@ export const { JOIN_LOG_CHANNEL } = process.env; export const { INTRO_CHANNEL } = process.env; export const { INTRO_ROLE } = process.env; -export const { REPEL_ROLE_NAME } = process.env; +export const { REPEL_ROLE_ID } = process.env; export const REPEL_DELETE_COUNT = Number.parseInt(process.env.REPEL_DELETE_COUNT) || 2; +export const { REPEL_LOG_CHANNEL_ID } = process.env; diff --git a/src/v2/commands/repel/index.ts b/src/v2/commands/repel/index.ts index 9a5de561..228b5981 100644 --- a/src/v2/commands/repel/index.ts +++ b/src/v2/commands/repel/index.ts @@ -9,7 +9,7 @@ import { type TextChannel, } from 'discord.js'; import type { CommandDataWithHandler } from '../../../types'; -import { REPEL_DELETE_COUNT, REPEL_ROLE_NAME } from '../../env'; +import { REPEL_DELETE_COUNT, REPEL_ROLE_ID } from '../../env'; const TARGET_KEY = 'target'; const MESSAGE_LINK_KEY = 'message_link'; @@ -64,16 +64,17 @@ export const repelInteraction: CommandDataWithHandler = { await reply(interaction, 'This command can only be used in a server.'); } const repelRole = interaction.guild.roles.cache.find( - role => role.name === REPEL_ROLE_NAME, + role => role.id === REPEL_ROLE_ID, ); if (!repelRole) { await reply( interaction, - `${REPEL_ROLE_NAME || 'Repel'} role not found. Please contact an admin.`, + 'Repel role not found. Please check the id in the environment variables.', ); return; } + const roleName = repelRole.name; const member = interaction.member as GuildMember; const canUseCommand = @@ -84,7 +85,7 @@ export const repelInteraction: CommandDataWithHandler = { if (!canUseCommand) { await reply( interaction, - `You do not have permission to use this command. You need the ${REPEL_ROLE_NAME} role or moderate members permission.`, + `You do not have permission to use this command`, ); return; } @@ -120,13 +121,13 @@ export const repelInteraction: CommandDataWithHandler = { return; } - if (targetMember.roles.cache.has(repelRole.id)) { - await reply( - interaction, - `You cannot repel a user with the ${REPEL_ROLE_NAME} role.`, - ); - return; - } + if (targetMember.roles.cache.has(repelRole.id)) { + await reply( + interaction, + `You cannot repel a user with the ${roleName} role.`, + ); + return; + } const botMember = await interaction.guild.members.fetch(client.user!.id); const isOwner = interaction.guild.ownerId === member.id; From 76321e36e6afbb06c8f01d8bc59a2dc44b1a53a6 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 9 Aug 2025 00:47:19 +0300 Subject: [PATCH 2/7] fix: fix typescript errors --- src/v2/commands/repel/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/v2/commands/repel/index.ts b/src/v2/commands/repel/index.ts index 228b5981..e980cc67 100644 --- a/src/v2/commands/repel/index.ts +++ b/src/v2/commands/repel/index.ts @@ -63,6 +63,13 @@ export const repelInteraction: CommandDataWithHandler = { if (!interaction.inGuild() || !interaction.guild) { await reply(interaction, 'This command can only be used in a server.'); } + if (!interaction.isChatInputCommand()) { + await reply( + interaction, + 'This command can only be used as a slash command.', + ); + return; + } const repelRole = interaction.guild.roles.cache.find( role => role.id === REPEL_ROLE_ID, ); From 0bc4998b91463c5e40b6279d70d6be885ea447e5 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 9 Aug 2025 05:05:20 +0300 Subject: [PATCH 3/7] feat: add channel logger utility --- src/v2/utils/channel-logger.ts | 392 +++++++++++++++++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 src/v2/utils/channel-logger.ts diff --git a/src/v2/utils/channel-logger.ts b/src/v2/utils/channel-logger.ts new file mode 100644 index 00000000..b3757838 --- /dev/null +++ b/src/v2/utils/channel-logger.ts @@ -0,0 +1,392 @@ +import { + Client, + TextChannel, + EmbedBuilder, + MessageCreateOptions, + ChannelType, +} from 'discord.js'; + +// Types for different log content structures +export interface SimpleLogContent { + type: 'simple'; + message: string; +} + +export interface EmbedLogContent { + type: 'embed'; + embed: EmbedBuilder | EmbedBuilder[]; + content?: string; +} + +export interface CustomLogContent { + type: 'custom'; + options: MessageCreateOptions; +} + +export type LogContent = SimpleLogContent | EmbedLogContent | CustomLogContent; + +export interface LoggerOptions { + client: Client; + channelId: string; + content: LogContent; + fallbackChannelId?: string; + silent?: boolean; +} + +/** + * Get channel by ID with proper type checking + */ +async function getChannel( + client: Client, + channelId: string, +): Promise { + try { + const channel = await client.channels.fetch(channelId); + + if (!channel || channel.type !== ChannelType.GuildText) { + return null; + } + + return channel as TextChannel; + } catch { + return null; + } +} + +/** + * Send message based on content type + */ +async function sendMessage( + channel: TextChannel, + content: LogContent, +): Promise { + try { + let messageOptions: MessageCreateOptions; + + switch (content.type) { + case 'simple': + messageOptions = { content: content.message }; + break; + + case 'embed': + messageOptions = { + embeds: Array.isArray(content.embed) + ? content.embed + : [content.embed], + content: content.content || undefined, + }; + break; + + case 'custom': + messageOptions = content.options; + break; + + default: + throw new Error('Invalid content type provided'); + } + + await channel.send(messageOptions); + return true; + } catch (error) { + console.error('Error sending message:', error); + return false; + } +} + +/** + * Main logging function - sends a message to the specified channel + */ +export async function logToChannel(options: LoggerOptions): Promise { + try { + const channel = await getChannel(options.client, options.channelId); + + if (!channel) { + // Try fallback channel if provided + if (options.fallbackChannelId) { + const fallbackChannel = await getChannel( + options.client, + options.fallbackChannelId, + ); + if (fallbackChannel) { + return await sendMessage(fallbackChannel, options.content); + } + } + + if (!options.silent) { + throw new Error( + `Channel with ID ${options.channelId} not found or not accessible`, + ); + } + return false; + } + + return await sendMessage(channel, options.content); + } catch (error) { + if (!options.silent) { + console.error('Channel Logger Error:', error); + throw error; + } + return false; + } +} + +/** + * Quick function for simple text logging + */ +export async function logSimple( + client: Client, + channelId: string, + message: string, + silent: boolean = false, +): Promise { + return logToChannel({ + client, + channelId, + content: { type: 'simple', message }, + silent, + }); +} + +/** + * Quick function for embed logging + */ +export async function logEmbed( + client: Client, + channelId: string, + embed: EmbedBuilder | EmbedBuilder[], + content?: string, + silent: boolean = false, +): Promise { + return logToChannel({ + client, + channelId, + content: { type: 'embed', embed, content }, + silent, + }); +} + +/** + * Quick function for custom message logging + */ +export async function logCustom( + client: Client, + channelId: string, + options: MessageCreateOptions, + fallbackChannelId?: string, + silent: boolean = false, +): Promise { + return logToChannel({ + client, + channelId, + content: { type: 'custom', options }, + fallbackChannelId, + silent, + }); +} + +// Template functions for common log scenarios + +/** + * Create a moderation action embed + */ +export function createModerationEmbed(options: { + action: string; + moderator: string; + target: string; + reason?: string; + duration?: string; + color?: number; +}): EmbedBuilder { + const embed = new EmbedBuilder() + .setTitle(`🔨 ${options.action}`) + .setColor(options.color || 0xff6b6b) + .addFields( + { name: 'Moderator', value: options.moderator, inline: true }, + { name: 'Target', value: options.target, inline: true }, + ) + .setTimestamp(); + + if (options.reason) { + embed.addFields({ name: 'Reason', value: options.reason, inline: false }); + } + + if (options.duration) { + embed.addFields({ + name: 'Duration', + value: options.duration, + inline: true, + }); + } + + return embed; +} + +/** + * Create a user join/leave embed + */ +export function createUserEventEmbed(options: { + type: 'join' | 'leave'; + user: string; + memberCount?: number; + color?: number; +}): EmbedBuilder { + const isJoin = options.type === 'join'; + const embed = new EmbedBuilder() + .setTitle(`${isJoin ? '📥' : '📤'} User ${isJoin ? 'Joined' : 'Left'}`) + .setColor(options.color || (isJoin ? 0x57f287 : 0xfaa61a)) + .addFields({ name: 'User', value: options.user, inline: true }) + .setTimestamp(); + + if (options.memberCount) { + embed.addFields({ + name: 'Member Count', + value: options.memberCount.toString(), + inline: true, + }); + } + + return embed; +} + +/** + * Create a message deletion embed + */ +export function createMessageDeletedEmbed(options: { + author: string; + channel: string; + content?: string; + attachments?: number; +}): EmbedBuilder { + const embed = new EmbedBuilder() + .setTitle('🗑️ Message Deleted') + .setColor(0xed4245) + .addFields( + { name: 'Author', value: options.author, inline: true }, + { name: 'Channel', value: options.channel, inline: true }, + ) + .setTimestamp(); + + if (options.content) { + embed.addFields({ + name: 'Content', + value: + options.content.length > 1024 + ? options.content.substring(0, 1021) + '...' + : options.content, + inline: false, + }); + } + + if (options.attachments && options.attachments > 0) { + embed.addFields({ + name: 'Attachments', + value: options.attachments.toString(), + inline: true, + }); + } + + return embed; +} + +/** + * Convenience function: Log a moderation action + */ +export async function logModerationAction( + client: Client, + channelId: string, + options: { + action: string; + moderator: string; + target: string; + reason?: string; + duration?: string; + color?: number; + }, + silent: boolean = false, +): Promise { + const embed = createModerationEmbed(options); + return logEmbed(client, channelId, embed, undefined, silent); +} + +/** + * Convenience function: Log a user join/leave event + */ +export async function logUserEvent( + client: Client, + channelId: string, + options: { + type: 'join' | 'leave'; + user: string; + memberCount?: number; + color?: number; + }, + silent: boolean = false, +): Promise { + const embed = createUserEventEmbed(options); + return logEmbed(client, channelId, embed, undefined, silent); +} + +/** + * Convenience function: Log a message deletion + */ +export async function logMessageDeleted( + client: Client, + channelId: string, + options: { + author: string; + channel: string; + content?: string; + attachments?: number; + }, + silent: boolean = false, +): Promise { + const embed = createMessageDeletedEmbed(options); + return logEmbed(client, channelId, embed, undefined, silent); +} + +// Example usage patterns: +/* +import { + logSimple, + logEmbed, + logToChannel, + logModerationAction, + createModerationEmbed +} from './utils/channel-logger'; + +// Simple text log +await logSimple(client, 'CHANNEL_ID', 'User performed an action'); + +// Embed log +const embed = new EmbedBuilder() + .setTitle('Test Log') + .setDescription('This is a test') + .setColor(0x00ff00); + +await logEmbed(client, 'CHANNEL_ID', embed); + +// Moderation action (using convenience function) +await logModerationAction(client, 'MOD_LOG_CHANNEL', { + action: 'Ban', + moderator: `<@${interaction.user.id}>`, + target: `<@${targetUser.id}>`, + reason: 'Spam', + color: 0xff0000 +}); + +// Custom complex message +await logToChannel({ + client, + channelId: 'ADMIN_CHANNEL', + content: { + type: 'custom', + options: { + content: `<@&MODERATOR_ROLE> Attention needed!`, + embeds: [embed1, embed2], + allowedMentions: { roles: ['MODERATOR_ROLE'] } + } + }, + fallbackChannelId: 'BACKUP_CHANNEL', + silent: true +}); +*/ From 552cac6b3de602870389885df02a660822ee7e0e Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 9 Aug 2025 05:06:41 +0300 Subject: [PATCH 4/7] feat: add Discord error codes enum --- src/enums.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/enums.ts b/src/enums.ts index a5b20d69..14136fd8 100644 --- a/src/enums.ts +++ b/src/enums.ts @@ -52,3 +52,9 @@ export enum Months { November = 10, December = 11, } + +/* https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes */ +export enum DiscordAPIErrorCode { + UnknownMember = 10007, + UnknownUser = 10013, +} From 642e7ff871177ac9156106d6ddd2afe52bcf6088 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 9 Aug 2025 05:07:08 +0300 Subject: [PATCH 5/7] feat: implement new repel command options + logging --- .env.example | 6 +- src/env.ts | 6 +- src/v2/commands/repel/index.ts | 244 ++++++++++++++++++++++++--------- 3 files changed, 184 insertions(+), 72 deletions(-) diff --git a/.env.example b/.env.example index 3b9e0490..873fa033 100644 --- a/.env.example +++ b/.env.example @@ -40,5 +40,7 @@ ONBOARDING_CHANNEL= JOIN_LOG_CHANNEL= INTRO_CHANNEL= INTRO_ROLE= -REPEL_ROLE_ID=MiniMod # The ID of the role that is used for MiniMods -REPEL_DELETE_COUNT=2 # The number of messages to delete when using the repel command +REPEL_ROLE_ID=1002411741776461844 # The ID of the role that is used for MiniMods +REPEL_DEFAULT_DELETE_COUNT=20 # The number of messages to delete when using the repel command +REPEL_LOG_CHANNEL_ID=1403558160144531589 # The channel where the repel command logs are sent +REPEL_DEFAULT_TIMEOUT=6 # Default timeout for the repel command in HOURS diff --git a/src/env.ts b/src/env.ts index f87f40c5..17400d38 100644 --- a/src/env.ts +++ b/src/env.ts @@ -44,6 +44,8 @@ export const { INTRO_CHANNEL } = process.env; export const { INTRO_ROLE } = process.env; export const { REPEL_ROLE_ID } = process.env; -export const REPEL_DELETE_COUNT = - Number.parseInt(process.env.REPEL_DELETE_COUNT) || 2; +export const REPEL_DEFAULT_DELETE_COUNT = + Number.parseInt(process.env.REPEL_DEFAULT_DELETE_COUNT) || 20; export const { REPEL_LOG_CHANNEL_ID } = process.env; +export const REPEL_DEFAULT_TIMEOUT = + Number.parseInt(process.env.REPEL_DEFAULT_TIMEOUT) || 6; diff --git a/src/v2/commands/repel/index.ts b/src/v2/commands/repel/index.ts index e980cc67..e017d810 100644 --- a/src/v2/commands/repel/index.ts +++ b/src/v2/commands/repel/index.ts @@ -1,6 +1,7 @@ import { ApplicationCommandOptionType, ChannelType, + EmbedBuilder, PermissionFlagsBits, User, type Client, @@ -9,12 +10,23 @@ import { type TextChannel, } from 'discord.js'; import type { CommandDataWithHandler } from '../../../types'; -import { REPEL_DELETE_COUNT, REPEL_ROLE_ID } from '../../env'; +import { + REPEL_DEFAULT_DELETE_COUNT, + REPEL_ROLE_ID, + REPEL_LOG_CHANNEL_ID, + REPEL_DEFAULT_TIMEOUT, +} from '../../env'; +import { DiscordAPIErrorCode } from '../../../enums'; +import { logEmbed } from '../../utils/channel-logger'; -const TARGET_KEY = 'target'; -const MESSAGE_LINK_KEY = 'message_link'; +enum RepelCommandOptions { + TARGET = 'target', + MESSAGE_LINK = 'message_link', + DELETE_COUNT = 'delete_count', + TIMEOUT = 'timeout', + REASON = 'reason', +} const DAY = 24 * 60 * 60 * 1000; -const TIMEOUT_DURATION = 6 * 60 * 60 * 1000; // 6 hours in milliseconds const reply = ( interaction: CommandInteraction, @@ -22,40 +34,89 @@ const reply = ( ephemeral = true, ) => interaction.reply({ content, ephemeral }); -const getTargetFromMessage = async ( - client: Client, - guild: any, - messageLink: string, -) => { - const match = messageLink.match(/(?:channels|@me)\/(?:(\d+)\/)?(\d+)\/(\d+)/); - if (!match) throw new Error('Invalid message link format.'); - const messageId = match[3]; - const channelId = match[2]; - - const channel = channelId ? await client.channels.fetch(channelId) : null; - if (channel?.type !== ChannelType.GuildText) - throw new Error('Invalid channel for message link.'); - - const message = await (channel as TextChannel).messages.fetch(messageId); - return await guild.members.fetch(message.author.id); -}; - export const repelInteraction: CommandDataWithHandler = { name: 'repel', - description: - 'Remove recent messages and timeout a user (requires timeout permissions)', + description: 'Remove recent messages and timeout a user', options: [ { - name: TARGET_KEY, + name: RepelCommandOptions.TARGET, description: 'The user to repel', type: ApplicationCommandOptionType.User, - required: false, + required: true, }, { - name: MESSAGE_LINK_KEY, - description: 'Message link to identify the user to repel', + name: RepelCommandOptions.REASON, + description: 'Reason for repelling the user', type: ApplicationCommandOptionType.String, + required: true, + }, + { + name: RepelCommandOptions.DELETE_COUNT, + description: `Number of messages to delete from the user (default: ${REPEL_DEFAULT_DELETE_COUNT})`, + type: ApplicationCommandOptionType.Integer, required: false, + choices: [ + { + name: '5 messages', + value: 5, + }, + { + name: '10 messages', + value: 10, + }, + { + name: '20 messages', + value: 20, + }, + { + name: '50 messages', + value: 50, + }, + { + name: '100 messages', + value: 100, + }, + { + name: '200 messages', + value: 200, + }, + ], + }, + { + name: RepelCommandOptions.TIMEOUT, + description: `Timeout duration in hours (default: ${REPEL_DEFAULT_TIMEOUT} hours)`, + type: ApplicationCommandOptionType.Integer, + required: false, + choices: [ + { + name: 'No timeout', + value: 0, + }, + { + name: '1 hour', + value: 1, + }, + { + name: '2 hours', + value: 2, + }, + { + name: '3 hours', + value: 3, + }, + { + name: '6 hours', + value: 6, + }, + { + name: '12 hours', + value: 12, + }, + { + name: '1 day', + value: 24, + }, + ], }, ], @@ -97,38 +158,35 @@ export const repelInteraction: CommandDataWithHandler = { return; } - const targetUser = interaction.options.get(TARGET_KEY, false)?.user as - | User - | undefined; - const messageLink = interaction.options.get(MESSAGE_LINK_KEY, false) - ?.value as string | undefined; + const targetUser = interaction.options.get( + RepelCommandOptions.TARGET, + false, + )?.user as User; + console.log('Target User:', targetUser); - if (!targetUser && !messageLink) { - await reply( - interaction, - 'You must specify either a user or a message link.', - ); - } + let targetGuildMember: GuildMember | null = null; + let userNotInServer = false; try { - let targetMember: GuildMember; - - if (targetUser) { - targetMember = await interaction.guild.members.fetch(targetUser.id); - } else if (messageLink) { - targetMember = await getTargetFromMessage( - client, - interaction.guild, - messageLink!, - ); + targetGuildMember = await interaction.guild.members.fetch(targetUser.id); + } catch (error: any) { + if ( + error.code === DiscordAPIErrorCode.UnknownMember || + error.code === DiscordAPIErrorCode.UnknownUser + ) { + userNotInServer = true; + } else { + throw error; } + } - if (targetMember.id === member.id) { + if (targetGuildMember !== null) { + if (targetGuildMember.id === member.id) { await reply(interaction, 'You cannot repel yourself.'); return; } - if (targetMember.roles.cache.has(repelRole.id)) { + if (targetGuildMember.roles.cache.has(repelRole.id)) { await reply( interaction, `You cannot repel a user with the ${roleName} role.`, @@ -139,13 +197,14 @@ export const repelInteraction: CommandDataWithHandler = { const botMember = await interaction.guild.members.fetch(client.user!.id); const isOwner = interaction.guild.ownerId === member.id; - if (targetMember.id === interaction.guild.ownerId) { + if (targetGuildMember.id === interaction.guild.ownerId) { await reply(interaction, 'Cannot moderate the server owner.'); } if ( !isOwner && - targetMember.roles.highest.position >= member.roles.highest.position + targetGuildMember.roles.highest.position >= + member.roles.highest.position ) { await reply( interaction, @@ -154,23 +213,35 @@ export const repelInteraction: CommandDataWithHandler = { } if ( - targetMember.roles.highest.position >= botMember.roles.highest.position + targetGuildMember.roles.highest.position >= + botMember.roles.highest.position ) { await reply( interaction, 'I cannot moderate this user due to role hierarchy.', ); } + } - await interaction.deferReply({ ephemeral: true }); + const targetId = userNotInServer ? targetUser.id : targetGuildMember!.id; + const targetTag = userNotInServer + ? targetUser.tag + : targetGuildMember!.user.tag; + try { + await interaction.deferReply({ ephemeral: true }); + const messagesToDelete = + interaction.options.getInteger( + RepelCommandOptions.DELETE_COUNT, + false, + ) ?? REPEL_DEFAULT_DELETE_COUNT; let deletedCount = 0; const textChannels = interaction.guild.channels.cache.filter( ch => ch.type === ChannelType.GuildText, ); for (const [, channel] of textChannels) { - if (deletedCount >= REPEL_DELETE_COUNT) break; + if (deletedCount >= messagesToDelete) break; try { const messages = await channel.messages.fetch({ @@ -179,12 +250,10 @@ export const repelInteraction: CommandDataWithHandler = { const userMessages = messages .filter( m => - m.author.id === targetMember.id && + m.author.id === targetId && Date.now() - m.createdTimestamp < 14 * DAY, ) - .first( - Math.min(REPEL_DELETE_COUNT - deletedCount, REPEL_DELETE_COUNT), - ); + .first(Math.min(messagesToDelete - deletedCount, messagesToDelete)); if (userMessages.length > 0) { userMessages.length === 1 ? await userMessages[0].delete() @@ -194,23 +263,62 @@ export const repelInteraction: CommandDataWithHandler = { } catch {} } - const isUserTimedOut = targetMember.communicationDisabledUntilTimestamp - ? targetMember.communicationDisabledUntilTimestamp > Date.now() - : false; + const isUserTimedOut = + targetGuildMember?.communicationDisabledUntilTimestamp + ? targetGuildMember.communicationDisabledUntilTimestamp > Date.now() + : false; - if (!isUserTimedOut) { - await targetMember.timeout( - TIMEOUT_DURATION, + const timeoutDurationInHours = + interaction.options.getInteger(RepelCommandOptions.TIMEOUT, false) ?? + REPEL_DEFAULT_TIMEOUT; + if ( + !isUserTimedOut && + timeoutDurationInHours > 0 && + targetGuildMember !== null + ) { + await targetGuildMember.timeout( + timeoutDurationInHours * 60 * 60 * 1000, `Repel command used by ${member.user.tag}`, ); await interaction.editReply({ - content: `Successfully repelled ${targetMember.user.tag}. Removed ${deletedCount} messages and timed out for 6 hours.`, + content: `Successfully repelled ${targetTag}. Removed ${deletedCount} messages and timed out for ${timeoutDurationInHours} hours.`, }); } else { await interaction.editReply({ - content: `Successfully repelled ${targetMember.user.tag}. Removed ${deletedCount} messages.`, + content: `Successfully repelled ${targetTag}. Removed ${deletedCount} messages.`, }); } + + const embed = new EmbedBuilder() + .setTitle('Repel Action') + .setDescription( + `<@${targetId}> has been repelled by <@${member.id}> in <#${interaction.channelId}>.`, + ) + .addFields( + { + name: 'Reason', + value: interaction.options.getString( + RepelCommandOptions.REASON, + true, + ), + }, + { + name: 'Deleted Messages', + value: deletedCount.toString(), + }, + { + name: 'Timeout Duration', + value: + isUserTimedOut || userNotInServer + ? 'No Timeout' + : timeoutDurationInHours === 0 + ? 'No Timeout' + : `${timeoutDurationInHours} hours`, + }, + ) + .setColor(0x00ff00) + .setTimestamp(); + await logEmbed(client, REPEL_LOG_CHANNEL_ID, embed, undefined, true); } catch (error: any) { const errorMsg = error.message || 'An error occurred while executing this command.'; From 320e1bf3d77bfc7c6713f3ec7565f838ac867ade Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 9 Aug 2025 20:25:41 +0300 Subject: [PATCH 6/7] fix: include channel where the command is used in --- src/v2/commands/repel/index.ts | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/v2/commands/repel/index.ts b/src/v2/commands/repel/index.ts index e017d810..6da69431 100644 --- a/src/v2/commands/repel/index.ts +++ b/src/v2/commands/repel/index.ts @@ -2,7 +2,9 @@ import { ApplicationCommandOptionType, ChannelType, EmbedBuilder, + GuildChannel, PermissionFlagsBits, + TextBasedChannel, User, type Client, type CommandInteraction, @@ -17,7 +19,7 @@ import { REPEL_DEFAULT_TIMEOUT, } from '../../env'; import { DiscordAPIErrorCode } from '../../../enums'; -import { logEmbed } from '../../utils/channel-logger'; +import { logEmbed, logSimple } from '../../utils/channel-logger'; enum RepelCommandOptions { TARGET = 'target', @@ -236,11 +238,22 @@ export const repelInteraction: CommandDataWithHandler = { false, ) ?? REPEL_DEFAULT_DELETE_COUNT; let deletedCount = 0; - const textChannels = interaction.guild.channels.cache.filter( - ch => ch.type === ChannelType.GuildText, - ); + const textChannels = interaction.guild.channels.cache + .filter( + (ch): ch is TextChannel => + (ch.type === ChannelType.GuildText || + ch.type === ChannelType.GuildVoice) && + ch.id !== interaction.channelId && + Boolean(ch.lastMessageId), + ) + .sort((a, b) => { + const aLastMessage = a.lastMessageId ? BigInt(a.lastMessageId) : 0n; + const bLastMessage = b.lastMessageId ? BigInt(b.lastMessageId) : 0n; + return Number(bLastMessage - aLastMessage); + }) + .first(50); - for (const [, channel] of textChannels) { + for (const channel of [interaction.channel, ...textChannels]) { if (deletedCount >= messagesToDelete) break; try { @@ -289,10 +302,15 @@ export const repelInteraction: CommandDataWithHandler = { }); } + const channelInfo = + interaction.channel?.type === ChannelType.GuildVoice + ? `**${interaction.channel.name}** voice chat` + : `<#${interaction.channelId}>`; + const embed = new EmbedBuilder() .setTitle('Repel Action') .setDescription( - `<@${targetId}> has been repelled by <@${member.id}> in <#${interaction.channelId}>.`, + `<@${targetId}> has been repelled by <@${member.id}> in ${channelInfo}.`, ) .addFields( { From ae407dc951af5fec3b3d202cf788cfcda99676ff Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sat, 9 Aug 2025 20:29:29 +0300 Subject: [PATCH 7/7] remove unused imports --- src/v2/commands/repel/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/v2/commands/repel/index.ts b/src/v2/commands/repel/index.ts index 6da69431..b4f68a11 100644 --- a/src/v2/commands/repel/index.ts +++ b/src/v2/commands/repel/index.ts @@ -2,9 +2,7 @@ import { ApplicationCommandOptionType, ChannelType, EmbedBuilder, - GuildChannel, PermissionFlagsBits, - TextBasedChannel, User, type Client, type CommandInteraction, @@ -19,7 +17,7 @@ import { REPEL_DEFAULT_TIMEOUT, } from '../../env'; import { DiscordAPIErrorCode } from '../../../enums'; -import { logEmbed, logSimple } from '../../utils/channel-logger'; +import { logEmbed } from '../../utils/channel-logger'; enum RepelCommandOptions { TARGET = 'target', @@ -164,7 +162,6 @@ export const repelInteraction: CommandDataWithHandler = { RepelCommandOptions.TARGET, false, )?.user as User; - console.log('Target User:', targetUser); let targetGuildMember: GuildMember | null = null; let userNotInServer = false;