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
2 changes: 1 addition & 1 deletion locales
21 changes: 7 additions & 14 deletions src/InterChat.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import db from './utils/Db.js';
import Logger from './utils/Logger.js';
import SuperClient from './SuperClient.js';
import SuperClient from './core/Client.js';
import CommandManager from './managers/CommandManager.js';
import { NetworkMessage } from './managers/NetworkManager.js';
import { check } from './utils/Profanity.js';
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
import { stripIndents } from 'common-tags';
Expand All @@ -17,7 +16,7 @@ class InterChat extends SuperClient {

this.once('ready', () => {
// initialize the client
this.init();
this.boot();

// initialize i18n for localization
loadLocales('locales/src/locales');
Expand Down Expand Up @@ -122,19 +121,13 @@ class InterChat extends SuperClient {
await logGuildLeave(guild, channels.goal);
});

// handle slash/ctx commands
this.on('messageCreate', (message) => this.networkManager.onMessageCreate(message));
this.on('interactionCreate', (interaction) =>
this.getCommandManager().handleInteraction(interaction),
this.commandManager.onInteractionCreate(interaction),
);
this.on('messageReactionAdd', (react, usr) =>
this.reactionUpdater.onMessageReactionAdd(react, usr),
);

// handle network reactions
this.on('messageReactionAdd', (react, usr) => this.getReactionUpdater().listen(react, usr));

// handle messages
this.on('messageCreate', async (message) => {
if (message.author.bot || message.system || message.webhookId) return;
this.getNetworkManager().handleNetworkMessage(message as NetworkMessage);
});

this.on('debug', (debug) => {
Logger.debug(debug);
Expand Down
71 changes: 71 additions & 0 deletions src/commands/BaseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import {
MessageComponentInteraction,
ModalSubmitInteraction,
RESTPostAPIApplicationCommandsJSONBody,
time,
RepliableInteraction,
} from 'discord.js';
import { InteractionFunction } from '../decorators/Interaction.js';
import { t } from '../utils/Locale.js';
import { emojis } from '../utils/Constants.js';

export type CmdInteraction = ChatInputCommandInteraction | ContextMenuCommandInteraction;

Expand All @@ -27,4 +31,71 @@ export default abstract class BaseCommand {
async handleComponents?(interaction: MessageComponentInteraction): Promise<unknown>;
async handleModals?(interaction: ModalSubmitInteraction): Promise<unknown>;
async autocomplete?(interaction: AutocompleteInteraction): Promise<unknown>;

async handleCooldown(interaction: RepliableInteraction): Promise<boolean> {
const remainingCooldown = this.getRemainingCooldown(interaction);

if (remainingCooldown) {
await this.sendCooldownError(interaction, remainingCooldown);
return true;
}

this.setUserCooldown(interaction);
return false;
}

async sendCooldownError(
interaction: RepliableInteraction,
remainingCooldown: number,
): Promise<void> {
const waitUntil = Math.round((Date.now() + remainingCooldown) / 1000);

await interaction.reply({
content: t(
{ phrase: 'errors.cooldown', locale: interaction.user.locale },
{ time: `${time(waitUntil, 'T')} (${time(waitUntil, 'R')})`, emoji: emojis.no },
),
ephemeral: true,
});
}

getRemainingCooldown(interaction: RepliableInteraction): number {
let remainingCooldown: number | undefined = undefined;

if (interaction.isChatInputCommand()) {
const subcommand = interaction.options.getSubcommand(false);
const subcommandGroup = interaction.options.getSubcommandGroup(false);

remainingCooldown = interaction.client.commandCooldowns?.getRemainingCooldown(
`${interaction.user.id}-${interaction.commandName}${subcommandGroup ? `-${subcommandGroup}` : ''}${subcommand ? `-${subcommand}` : ''}`,
);
}
else if (interaction.isContextMenuCommand()) {
remainingCooldown = interaction.client.commandCooldowns?.getRemainingCooldown(
`${interaction.user.id}-${interaction.commandName}`,
);
}

return remainingCooldown || 0;
}

setUserCooldown(interaction: RepliableInteraction): void {
if (!this.cooldown) return;

if (interaction.isChatInputCommand()) {
const subcommand = interaction.options.getSubcommand(false);
const subcommandGroup = interaction.options.getSubcommandGroup(false);

interaction.client.commandCooldowns?.setCooldown(
`${interaction.user.id}-${interaction.commandName}${subcommandGroup ? `-${subcommandGroup}` : ''}${subcommand ? `-${subcommand}` : ''}`,
this.cooldown,
);
}
else if (interaction.isContextMenuCommand()) {
interaction.client.commandCooldowns?.setCooldown(
`${interaction.user.id}-${interaction.commandName}`,
this.cooldown,
);
}
}
}
2 changes: 1 addition & 1 deletion src/commands/context-menu/blacklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export default class Blacklist extends BaseCommand {
},
);

const blacklistManager = interaction.client.getBlacklistManager();
const blacklistManager = interaction.client.blacklistManager;

// user blacklist
if (customId.suffix === 'user') {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/context-menu/editMsg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export default class EditMessage extends BaseCommand {
const userInput = interaction.fields.getTextInputValue('newMessage');
const hubSettings = new HubSettingsBitField(messageInDb.originalMsg.hub.settings);
const newMessage = hubSettings.has('HideLinks') ? replaceLinks(userInput) : userInput;
const networkManager = interaction.client.getNetworkManager();
const networkManager = interaction.client.networkManager;

if (
hubSettings.has('BlockInvites') &&
Expand Down
2 changes: 1 addition & 1 deletion src/commands/slash/Main/blacklist/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default class UserBlacklist extends BlacklistCommand {
return;
}

const blacklistManager = interaction.client.getBlacklistManager();
const blacklistManager = interaction.client.blacklistManager;
const subCommandGroup = interaction.options.getSubcommandGroup();
const serverOpt = interaction.options.getString('server', true);

Expand Down
2 changes: 1 addition & 1 deletion src/commands/slash/Main/blacklist/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default class Server extends BlacklistCommand {
const reason = interaction.options.getString('reason') ?? 'No reason provided.';
const duration = parse(`${interaction.options.getString('duration')}`);

const blacklistManager = interaction.client.getBlacklistManager();
const blacklistManager = interaction.client.blacklistManager;

if (subcommandGroup == 'add') {
// get ID if user inputted a @ mention
Expand Down
2 changes: 1 addition & 1 deletion src/commands/slash/Main/hub/browse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ export default class Browse extends Hub {
const webhook = await getOrCreateWebhook(channel);
if (!webhook) return;

const networkManager = interaction.client.getNetworkManager();
const networkManager = interaction.client.networkManager;
// finally make the connection
await db.connectedList.create({
data: {
Expand Down
8 changes: 7 additions & 1 deletion src/commands/slash/Main/hub/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ export default class Create extends Hub {
readonly cooldown = 60 * 60 * 1000; // 1 hour

async execute(interaction: ChatInputCommandInteraction<CacheType>) {
const locale = interaction.user.locale;
const { locale } = interaction.user;

const isOnCooldown = this.getRemainingCooldown(interaction);
if (isOnCooldown) return this.sendCooldownError(interaction, isOnCooldown);

const modal = new ModalBuilder()
.setTitle(t({ phrase: 'hub.create.modal.title', locale }))
Expand Down Expand Up @@ -166,6 +169,9 @@ export default class Create extends Hub {
)
.setTimestamp();

const command = Hub.subcommands.get('create');
command?.setUserCooldown(interaction);

await interaction.editReply({ embeds: [successEmbed] });
}
}
2 changes: 1 addition & 1 deletion src/commands/slash/Main/hub/join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class JoinSubCommand extends Hub {
if (!interaction.inCachedGuild()) return;

const locale = interaction.user.locale;
const networkManager = interaction.client.getNetworkManager();
const networkManager = interaction.client.networkManager;
// FIXME: Change later
const hubName = interaction.options.getString('hub') ?? 'Crib';
const invite = interaction.options.getString('invite');
Expand Down
2 changes: 1 addition & 1 deletion src/commands/slash/Main/hub/servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { paginate } from '../../../../utils/Pagination.js';
import db from '../../../../utils/Db.js';
import { simpleEmbed } from '../../../../utils/Utils.js';
import { t } from '../../../../utils/Locale.js';
import SuperClient from '../../../../SuperClient.js';
import SuperClient from '../../../../core/Client.js';

export default class Servers extends Hub {
async execute(interaction: ChatInputCommandInteraction) {
Expand Down
5 changes: 4 additions & 1 deletion src/commands/slash/Staff/purge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { emojis } from '../../../utils/Constants.js';
import { simpleEmbed, msToReadable, deleteMsgsFromDb } from '../../../utils/Utils.js';
import Logger from '../../../utils/Logger.js';
import { broadcastedMessages } from '@prisma/client';
import SuperClient from '../../../SuperClient.js';
import SuperClient from '../../../core/Client.js';

const limitOpt: APIApplicationCommandBasicOption = {
type: ApplicationCommandOptionType.Integer,
Expand Down Expand Up @@ -105,6 +105,9 @@ export default class Purge extends BaseCommand {
};

async execute(interaction: ChatInputCommandInteraction) {
const isOnCooldown = await this.handleCooldown(interaction);
if (isOnCooldown) return;

await interaction.deferReply({ fetchReply: true });

const subcommand = interaction.options.getSubcommand();
Expand Down
85 changes: 32 additions & 53 deletions src/SuperClient.ts → src/core/Client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
Client,
Client as Client,
IntentsBitField,
Partials,
Options,
Expand All @@ -9,29 +9,34 @@ import {
WebhookClient,
} from 'discord.js';
import { ClusterClient, getInfo } from 'discord-hybrid-sharding';
import { commandsMap, interactionsMap } from './commands/BaseCommand.js';
import db from './utils/Db.js';
import { commandsMap, interactionsMap } from '../commands/BaseCommand.js';
import db from '../utils/Db.js';
import Sentry from '@sentry/node';
import Scheduler from './services/SchedulerService.js';
import NSFWClient from './utils/NSFWDetection.js';
import CommandManager from './managers/CommandManager.js';
import NetworkManager from './managers/NetworkManager.js';
import ReactionUpdater from './updater/ReactionUpdater.js';
import CooldownService from './services/CooldownService.js';
import BlacklistManager from './managers/BlacklistManager.js';
import { RemoveMethods } from './typings/index.js';
import { isDevBuild } from './utils/Constants.js';
import Scheduler from '../services/SchedulerService.js';
import NSFWClient from '../utils/NSFWDetection.js';
import CommandManager from '../managers/CommandManager.js';
import NetworkManager from '../managers/NetworkManager.js';
import ReactionUpdater from '../updater/ReactionUpdater.js';
import CooldownService from '../services/CooldownService.js';
import BlacklistManager from '../managers/BlacklistManager.js';
import { RemoveMethods } from '../typings/index.js';
import { isDevBuild } from '../utils/Constants.js';
import { ActivityType } from 'discord.js';
import {
JoinLeaveLogger,
ModLogsLogger,
ProfanityLogger,
ReportLogger,
} from './services/HubLoggerService.js';
} from '../services/HubLoggerService.js';
import 'dotenv/config';
import { supportedLocaleCodes } from './utils/Locale.js';
import { supportedLocaleCodes } from '../utils/Locale.js';

export default abstract class SuperClient<R extends boolean = boolean> extends Client<R> {
// A static instance of the SuperClient class to be used globally.
public static instance: SuperClient;

private readonly scheduler = new Scheduler();

export default abstract class SuperClient extends Client {
readonly description = 'The only cross-server chatting bot you\'ll ever need.';
readonly version = process.env.npm_package_version ?? 'Unknown';
readonly commands = commandsMap;
Expand All @@ -41,19 +46,15 @@ export default abstract class SuperClient extends Client {
readonly commandCooldowns = new CooldownService();
readonly reactionCooldowns = new Collection<string, number>();
readonly cluster = new ClusterClient(this);

private readonly scheduler = new Scheduler();
private readonly commandHandler = new CommandManager(this);
private readonly networkHandler = new NetworkManager(this);
private readonly blacklistManager = new BlacklistManager(this.scheduler);
private readonly reactionUpdater = new ReactionUpdater(this);
private readonly nsfwDetector = new NSFWClient();
public readonly reportLogger = new ReportLogger(this);
public readonly profanityLogger = new ProfanityLogger(this);
public readonly modLogsLogger = new ModLogsLogger(this);
public readonly joinLeaveLogger = new JoinLeaveLogger(this);

private static self: SuperClient;
readonly commandManager = new CommandManager(this);
readonly networkManager = new NetworkManager();
readonly blacklistManager = new BlacklistManager(this.scheduler);
readonly nsfwDetector = new NSFWClient();
readonly reportLogger = new ReportLogger(this);
readonly reactionUpdater = new ReactionUpdater(this);
readonly profanityLogger = new ProfanityLogger(this);
readonly modLogsLogger = new ModLogsLogger(this);
readonly joinLeaveLogger = new JoinLeaveLogger(this);

constructor() {
super({
Expand Down Expand Up @@ -87,7 +88,7 @@ export default abstract class SuperClient extends Client {
status: 'idle',
activities: [
{
state: 'Watching over 100+ cross-server hubs',
state: 'Watching over 200+ cross-server hubs',
name: 'custom',
type: ActivityType.Custom,
},
Expand All @@ -100,8 +101,8 @@ export default abstract class SuperClient extends Client {
* Initializes the SuperClient instance.
* Sets the instance to the current object and initializes Sentry error monitoring and handling if not in development mode.
*/
protected init() {
SuperClient.self = this;
protected boot() {
SuperClient.instance = this;

if (!isDevBuild) {
// error monitoring & handling
Expand All @@ -114,13 +115,6 @@ export default abstract class SuperClient extends Client {
}
}

/**
* Returns the instance of the SuperClient class.
*/
public static getInstance(): SuperClient {
return this.self;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
static resolveEval = <T>(value: T[]) =>
value?.find((res) => !!res) as RemoveMethods<T> | undefined;
Expand All @@ -145,22 +139,7 @@ export default abstract class SuperClient extends Client {
return (fetch?.locale as supportedLocaleCodes | undefined) || 'en';
}

getCommandManager(): CommandManager {
return this.commandHandler;
}
getNetworkManager(): NetworkManager {
return this.networkHandler;
}
getScheduler(): Scheduler {
return this.scheduler;
}
getBlacklistManager(): BlacklistManager {
return this.blacklistManager;
}
getReactionUpdater(): ReactionUpdater {
return this.reactionUpdater;
}
getNSFWDetector(): NSFWClient {
return this.nsfwDetector;
}
}
2 changes: 1 addition & 1 deletion src/Factory.ts → src/core/Factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import SuperClient from './SuperClient.js';
import SuperClient from './Client.js';

export default abstract class Factory {
protected readonly client: SuperClient;
Expand Down
Loading