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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"sonarlint.connectedMode.project": {
"connectionId": "discord-interchat",
"projectKey": "Discord-InterChat_InterChat"
}
},
"codescene.previewCodeHealthMonitoring": true
}
26 changes: 20 additions & 6 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,18 @@ model ServerInfraction {
@@index([serverId, hubId, status])
}

// TODO: major refactor needed for mode and profFilter thing
model connectedList {
id String @id @default(auto()) @map("_id") @db.ObjectId
mode Int @default(0) // 0 = compact, 1 = embed
channelId String @unique // channel can be thread, or a normal channel
parentId String? // ID of the parent channel, if it's a thread @map("parentChannelId")
parentId String? // ID of the parent channel, if it's a thread
serverId String
connected Boolean
compact Boolean
invite String?
profFilter Boolean
embedColor String?
webhookURL String
lastActive DateTime? @default(now())
// TODO: rename to createdAt
date DateTime @default(now())
hub Hub? @relation(fields: [hubId], references: [id])
hubId String @db.ObjectId
Expand All @@ -121,18 +121,32 @@ model Hub {
appealCooldownHours Int @default(168) // 7 days
createdAt DateTime @default(now())
settings Int // each bit is a setting
// all the stuff below is relations to other collections
// relations
invites HubInvite[]
moderators HubModerator[]
connections connectedList[]
logConfig HubLogConfig[]
msgBlockList MessageBlockList[]
originalMessages originalMessages[]
userInfractions UserInfraction[]
serverInfractions ServerInfraction[]

@@index([id, name, ownerId])
}

model MessageBlockList {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
words String
createdBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
hub Hub @relation(fields: [hubId], references: [id])
hubId String @db.ObjectId

@@index([id, words])
}

model HubLogConfig {
id String @id @default(auto()) @map("_id") @db.ObjectId
modLogs String?
Expand All @@ -147,7 +161,7 @@ model HubLogConfig {
}

model HubInvite {
code String @id @default(nanoid(10)) @map("_id")
code String @id @default(nanoid(10)) @map("_id")
expires DateTime
hub Hub @relation(fields: [hubId], references: [id])
hubId String @db.ObjectId
Expand Down
51 changes: 24 additions & 27 deletions src/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@ import 'dotenv/config';
const shardsPerClusters = 5;
const clusterManager = new ClusterManager('build/index.js', {
token: process.env.DISCORD_TOKEN,
totalShards: 'auto',
totalClusters: 'auto',
shardsPerClusters,
});

clusterManager.extend(new HeartbeatManager({ interval: 60 * 1000, maxMissedHeartbeats: 2 }));
clusterManager.extend(new HeartbeatManager({ interval: 10 * 1000, maxMissedHeartbeats: 2 }));
clusterManager.extend(new ReClusterManager());

clusterManager.on('clusterReady', (cluster) => {
Logger.info(
`Cluster ${cluster.id} is ready with shards ${cluster.shardList[0]}...${cluster.shardList.at(-1)}.`,
);

if (cluster.id === clusterManager.totalClusters - 1) startTasks();

cluster.on('message', async (message) => {
if (message === 'recluster') {
Logger.info('Recluster requested, starting recluster...');
Expand All @@ -34,38 +41,28 @@ clusterManager.on('clusterReady', (cluster) => {
});
});


// clusterManager.on('clientRequest', (n) => {
// cons
// })

// spawn clusters and start the api that handles nsfw filter and votes
clusterManager
.spawn({ timeout: -1 })
.then(() => {
const scheduler = new Scheduler();
clusterManager.spawn({ timeout: -1 });

deleteExpiredInvites().catch(Logger.error);

// store network message timestamps to connectedList every minute
scheduler.addRecurringTask('storeMsgTimestamps', 10 * 60 * 1000, storeMsgTimestamps);
scheduler.addRecurringTask('deleteExpiredInvites', 60 * 60 * 1000, deleteExpiredInvites);
function startTasks() {
pauseIdleConnections(clusterManager).catch(Logger.error);
deleteExpiredInvites().catch(Logger.error);

// production only tasks
if (Constants.isDevBuild) return;
const scheduler = new Scheduler();

// store network message timestamps to connectedList every minute
scheduler.addRecurringTask('storeMsgTimestamps', 10 * 60 * 1000, storeMsgTimestamps);
scheduler.addRecurringTask('cleanupTasks', 60 * 60 * 1000, () => {
deleteExpiredInvites().catch(Logger.error);
pauseIdleConnections(clusterManager).catch(Logger.error);
});

// production only tasks
if (!Constants.isDevBuild) {
scheduler.addRecurringTask('syncBotlistStats', 10 * 60 * 10_000, async () => {
// perform start up tasks
const serverCount = (await clusterManager.fetchClientValues('guilds.cache.size')).reduce(
(p: number, n: number) => p + n,
0,
);
const servers = await clusterManager.fetchClientValues('guilds.cache.size');
const serverCount = servers.reduce((p: number, n: number) => p + n, 0);
syncBotlistStats({ serverCount, shardCount: clusterManager.totalShards });
});
scheduler.addRecurringTask('pauseIdleConnections', 60 * 60 * 1000, () =>
pauseIdleConnections(clusterManager),
);
})
.catch(Logger.error);
}
}
137 changes: 137 additions & 0 deletions src/commands/slash/Main/hub/blockwords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import HubCommand from '#main/commands/slash/Main/hub/index.js';
import { emojis } from '#main/config/Constants.js';
import { RegisterInteractionHandler } from '#main/decorators/Interaction.js';
import { CustomID } from '#utils/CustomID.js';
import db from '#utils/Db.js';
import { isStaffOrHubMod } from '#utils/hub/utils.js';
import { t } from '#utils/Locale.js';
import {
buildBlockWordsListEmbed,
buildBlockWordsModal,
buildModifyBlockedWordsBtn,
sanitizeWords,
} from '#utils/moderation/blockedWords.js';
import { Hub, MessageBlockList } from '@prisma/client';
import {
type ButtonInteraction,
type ChatInputCommandInteraction,
type ModalSubmitInteraction,
} from 'discord.js';

export default class BlockWordCommand extends HubCommand {
async execute(interaction: ChatInputCommandInteraction) {
const hubName = interaction.options.getString('hub', true);
const hub = await this.fetchHub({ name: hubName });

if (!hub || !isStaffOrHubMod(interaction.user.id, hub)) {
const locale = await this.getLocale(interaction);
await this.replyEmbed(interaction, t('hub.notFound_mod', locale, { emoji: emojis.no }), {
ephemeral: true,
});
return;
}

switch (interaction.options.getSubcommand()) {
case 'modify':
await this.handleModifySubcommand(interaction, hub);
break;
case 'create':
await this.handleAdd(interaction, hub);
break;
default:
break;
}
}

@RegisterInteractionHandler('blockwordsButton', 'modify')
async handleModifyButtons(interaction: ButtonInteraction) {
const customId = CustomID.parseCustomId(interaction.customId);
const [hubId, ruleId] = customId.args;

const hub = await this.fetchHub({ id: hubId });

if (!hub || !isStaffOrHubMod(interaction.user.id, hub)) {
const locale = await this.getLocale(interaction);
await this.replyEmbed(interaction, t('hub.notFound_mod', locale, { emoji: emojis.no }), {
ephemeral: true,
});
return;
}

const blockWords = hub.msgBlockList;
const presetRule = blockWords.find((r) => r.id === ruleId);

if (!presetRule) {
await interaction.reply({ content: 'This rule does not exist.', ephemeral: true });
return;
}

const modal = buildBlockWordsModal(hub.id, { presetRule });
await interaction.showModal(modal);
}

@RegisterInteractionHandler('blockwordsModal')
async handleModals(interaction: ModalSubmitInteraction) {
const customId = CustomID.parseCustomId(interaction.customId);
const [hubId, ruleId] = customId.args as [string, string?];

const hub = await this.fetchHub({ id: hubId });
if (!hub) return;

await interaction.reply({
content: `${emojis.loading} Validating blocked words...`,
ephemeral: true,
});

const name = interaction.fields.getTextInputValue('name');
const newWords = sanitizeWords(interaction.fields.getTextInputValue('words'));
if (!ruleId) {
if (hub.msgBlockList.length >= 2) {
await interaction.editReply('You can only have 2 block word rules per hub.');
return;
}

await db.messageBlockList.create({
data: { hubId, name, createdBy: interaction.user.id, words: newWords },
});
await interaction.editReply(`${emojis.yes} Rule added.`);
}
else if (newWords.length === 0) {
await db.messageBlockList.delete({ where: { id: ruleId } });
await interaction.editReply(`${emojis.yes} Rule removed.`);
}
else {
await db.messageBlockList.update({ where: { id: ruleId }, data: { words: newWords, name } });
await interaction.editReply(`${emojis.yes} Rule updated.`);
}
}

private async handleModifySubcommand(
interaction: ChatInputCommandInteraction,
hub: Hub & { msgBlockList: MessageBlockList[] },
) {
const blockWords = hub.msgBlockList;

if (!blockWords.length) {
await this.replyEmbed(
interaction,
'No block word rules are in this hub yet. Use `/hub blockwords add` to add some.',
{ ephemeral: true },
);
return;
}

const embed = buildBlockWordsListEmbed(blockWords);
const buttons = buildModifyBlockedWordsBtn(hub.id, blockWords);
await interaction.reply({ embeds: [embed], components: [buttons] });
}

private async handleAdd(interaction: ChatInputCommandInteraction | ButtonInteraction, hub: Hub) {
const modal = buildBlockWordsModal(hub.id);
await interaction.showModal(modal);
}

private async fetchHub({ id, name }: { id?: string; name?: string }) {
return await db.hub.findFirst({ where: { id, name }, include: { msgBlockList: true } });
}
}
21 changes: 20 additions & 1 deletion src/commands/slash/Main/hub/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,25 @@ export default class HubCommand extends BaseCommand {
},
],
},
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: 'blockwords',
description: 'Manage blocked words in your hub.',
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: 'create',
description: 'Create a new blocked word rule to your hub.',
options: [hubOption],
},
{
type: ApplicationCommandOptionType.Subcommand,
name: 'modify',
description: 'Modify an existing blocked word rule in your hub.',
options: [hubOption],
},
],
},
],
};

Expand All @@ -416,7 +435,7 @@ export default class HubCommand extends BaseCommand {
}

async autocomplete(interaction: AutocompleteInteraction): Promise<void> {
const managerCmds = ['edit', 'settings', 'invite', 'moderator', 'logging', 'appeal'];
const managerCmds = ['edit', 'settings', 'invite', 'moderator', 'logging', 'appeal', 'blockwords'];
const modCmds = ['servers'];

const subcommand = interaction.options.getSubcommand();
Expand Down
6 changes: 6 additions & 0 deletions src/core/BaseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
type RESTPostAPIChatInputApplicationCommandsJSONBody,
type RESTPostAPIContextMenuApplicationCommandsJSONBody,
Collection,
Interaction,
time,
} from 'discord.js';

Expand Down Expand Up @@ -175,4 +176,9 @@ export default abstract class BaseCommand {
MetadataHandler.loadMetadata(command, map);
Logger.debug(`Finished adding interactions for command: ${command.data.name}`);
}

protected async getLocale(interaction: Interaction): Promise<supportedLocaleCodes> {
const { userManager } = interaction.client;
return await userManager.getUserLocale(interaction.user.id);
}
}
17 changes: 0 additions & 17 deletions src/events/shardReady.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/managers/VoteManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Constants, { emojis } from '#main/config/Constants.js';
import UserDbManager from '#main/managers/UserDbManager.js';
import Scheduler from '#main/modules/SchedulerService.js';
import Logger from '#main/utils/Logger.js';
import Logger from '#utils/Logger.js';
import type { WebhookPayload } from '#types/topgg.d.ts';
import db from '#utils/Db.js';
import { getOrdinalSuffix } from '#utils/Utils.js';
Expand Down
2 changes: 2 additions & 0 deletions src/utils/CustomID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export class CustomID {
* @param values - The value to add as an argument.
* @returns CustomID - The CustomID instance for method chaining.
*/

// TODO: Rename this to set args and add a new method for adding args
addArgs(...values: string[]): CustomID {
if (!values) return this;

Expand Down
Loading
Loading