Skip to content

Commit

Permalink
feat(hub): add MessageBlockList (#163)
Browse files Browse the repository at this point in the history
- Change the type of `dbReferrence` in `ReferredMsgData` interface to use the new `OriginalMessage` and `Broadcast` types
- Change the type of `dbReferredAuthor` in `ReferredMsgData` interface to use the new `UserData` type
- Add a new `MessageBlockList` model to the Prisma schema to store a list of blocked words for each hub
- Introduce a new `mode` field in the `connectedList` model to track the mode (compact or embed) for each channel connection
  • Loading branch information
dev-737 authored Oct 15, 2024
1 parent f0c111e commit 10ee79f
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 58 deletions.
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

0 comments on commit 10ee79f

Please sign in to comment.