Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.

Commit 117bbf1

Browse files
authored
refactor(core): update prefix CommandData` interface, execute method, util functions (#191)
- Added 'Utility' category to CommandData interface - Changed requiredBotPermissions and requiredUserPermissions to be single PermissionsBitField - Updated execute method in BasePrefixCommand to include new functionality - Refactored Util functions in BasePrefixCommand - Updated messages in execute method to use emojis and handle errors
1 parent d91bc4a commit 117bbf1

File tree

10 files changed

+239
-91
lines changed

10 files changed

+239
-91
lines changed

src/commands/prefix/deleteMsg.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,32 @@ export default class DeleteMsgCommand extends BasePrefixCommand {
1616
name: 'deletemsg',
1717
description: 'Delete a message',
1818
category: 'Network',
19-
usage: 'deletemsg <message ID or link>',
19+
usage: 'deletemsg ` message ID or link `',
2020
examples: [
2121
'deletemsg 123456789012345678',
2222
'deletemsg https://discord.com/channels/123456789012345678/123456789012345678/123456789012345678',
2323
],
2424
aliases: ['delmsg', 'dmsg', 'delete', 'del'],
2525
dbPermission: false,
26+
totalArgs: 1,
2627
};
2728

28-
public async execute(message: Message<true>, args: string[]): Promise<void> {
29-
const originalMsgId = message.reference?.messageId ?? getMessageIdFromStr(args[0]);
30-
const originalMsg = originalMsgId ? await this.getOriginalMessage(originalMsgId) : null;
29+
protected async run(message: Message<true>, args: string[]): Promise<void> {
30+
const msgId = message.reference?.messageId ?? getMessageIdFromStr(args[0]);
31+
const originalMsg = msgId ? await this.getOriginalMessage(msgId) : null;
3132

3233
if (!originalMsg) {
3334
await message.channel.send('Please provide a valid message ID or link to delete.');
3435
return;
3536
}
3637

3738
const hub = await fetchHub(originalMsg.hubId);
38-
if (!hub || !isStaffOrHubMod(message.author.id, hub)) {
39-
await message.channel.send('You do not have permission to use this command.');
39+
if (
40+
!hub ||
41+
!isStaffOrHubMod(message.author.id, hub) ||
42+
originalMsg.authorId !== message.author.id
43+
) {
44+
await message.channel.send('You do not have permission to use this command on that message.');
4045
return;
4146
}
4247

src/commands/prefix/modpanel.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,20 @@ export default class BlacklistPrefixCommand extends BasePrefixCommand {
1313
name: 'modpanel',
1414
description: 'Blacklist a user or server from using the bot',
1515
category: 'Moderation',
16-
usage: 'blacklist <user ID or server ID>',
16+
usage: 'blacklist ` user ID or server ID `',
1717
examples: [
1818
'blacklist 123456789012345678',
1919
'blacklist 123456789012345678',
2020
'> Reply to a message with `blacklist` to blacklist the user who sent the message',
2121
],
2222
aliases: ['bl', 'modactions', 'modpanel', 'mod', 'ban'],
2323
dbPermission: false,
24+
totalArgs: 1,
2425
};
2526

26-
public async execute(message: Message<true>, args: string[]) {
27-
const originalMessageId = message.reference?.messageId ?? getMessageIdFromStr(args[0]);
28-
const originalMessage = originalMessageId
29-
? await this.getOriginalMessage(originalMessageId)
30-
: null;
27+
protected async run(message: Message<true>, args: string[]) {
28+
const msgId = message.reference?.messageId ?? getMessageIdFromStr(args[0]);
29+
const originalMessage = msgId ? await this.getOriginalMessage(msgId) : null;
3130

3231
if (!originalMessage) {
3332
await message.channel.send('Please provide a valid message ID or link.');

src/commands/prefix/report.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import BasePrefixCommand, { CommandData } from '#main/core/BasePrefixCommand.js';
2+
import { sendHubReport } from '#main/utils/HubLogger/Report.js';
3+
import {
4+
findOriginalMessage,
5+
getBroadcasts,
6+
getMessageIdFromStr,
7+
getOriginalMessage,
8+
} from '#main/utils/network/messageUtils.js';
9+
import { GuildTextBasedChannel, Message } from 'discord.js';
10+
import ms from 'ms';
11+
12+
export default class ReportPrefixCommand extends BasePrefixCommand {
13+
public readonly data: CommandData = {
14+
name: 'report',
15+
description: 'Report a message',
16+
category: 'Utility',
17+
usage: 'report ` [message ID or link] ` ` reason ` ',
18+
examples: [
19+
'report 123456789012345678',
20+
'report https://discord.com/channels/123456789012345678/123456789012345678/123456789012345678',
21+
'report 123456789012345678 Spamming',
22+
],
23+
aliases: ['r'],
24+
totalArgs: 1,
25+
cooldown: ms('30s'),
26+
};
27+
28+
protected async run(message: Message<true>, args: string[]) {
29+
const msgId = message.reference?.messageId ?? getMessageIdFromStr(args[0] ?? args[1]);
30+
const originalMsg = msgId ? await this.getOriginalMessage(msgId) : null;
31+
const broadcastMsgs = originalMsg
32+
? await getBroadcasts(originalMsg.messageId, originalMsg.hubId)
33+
: null;
34+
35+
if (!broadcastMsgs || !originalMsg) {
36+
await message.channel.send('Please provide a valid message ID or link.');
37+
return;
38+
}
39+
40+
const broadcastMsg = Object.values(broadcastMsgs).find((m) => m.messageId === msgId);
41+
if (!broadcastMsg) {
42+
await message.channel.send('Please provide a valid message ID or link.');
43+
return;
44+
}
45+
46+
const fetchedMsg = await (
47+
message.client.channels.cache.get(broadcastMsg.channelId) as GuildTextBasedChannel
48+
)?.messages
49+
.fetch(broadcastMsg.messageId)
50+
.catch(() => null);
51+
52+
await sendHubReport(originalMsg.hubId, message.client, {
53+
userId: originalMsg.authorId,
54+
serverId: originalMsg.guildId,
55+
reason: message.reference?.messageId ? args[0] : args.slice(1).join(' '),
56+
reportedBy: message.author,
57+
evidence: { messageId: broadcastMsg.messageId, content: fetchedMsg?.content },
58+
});
59+
}
60+
private async getOriginalMessage(messageId: string) {
61+
return (await getOriginalMessage(messageId)) ?? (await findOriginalMessage(messageId)) ?? null;
62+
}
63+
}

src/core/BasePrefixCommand.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,73 @@
1+
import { emojis } from '#main/config/Constants.js';
2+
import Logger from '#main/utils/Logger.js';
3+
import { isDev } from '#main/utils/Utils.js';
14
import { Message, PermissionsBitField } from 'discord.js';
25

36
export interface CommandData {
47
name: string;
58
description: string;
6-
category: 'Moderation' | 'Network'; // add more categories as needed
9+
category: 'Moderation' | 'Network' | 'Utility'; // add more categories as needed
710
usage: string;
811
examples: string[];
912
aliases: string[];
1013
dbPermission?: boolean;
14+
totalArgs: number;
1115
cooldown?: number;
1216
ownerOnly?: boolean;
13-
requiredBotPermissions?: PermissionsBitField[];
14-
requiredUserPermissions?: PermissionsBitField[];
17+
requiredBotPermissions?: PermissionsBitField;
18+
requiredUserPermissions?: PermissionsBitField;
1519
}
1620

1721
export default abstract class BasePrefixCommand {
1822
public abstract readonly data: CommandData;
19-
public abstract execute(message: Message, args: string[]): Promise<void>;
23+
protected abstract run(message: Message, args: string[]): Promise<void>;
24+
public async execute(message: Message, args: string[]): Promise<void> {
25+
try {
26+
// Check if command is owner-only
27+
if (this.data.ownerOnly && !isDev(message.author.id)) {
28+
await message.reply(`${emojis.botdev} This command can only be used by the bot owner.`);
29+
return;
30+
}
31+
32+
// Check user permissions
33+
const { requiredBotPermissions, requiredUserPermissions } = this.data;
34+
35+
const missingPerms =
36+
requiredUserPermissions &&
37+
message.member?.permissions.missing(requiredUserPermissions, true);
38+
if (missingPerms?.length) {
39+
await message.reply(`${emojis.neutral} You're missing the following permissions: ${missingPerms.join(', ')}`);
40+
return;
41+
}
42+
43+
const botMissingPerms =
44+
requiredBotPermissions &&
45+
message.guild?.members.me?.permissions.missing(requiredBotPermissions, true);
46+
if (botMissingPerms?.length) {
47+
await message.reply(`${emojis.no} I'm missing the following permissions: ${botMissingPerms.join(', ')}`);
48+
return;
49+
}
50+
51+
if (this.data.dbPermission && !message.inGuild()) {
52+
await message.reply(`${emojis.no} This command can only be used in a server.`);
53+
return;
54+
}
55+
56+
if (this.data.totalArgs > args.length) {
57+
const examplesStr =
58+
this.data.examples.length > 0 ? `\n**Examples**: ${this.data.examples.join('\n')}` : '';
59+
await message.reply(
60+
`${emojis.neutral} One or more args missing.\n**Usage**: ${this.data.usage}\n${examplesStr}`,
61+
);
62+
return;
63+
}
64+
65+
// Run command
66+
await this.run(message, args);
67+
}
68+
catch (error) {
69+
Logger.error(error);
70+
await message.reply('There was an error executing this command!');
71+
}
72+
}
2073
}

src/events/messageCreate.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import BaseEventListener from '#main/core/BaseEventListener.js';
33
import HubSettingsManager from '#main/managers/HubSettingsManager.js';
44
import Logger from '#main/utils/Logger.js';
55
import { checkBlockedWords } from '#main/utils/network/blockwordsRunner.js';
6-
import handlePrefixCommand from '#main/utils/PrefixCmdHandler.js';
76
import { generateJumpButton as getJumpButton } from '#utils/ComponentUtils.js';
87
import { getConnectionHubId, getHubConnections } from '#utils/ConnectedListUtils.js';
98
import db from '#utils/Db.js';
@@ -40,7 +39,7 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> {
4039
if (!message.inGuild() || !isHumanMessage(message)) return;
4140

4241
if (message.content.startsWith('c!')) {
43-
await handlePrefixCommand(message, 'c!');
42+
await this.handlePrefixCommand(message, 'c!');
4443
return;
4544
}
4645

@@ -66,6 +65,23 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> {
6665
await this.processMessage(message, hub, hubConnections, settings, connection, attachmentURL);
6766
}
6867

68+
private async handlePrefixCommand(message: Message, prefix: string) {
69+
// Split message into command and arguments
70+
const args = message.content.slice(prefix.length).trim().split(/ +/);
71+
const commandName = args.shift()?.toLowerCase();
72+
73+
if (!commandName) return;
74+
75+
// Find command by name or alias
76+
const command =
77+
message.client.prefixCommands.get(commandName) ||
78+
message.client.prefixCommands.find((cmd) => cmd.data.aliases?.includes(commandName));
79+
80+
if (!command) return;
81+
82+
await command.execute(message, args);
83+
}
84+
6985
private async getHub(hubId: string) {
7086
return await db.hub.findFirst({
7187
where: { id: hubId },
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { emojis } from '#main/config/Constants.js';
2+
import { RegisterInteractionHandler } from '#main/decorators/Interaction.js';
3+
import { CustomID } from '#main/utils/CustomID.js';
4+
import { InfoEmbed } from '#main/utils/EmbedUtils.js';
5+
import { fetchHub, isStaffOrHubMod } from '#main/utils/hub/utils.js';
6+
import modActionsPanel from '#main/utils/moderation/modActions/modActionsPanel.js';
7+
import { findOriginalMessage, getOriginalMessage } from '#main/utils/network/messageUtils.js';
8+
import { ButtonInteraction } from 'discord.js';
9+
10+
export default class ModActionsButton {
11+
@RegisterInteractionHandler('showModPanel')
12+
async handler(interaction: ButtonInteraction): Promise<void> {
13+
await interaction.deferUpdate();
14+
15+
const customId = CustomID.parseCustomId(interaction.customId);
16+
const [messageId] = customId.args;
17+
18+
const originalMessage =
19+
(await getOriginalMessage(messageId)) ?? (await findOriginalMessage(messageId));
20+
const hub = originalMessage ? await fetchHub(originalMessage?.hubId) : null;
21+
22+
if (!originalMessage || !hub || !isStaffOrHubMod(interaction.user.id, hub)) {
23+
await interaction.editReply({ components: [] });
24+
await interaction.followUp({
25+
embeds: [new InfoEmbed({ description: `${emojis.slash} Message was deleted.` })],
26+
ephemeral: true,
27+
});
28+
return;
29+
}
30+
31+
if (!isStaffOrHubMod(interaction.user.id, hub)) return;
32+
33+
const panel = await modActionsPanel.buildMessage(interaction, originalMessage);
34+
await interaction.followUp({
35+
embeds: [panel.embed],
36+
components: panel.buttons,
37+
ephemeral: true,
38+
});
39+
}
40+
}

src/utils/HubLogger/Default.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { ClusterClient } from 'discord-hybrid-sharding';
2-
import type { Channel, Client, EmbedBuilder } from 'discord.js';
2+
import type {
3+
APIActionRowComponent,
4+
APIMessageActionRowComponent,
5+
Channel,
6+
Client,
7+
EmbedBuilder,
8+
} from 'discord.js';
39

410
/**
511
* Sends a log message to the specified channel with the provided embed.
@@ -10,7 +16,10 @@ export const sendLog = async (
1016
cluster: ClusterClient<Client>,
1117
channelId: string,
1218
embed: EmbedBuilder,
13-
content?: string,
19+
opts?: {
20+
content?: string;
21+
components: APIActionRowComponent<APIMessageActionRowComponent>[];
22+
},
1423
) => {
1524
await cluster.broadcastEval(
1625
async (shardClient, ctx) => {
@@ -19,9 +28,11 @@ export const sendLog = async (
1928
.catch(() => null)) as Channel | null;
2029

2130
if (channel?.isSendable()) {
22-
await channel.send({ content: ctx.content, embeds: [ctx.embed] }).catch(() => null);
31+
await channel
32+
.send({ content: ctx.content, embeds: [ctx.embed], components: ctx.components })
33+
.catch(() => null);
2334
}
2435
},
25-
{ context: { channelId, embed, content } },
36+
{ context: { channelId, embed, content: opts?.content, components: opts?.components } },
2637
);
2738
};

src/utils/HubLogger/Report.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import { getBroadcast, getOriginalMessage } from '#main/utils/network/messageUtils.js';
1+
import { CustomID } from '#main/utils/CustomID.js';
2+
import {
3+
findOriginalMessage,
4+
getBroadcast,
5+
getOriginalMessage,
6+
} from '#main/utils/network/messageUtils.js';
27
import { stripIndents } from 'common-tags';
38
import {
9+
ActionRowBuilder,
10+
ButtonBuilder,
11+
ButtonStyle,
412
EmbedBuilder,
513
messageLink,
614
roleMention,
@@ -43,7 +51,8 @@ const genJumpLink = async (
4351
) => {
4452
if (!messageId) return null;
4553

46-
const originalMsg = await getOriginalMessage(messageId);
54+
const originalMsg =
55+
(await getOriginalMessage(messageId)) ?? (await findOriginalMessage(messageId));
4756
if (!originalMsg) return null;
4857

4958
// fetch the reports server ID from the log channel's ID
@@ -89,6 +98,8 @@ export const sendHubReport = async (
8998
const hub = await db.hub.findFirst({ where: { id: hubId }, include: { logConfig: true } });
9099
if (!hub?.logConfig[0]?.reports?.channelId) return;
91100

101+
if (!evidence?.messageId) return;
102+
92103
const { channelId: reportsChannelId, roleId: reportsRoleId } = hub.logConfig[0].reports;
93104
const server = await client.fetchGuild(serverId);
94105
const jumpLink = await genJumpLink(hubId, client, evidence?.messageId, reportsChannelId);
@@ -117,5 +128,20 @@ export const sendHubReport = async (
117128
});
118129

119130
const mentionRole = reportsRoleId ? roleMention(reportsRoleId) : undefined;
120-
await sendLog(client.cluster, reportsChannelId, embed, mentionRole);
131+
const button = new ActionRowBuilder<ButtonBuilder>()
132+
.addComponents(
133+
new ButtonBuilder()
134+
.setCustomId(
135+
new CustomID().setIdentifier('showModPanel').addArgs(evidence.messageId).toString(),
136+
)
137+
.setStyle(ButtonStyle.Danger)
138+
.setLabel('Take Action')
139+
.setEmoji(emojis.blobFastBan),
140+
)
141+
.toJSON();
142+
143+
await sendLog(client.cluster, reportsChannelId, embed, {
144+
content: mentionRole,
145+
components: [button],
146+
});
121147
};

0 commit comments

Comments
 (0)