Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/commands/prefix/deleteMsg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { emojis } from '#main/config/Constants.js';
import BasePrefixCommand, { CommandData } from '#main/core/BasePrefixCommand.js';
import { fetchHub, isStaffOrHubMod } from '#main/utils/hub/utils.js';
import { deleteMessageFromHub } from '#main/utils/moderation/deleteMessage.js';
import {
findOriginalMessage,
getBroadcasts,
getMessageIdFromStr,
getOriginalMessage,
OriginalMessage,
} from '#main/utils/network/messageUtils.js';
import { Message } from 'discord.js';

export default class DeleteMsgCommand extends BasePrefixCommand {
public readonly data: CommandData = {
name: 'deletemsg',
description: 'Delete a message',
category: 'Network',
usage: 'deletemsg <message ID or link>',
examples: [
'deletemsg 123456789012345678',
'deletemsg https://discord.com/channels/123456789012345678/123456789012345678/123456789012345678',
],
aliases: ['delmsg', 'dmsg', 'delete', 'del'],
dbPermission: false,
};

public async execute(message: Message<true>, args: string[]): Promise<void> {
const originalMsgId = message.reference?.messageId ?? getMessageIdFromStr(args[0]);
const originalMsg = originalMsgId ? await this.getOriginalMessage(originalMsgId) : null;

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

const hub = await fetchHub(originalMsg.hubId);
if (!hub || !isStaffOrHubMod(message.author.id, hub)) {
await message.channel.send('You do not have permission to use this command.');
return;
}

const reply = await message.reply(`${emojis.loading} Deleting message...`);

const deleted = await deleteMessageFromHub(
originalMsg.hubId,
originalMsg.messageId,
Object.values(await getBroadcasts(originalMsg.messageId, originalMsg.hubId)),
).catch(() => null);

await reply.edit(
`${emojis.delete} Deleted messages from **${deleted?.deletedCount ?? '0'}** servers.`,
);
}

private async getOriginalMessage(messageId: string) {
const originalMsg =
(await getOriginalMessage(messageId)) ?? (await findOriginalMessage(messageId));
return originalMsg;
}

private async getOriginalMsgs(args: string[]): Promise<OriginalMessage[]> {
const promises = args.map(async (arg) => {
const messageId = getMessageIdFromStr(arg);
return messageId ? await this.getOriginalMessage(messageId) : null;
});

const results = await Promise.all(promises);
return results.filter((id): id is OriginalMessage => id !== null);
}
}
49 changes: 49 additions & 0 deletions src/commands/prefix/modpanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import BasePrefixCommand, { CommandData } from '#main/core/BasePrefixCommand.js';
import { fetchHub, isStaffOrHubMod } from '#main/utils/hub/utils.js';
import modActionsPanel from '#main/utils/moderation/modActions/modActionsPanel.js';
import {
findOriginalMessage,
getMessageIdFromStr,
getOriginalMessage,
} from '#main/utils/network/messageUtils.js';
import { Message } from 'discord.js';

export default class BlacklistPrefixCommand extends BasePrefixCommand {
public readonly data: CommandData = {
name: 'modpanel',
description: 'Blacklist a user or server from using the bot',
category: 'Moderation',
usage: 'blacklist <user ID or server ID>',
examples: [
'blacklist 123456789012345678',
'blacklist 123456789012345678',
'> Reply to a message with `blacklist` to blacklist the user who sent the message',
],
aliases: ['bl', 'modactions', 'modpanel', 'mod', 'ban'],
dbPermission: false,
};

public async execute(message: Message<true>, args: string[]) {
const originalMessageId = message.reference?.messageId ?? getMessageIdFromStr(args[0]);
const originalMessage = originalMessageId
? await this.getOriginalMessage(originalMessageId)
: null;

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

const hub = await fetchHub(originalMessage.hubId);
if (!hub || !isStaffOrHubMod(message.author.id, hub)) {
await message.channel.send('You do not have permission to use this command.');
return;
}

const modPanel = await modActionsPanel.buildMessage(message, originalMessage);
await message.reply({ embeds: [modPanel.embed], components: modPanel.buttons });
}
private async getOriginalMessage(messageId: string) {
return (await getOriginalMessage(messageId)) ?? (await findOriginalMessage(messageId)) ?? null;
}
}
1 change: 1 addition & 0 deletions src/config/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export default {
Hexcode: /^#[0-9A-F]{6}$/i,
ChannelMention: /<#|!|>/g,
ImgurImage: /https?:\/\/i\.imgur\.com\/[a-zA-Z0-9]+\.((jpg)|(jpeg)|(png)|(gif))/g,
MessageLink: /https:\/\/discord.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19})/g,
},

Links: {
Expand Down
8 changes: 6 additions & 2 deletions src/core/BaseClient.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import Constants from '#main/config/Constants.js';
import type BaseCommand from '#main/core/BaseCommand.js';
import type BasePrefixCommand from '#main/core/BasePrefixCommand.js';
import type { InteractionFunction } from '#main/decorators/Interaction.js';
import AntiSpamManager from '#main/managers/AntiSpamManager.js';
import UserDbManager from '#main/managers/UserDbManager.js';
import CooldownService from '#main/modules/CooldownService.js';
import EventLoader from '#main/modules/Loaders/EventLoader.js';
import Scheduler from '#main/modules/SchedulerService.js';
import type { RemoveMethods } from '#types/index.d.ts';
import { loadCommandFiles, loadInteractions } from '#utils/CommandUtils.js';
import { loadCommands, loadInteractions } from '#utils/CommandUtils.js';
import { loadLocales } from '#utils/Locale.js';
import { resolveEval } from '#utils/Utils.js';
import { ClusterClient, getInfo } from 'discord-hybrid-sharding';
Expand Down Expand Up @@ -37,8 +38,11 @@ export default class InterChatClient extends Client {
readonly cluster = new ClusterClient(this);
readonly eventLoader = new EventLoader(this);
readonly commandCooldowns = new CooldownService();

public readonly commands = new Collection<string, BaseCommand>();
public readonly interactions = new Collection<string, InteractionFunction>();
public readonly prefixCommands = new Collection<string, BasePrefixCommand>();

public readonly antiSpamManager = new AntiSpamManager({
spamThreshold: 4,
timeWindow: 5000,
Expand Down Expand Up @@ -87,7 +91,7 @@ export default class InterChatClient extends Client {
loadLocales('locales');

// load commands
loadCommandFiles(this.commands, this.interactions);
loadCommands(this.commands, this.prefixCommands, this.interactions);
loadInteractions(this.interactions);
this.eventLoader.load();

Expand Down
20 changes: 20 additions & 0 deletions src/core/BasePrefixCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Message, PermissionsBitField } from 'discord.js';

export interface CommandData {
name: string;
description: string;
category: 'Moderation' | 'Network'; // add more categories as needed
usage: string;
examples: string[];
aliases: string[];
dbPermission?: boolean;
cooldown?: number;
ownerOnly?: boolean;
requiredBotPermissions?: PermissionsBitField[];
requiredUserPermissions?: PermissionsBitField[];
}

export default abstract class BasePrefixCommand {
public abstract readonly data: CommandData;
public abstract execute(message: Message, args: string[]): Promise<void>;
}
25 changes: 12 additions & 13 deletions src/events/messageCreate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ConnectionMode } from '#main/config/Constants.js';
import BaseEventListener from '#main/core/BaseEventListener.js';
import HubSettingsManager from '#main/managers/HubSettingsManager.js';
import Logger from '#main/utils/Logger.js';
import { checkBlockedWords } from '#main/utils/network/blockwordsRunner.js';
import handlePrefixCommand from '#main/utils/PrefixCmdHandler.js';
import { generateJumpButton as getJumpButton } from '#utils/ComponentUtils.js';
import { getConnectionHubId, getHubConnections } from '#utils/ConnectedListUtils.js';
import db from '#utils/Db.js';
Expand Down Expand Up @@ -37,6 +39,11 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> {
async execute(message: Message) {
if (!message.inGuild() || !isHumanMessage(message)) return;

if (message.content.startsWith('c!')) {
await handlePrefixCommand(message, 'c!');
return;
}

const { connection, hubConnections } = await this.getConnectionAndHubConnections(message);
if (!connection?.connected || !hubConnections) return;

Expand Down Expand Up @@ -88,19 +95,6 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> {
embedColor: connection.embedColor as HexColorString,
});

await this.storeMessage(message, sendResult, connection, referredMsgData);
}

private async fetchReferredMessage(message: Message<true>): Promise<Message | null> {
return message.reference ? await message.fetchReference().catch(() => null) : null;
}

private async storeMessage(
message: Message<true>,
sendResult: NetworkWebhookSendResult[],
connection: connectedList,
referredMsgData: ReferredMsgData,
) {
await storeMessageData(
message,
sendResult,
Expand All @@ -110,6 +104,10 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> {
);
}

private async fetchReferredMessage(message: Message<true>): Promise<Message | null> {
return message.reference ? await message.fetchReference().catch(() => null) : null;
}

private async broadcastMessage(
message: Message<true>,
hub: Hub,
Expand Down Expand Up @@ -152,6 +150,7 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> {
return { messageRes, webhookURL: connection.webhookURL, mode };
}
catch (e) {
Logger.error(`Failed to send message to ${connection.channelId}`, e);
return { error: e.message, webhookURL: connection.webhookURL };
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/interactions/InactiveConnectInteraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class InactiveConnectInteraction {
t('connection.channelNotFound', locale, { emoji: emojis.no }),
);

await interaction.reply({ embeds: [notFoundEmbed], ephemeral: true });
await interaction.followUp({ embeds: [notFoundEmbed], ephemeral: true });
return;
}

Expand Down
18 changes: 11 additions & 7 deletions src/modules/HubJoinService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
ChatInputCommandInteraction,
GuildTextBasedChannel,
MessageComponentInteraction,
Snowflake,
} from 'discord.js';

export class HubJoinService {
Expand Down Expand Up @@ -58,7 +57,9 @@ export class HubJoinService {
return false;
}

if ((await this.isAlreadyInHub(channel.id)) || (await this.isBlacklisted(hub))) return false;
if ((await this.isAlreadyInHub(channel, hub.id)) || (await this.isBlacklisted(hub))) {
return false;
}

const onboardingSuccess = await this.processOnboarding(hub, channel);
if (!onboardingSuccess) return false;
Expand Down Expand Up @@ -119,13 +120,16 @@ export class HubJoinService {
});
}

private async isAlreadyInHub(channelId: Snowflake) {
const channelInHub = await db.connectedList.findFirst({ where: { channelId } });
private async isAlreadyInHub(channel: GuildTextBasedChannel, hubId: string) {
const channelInHub = await db.connectedList.findFirst({
where: { OR: [{ channelId: channel.id }, { serverId: channel.guildId, hubId }] },
include: { hub: { select: { name: true } } },
});

if (channelInHub) {
const otherHub = await db.hub.findFirst({ where: { id: channelInHub.hubId } });
await this.replyError('hub.alreadyJoined', {
channel: `<#${channelId}>`,
hub: `${otherHub?.name}`,
channel: `<#${channelInHub.channelId}>`,
hub: `${channelInHub.hub?.name}`,
emoji: emojis.no,
});
return true;
Expand Down
37 changes: 29 additions & 8 deletions src/modules/Loaders/CommandLoader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import BaseCommand from '#main/core/BaseCommand.js';
import BasePrefixCommand from '#main/core/BasePrefixCommand.js';
import { type Class, FileLoader, type ResourceLoader } from '#main/core/FileLoader.js';
import { InteractionFunction } from '#main/decorators/Interaction.js';
import Logger from '#utils/Logger.js';
Expand All @@ -9,15 +10,18 @@ import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));

export class CommandLoader implements ResourceLoader {
private readonly map: Collection<string, BaseCommand>;
private readonly commandMap: Collection<string, BaseCommand>;
private readonly prefixMap: Collection<string, BasePrefixCommand>;
private readonly interactionsMap: Collection<string, InteractionFunction>;
private readonly fileLoader: FileLoader;

constructor(
map: Collection<string, BaseCommand>,
commandMap: Collection<string, BaseCommand>,
prefixMap: Collection<string, BasePrefixCommand>,
interactionsMap: Collection<string, InteractionFunction>,
) {
this.map = map;
this.prefixMap = prefixMap;
this.commandMap = commandMap;
this.interactionsMap = interactionsMap;
this.fileLoader = new FileLoader(join(__dirname, '..', '..', 'commands'), { recursive: true });
}
Expand All @@ -28,14 +32,31 @@ export class CommandLoader implements ResourceLoader {

private async processFile(filePath: string): Promise<void> {
Logger.debug(`Importing command file: ${filePath}`);
const imported = await FileLoader.import<{ default: Class<BaseCommand> }>(filePath);
const imported = await FileLoader.import<{ default: Class<BaseCommand | BasePrefixCommand> }>(
filePath,
);
const command = new imported.default();
const fileName = filePath.replaceAll('\\', '/').split('/').pop() as string;

command.build(fileName.replace('.js', ''), {
commandsMap: this.map,
interactionsMap: this.interactionsMap,
});
// FIXME: do smth about this
if (command instanceof BasePrefixCommand) {
this.prefixMap.set(command.data.name, command);
}
else {
command.build(fileName.replace('.js', ''), {
commandsMap: this.commandMap,
interactionsMap: this.interactionsMap,
});
}
Logger.debug(`Finished loading command: ${command.data.name}`);
}
}

export interface ICommand {
readonly name: string;
readonly description: string;
readonly category: string;
readonly cooldown?: number;
readonly staffOnly?: boolean;
readonly type: 'prefix' | 'slash' | 'context';
}
2 changes: 2 additions & 0 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import BaseCommand from '#main/core/BaseCommand.js';
import BasePrefixCommand from '#main/core/BasePrefixCommand.js';
import { InteractionFunction } from '#main/decorators/Interaction.ts';
import AntiSpamManager from '#main/managers/AntiSpamManager.js';
import UserDbManager from '#main/managers/UserDbManager.js';
Expand Down Expand Up @@ -27,6 +28,7 @@ declare module 'discord.js' {
readonly description: string;
readonly commands: Collection<string, BaseCommand>;
readonly interactions: Collection<string, InteractionFunction | undefined>;
readonly prefixCommands: Collection<string, BasePrefixCommand>;

readonly commandCooldowns: CooldownService;
readonly reactionCooldowns: Collection<string, number>;
Expand Down
8 changes: 5 additions & 3 deletions src/utils/CommandUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type BaseCommand from '#main/core/BaseCommand.js';
import type BasePrefixCommand from '#main/core/BasePrefixCommand.js';
import { type InteractionFunction } from '#main/decorators/Interaction.js';
import { CommandLoader } from '#main/modules/Loaders/CommandLoader.js';
import { InteractionLoader } from '#main/modules/Loaders/InteractionLoader.js';
Expand All @@ -23,11 +24,12 @@ export const loadInteractions = async (map: Collection<string, InteractionFuncti
* Recursively loads all command files from the given directory and its subdirectories.
* @param commandDir The directory to load command files from.
*/
export const loadCommandFiles = async (
map: Collection<string, BaseCommand>,
export const loadCommands = async (
commandsMap: Collection<string, BaseCommand>,
prefixMap: Collection<string, BasePrefixCommand>,
interactionsMap: Collection<string, InteractionFunction>,
) => {
const loader = new CommandLoader(map, interactionsMap);
const loader = new CommandLoader(commandsMap, prefixMap, interactionsMap);
await loader.load();
};

Expand Down
Loading