diff --git a/.env.example b/.env.example index 9acff9fde..7bec7736d 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,4 @@ NETWORK_API_KEY="your network api key" # for posting to global chat (ask devoid) NODE_ENV=development # change to production when deploying DEBUG=false # set to true to enable debug logging PORT=3000 # or anything else for production +NSFW_AI_MODEL=MobileNetV2 # InceptionV3 for prod diff --git a/.vscode/settings.json b/.vscode/settings.json index 20e53db5a..b9f178ddf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,6 @@ "javascript", "typescript" ], - "eslint.experimental.useFlatConfig": true + "eslint.experimental.useFlatConfig": true, + "typescript.tsdk": "node_modules/typescript/lib" } \ No newline at end of file diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index e98ac6911..000000000 Binary files a/bun.lockb and /dev/null differ diff --git a/package.json b/package.json index 8067641a2..4f61075bb 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "express": "^4.19.2", "google-translate-api-x": "^10.6.8", "husky": "^9.0.11", + "ioredis": "^5.4.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "lz-string": "^1.5.0", @@ -40,12 +41,12 @@ "winston": "^3.13.0" }, "devDependencies": { - "@stylistic/eslint-plugin": "^2.2.1", + "@stylistic/eslint-plugin": "^2.3.0", "@types/common-tags": "^1.8.4", "@types/express": "^4.17.21", "@types/js-yaml": "^4.0.9", - "@types/lodash": "^4.17.5", - "@types/node": "^20.14.2", + "@types/lodash": "^4.17.6", + "@types/node": "^20.14.9", "@types/source-map-support": "^0.5.10", "cz-conventional-changelog": "^3.3.0", "eslint": "8.57.0", @@ -54,8 +55,8 @@ "prisma": "^5.15.0", "standard-version": "^9.5.0", "tsc-watch": "^6.2.0", - "typescript": "^5.4.5", - "typescript-eslint": "^7.13.0" + "typescript": "^5.5.3", + "typescript-eslint": "^7.15.0" }, "config": { "commitizen": { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ceeca019c..7fffa3641 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,5 +1,5 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" previewFeatures = ["tracing"] } @@ -8,11 +8,6 @@ datasource db { url = env("DATABASE_URL") } -type MessageDataChannelAndMessageIds { - channelId String - messageId String -} - type MessageDataReference { channelId String guildId String? @@ -73,10 +68,9 @@ type hubBlacklist { } model blacklistedServers { - id String @id @default(auto()) @map("_id") @db.ObjectId - serverId String @unique - serverName String - hubs hubBlacklist[] + id String @id @map("_id") @db.String + serverName String + blacklistedFrom hubBlacklist[] } model connectedList { @@ -146,21 +140,8 @@ model broadcastedMessages { originalMsgId String @db.String } -model userBadges { - userId String @id @map("_id") - badges String[] -} - -model blacklistedUsers { - id String @id @default(auto()) @map("_id") @db.ObjectId - userId String @unique - username String - hubs hubBlacklist[] -} - model userData { - id String @id @default(auto()) @map("_id") @db.ObjectId - userId String @unique + id String @id @map("_id") @db.String voteCount Int @default(0) // username is only guarenteed to be set and/or used for blacklisted users username String? diff --git a/src/api/index.ts b/src/api/index.ts index 4a6652871..7937d8483 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -14,5 +14,5 @@ export const startApi = (data: { voteManager: VoteManager }) => { if (data.voteManager) app.use(dblRoute(data.voteManager)); const port = process.env.PORT; - app.listen(port, () => Logger.info(`API listening on port http://localhost:${port}.`)); + app.listen(port, () => Logger.info(`API listening on http://localhost:${port}.`)); }; diff --git a/src/api/routes/nsfw.ts b/src/api/routes/nsfw.ts index 7ebde2d77..0bf2797ea 100644 --- a/src/api/routes/nsfw.ts +++ b/src/api/routes/nsfw.ts @@ -2,7 +2,7 @@ import Logger from '../../utils/Logger.js'; import { Router } from 'express'; import { captureException } from '@sentry/node'; import { node } from '@tensorflow/tfjs-node'; -import { REGEX, isDevBuild } from '../../utils/Constants.js'; +import { REGEX } from '../../utils/Constants.js'; import { createRequire } from 'module'; import { NSFWJS } from 'nsfwjs'; @@ -10,7 +10,7 @@ const require = createRequire(import.meta.url); const { load } = require('nsfwjs'); // InceptionV3 is more accurate but slower and takes up a shit ton of memory -const nsfwModel: NSFWJS = await load(isDevBuild ? 'MobileNetV2' : 'InceptionV3'); +const nsfwModel: NSFWJS = await load(process.env.NSFW_AI_MODEL); const router = Router(); router.post('/nsfw', async (req, res) => { diff --git a/src/cluster.ts b/src/cluster.ts index d6abadd43..83bae1a8c 100644 --- a/src/cluster.ts +++ b/src/cluster.ts @@ -1,15 +1,17 @@ import db from './utils/Db.js'; import Logger from './utils/Logger.js'; import Scheduler from './services/SchedulerService.js'; -import syncBotlistStats from './scripts/tasks/syncBotlistStats.js'; -import updateBlacklists from './scripts/tasks/updateBlacklists.js'; -import deleteExpiredInvites from './scripts/tasks/deleteExpiredInvites.js'; -import pauseIdleConnections from './scripts/tasks/pauseIdleConnections.js'; +import syncBotlistStats from './tasks/syncBotlistStats.js'; +import updateBlacklists from './tasks/updateBlacklists.js'; +import storeMsgTimestamps from './tasks/storeMsgTimestamps.js'; +import deleteExpiredInvites from './tasks/deleteExpiredInvites.js'; +import pauseIdleConnections from './tasks/pauseIdleConnections.js'; import { startApi } from './api/index.js'; import { isDevBuild } from './utils/Constants.js'; +import { getUsername } from './utils/Utils.js'; import { VoteManager } from './managers/VoteManager.js'; import { ClusterManager } from 'discord-hybrid-sharding'; -import { getUsername, wait } from './utils/Utils.js'; +import { getAllConnections } from './utils/ConnectedList.js'; import 'dotenv/config'; const clusterManager = new ClusterManager('build/index.js', { @@ -18,52 +20,55 @@ const clusterManager = new ClusterManager('build/index.js', { totalClusters: 'auto', }); -clusterManager.on('clusterCreate', async (cluster) => { - // if it is the last cluster - if (cluster.id === clusterManager.totalClusters - 1) { +const voteManager = new VoteManager(clusterManager); +voteManager.on('vote', async (vote) => { + const username = (await getUsername(clusterManager, vote.user)) ?? undefined; + await voteManager.incrementUserVote(vote.user, username); + await voteManager.addVoterRole(vote.user); + await voteManager.announceVote(vote); +}); + +startApi({ voteManager }); + +// spawn clusters and start the api that handles nsfw filter and votes +clusterManager + .spawn({ timeout: -1 }) + .then(async () => { const scheduler = new Scheduler(); - // remove expired blacklists or set new timers for them - const serverQuery = await db.blacklistedServers.findMany({ - where: { hubs: { some: { expires: { isSet: true } } } }, - }); - const userQuery = await db.userData.findMany({ - where: { blacklistedFrom: { some: { expires: { isSet: true } } } }, - }); + const blacklistQuery = { where: { blacklistedFrom: { some: { expires: { isSet: true } } } } }; - updateBlacklists(serverQuery, scheduler).catch(Logger.error); - updateBlacklists(userQuery, scheduler).catch(Logger.error); + // populate cache + await db.blacklistedServers.findMany(blacklistQuery); + await db.userData.findMany(blacklistQuery); - // code must be in production to run these tasks - if (isDevBuild) return; - // give time for shards to connect for these tasks - await wait(10_000); + updateBlacklists(clusterManager).catch(Logger.error); + deleteExpiredInvites().catch(Logger.error); + if (isDevBuild) return; // perform start up tasks - syncBotlistStats(clusterManager).catch(Logger.error); - deleteExpiredInvites().catch(Logger.error); + const serverCount = (await clusterManager.fetchClientValues('guilds.cache.size')).reduce( + (p: number, n: number) => p + n, + 0, + ); + + syncBotlistStats({ serverCount, shardCount: clusterManager.totalShards }).catch(Logger.error); pauseIdleConnections(clusterManager).catch(Logger.error); + // store network message timestamps to connectedList every minute + scheduler.addRecurringTask('storeMsgTimestamps', 60 * 1_000, () => storeMsgTimestamps); scheduler.addRecurringTask('deleteExpiredInvites', 60 * 60 * 1000, deleteExpiredInvites); - scheduler.addRecurringTask('pauseIdleConnections', 60 * 60 * 1000, () => - pauseIdleConnections(clusterManager), + scheduler.addRecurringTask('populateConnectionCache', 5 * 60 * 1000, () => + getAllConnections({ connected: true }), + ); + scheduler.addRecurringTask('deleteExpiredBlacklists', 10 * 1000, () => + updateBlacklists(clusterManager), ); scheduler.addRecurringTask('syncBotlistStats', 10 * 60 * 10_000, () => - syncBotlistStats(clusterManager), + syncBotlistStats({ serverCount, shardCount: clusterManager.totalShards }), ); - } -}); - -const voteManager = new VoteManager(clusterManager); -voteManager.on('vote', async (vote) => { - const username = (await getUsername(clusterManager, vote.user)) ?? undefined; - await voteManager.incrementUserVote(vote.user, username); - await voteManager.addVoterRole(vote.user); - await voteManager.announceVote(vote); -}); - -// spawn clusters and start the api that handles nsfw filter and votes -clusterManager - .spawn({ timeout: -1 }) - .then(() => startApi({ voteManager })) + scheduler.addRecurringTask('pauseIdleConnections', 60 * 60 * 1000, () => + pauseIdleConnections(clusterManager), + ); + }) .catch(Logger.error); diff --git a/src/commands/context-menu/blacklist.ts b/src/commands/context-menu/blacklist.ts index e6f75596c..633b96862 100644 --- a/src/commands/context-menu/blacklist.ts +++ b/src/commands/context-menu/blacklist.ts @@ -59,6 +59,14 @@ export default class Blacklist extends BaseCommand { return; } + if (messageInDb.originalMsg.authorId === interaction.user.id) { + await interaction.reply({ + content: ' Nuh uh! You\'re stuck with us.', + ephemeral: true, + }); + return; + } + const server = await interaction.client.fetchGuild(messageInDb.originalMsg.serverId); const user = await interaction.client.users.fetch(messageInDb.originalMsg.authorId); @@ -177,7 +185,7 @@ export default class Blacklist extends BaseCommand { const reason = interaction.fields.getTextInputValue('reason'); const duration = parse(interaction.fields.getTextInputValue('duration')); - const expires = duration ? new Date(Date.now() + duration) : undefined; + const expires = duration ? new Date(Date.now() + duration) : null; const successEmbed = new EmbedBuilder().setColor('Green').addFields( { @@ -192,11 +200,24 @@ export default class Blacklist extends BaseCommand { }, ); - const blacklistManager = interaction.client.blacklistManager; + const { userManager } = interaction.client; // user blacklist if (customId.suffix === 'user') { const user = await interaction.client.users.fetch(originalMsg.authorId).catch(() => null); + + if (!user) { + await interaction.reply({ + embeds: [ + simpleEmbed( + `${emojis.neutral} Unable to fetch user. They may have deleted their account?`, + ), + ], + ephemeral: true, + }); + return; + } + successEmbed.setDescription( t( { phrase: 'blacklist.user.success', locale }, @@ -204,24 +225,15 @@ export default class Blacklist extends BaseCommand { ), ); - await blacklistManager.addUserBlacklist( - originalMsg.hubId, - originalMsg.authorId, + await userManager.addBlacklist({ id: user.id, name: user.username }, originalMsg.hubId, { reason, - interaction.user.id, + moderatorId: interaction.user.id, expires, - ); + }); - if (expires) { - blacklistManager.scheduleRemoval('user', originalMsg.authorId, originalMsg.hubId, expires); - } if (user) { - blacklistManager - .notifyBlacklist('user', originalMsg.authorId, { - hubId: originalMsg.hubId, - expires, - reason, - }) + userManager + .sendNotification({ target: user, hubId: originalMsg.hubId, expires, reason }) .catch(() => null); await logBlacklist(originalMsg.hubId, interaction.client, { @@ -241,6 +253,7 @@ export default class Blacklist extends BaseCommand { // server blacklist else { + const { serverBlacklists } = interaction.client; const server = await interaction.client.fetchGuild(originalMsg.serverId); successEmbed.setDescription( @@ -250,34 +263,25 @@ export default class Blacklist extends BaseCommand { ), ); - await blacklistManager.addServerBlacklist( - originalMsg.serverId, + await serverBlacklists.addBlacklist( + { name: server?.name ?? 'Unknown Server', id: originalMsg.serverId }, originalMsg.hubId, - reason, - interaction.user.id, - expires, + { + reason, + moderatorId: interaction.user.id, + expires, + }, ); // Notify server of blacklist - await blacklistManager.notifyBlacklist('server', originalMsg.serverId, { + await serverBlacklists.sendNotification({ + target: { id: originalMsg.serverId }, hubId: originalMsg.hubId, expires, reason, }); - if (expires) { - blacklistManager.scheduleRemoval( - 'server', - originalMsg.serverId, - originalMsg.hubId, - expires, - ); - } - - await deleteConnections({ - serverId: originalMsg.serverId, - hubId: originalMsg.hubId, - }); + await deleteConnections({ serverId: originalMsg.serverId, hubId: originalMsg.hubId }); if (server) { await logBlacklist(originalMsg.hubId, interaction.client, { diff --git a/src/commands/context-menu/deleteMsg.ts b/src/commands/context-menu/deleteMsg.ts index f838bc72b..96e9bffbf 100644 --- a/src/commands/context-menu/deleteMsg.ts +++ b/src/commands/context-menu/deleteMsg.ts @@ -10,6 +10,7 @@ import { REGEX, emojis } from '../../utils/Constants.js'; import { t } from '../../utils/Locale.js'; import { logMsgDelete } from '../../utils/HubLogger/ModLogs.js'; import { captureException } from '@sentry/node'; +import { getAllConnections } from '../../utils/ConnectedList.js'; export default class DeleteMessage extends BaseCommand { readonly data: RESTPostAPIApplicationCommandsJSONBody = { @@ -80,10 +81,10 @@ export default class DeleteMessage extends BaseCommand { let passed = 0; + const allConnections = await getAllConnections(); + for await (const dbMsg of originalMsg.broadcastMsgs) { - const connection = interaction.client.connectionCache.find( - (c) => c.channelId === dbMsg.channelId, - ); + const connection = allConnections?.find((c) => c.channelId === dbMsg.channelId); if (!connection) break; diff --git a/src/commands/context-menu/messageInfo.ts b/src/commands/context-menu/messageInfo.ts index d141a040e..eabae61ac 100644 --- a/src/commands/context-menu/messageInfo.ts +++ b/src/commands/context-menu/messageInfo.ts @@ -25,6 +25,7 @@ import { RegisterInteractionHandler } from '../../decorators/Interaction.js'; import { supportedLocaleCodes, t } from '../../utils/Locale.js'; import { simpleEmbed } from '../../utils/Utils.js'; import { sendHubReport } from '../../utils/HubLogger/Report.js'; +import { getAllConnections } from '../../utils/ConnectedList.js'; export default class MessageInfo extends BaseCommand { readonly data: RESTPostAPIApplicationCommandsJSONBody = { @@ -76,7 +77,7 @@ export default class MessageInfo extends BaseCommand { const components = MessageInfo.buildButtons(target.id, interaction.user.locale); - const guildConnected = interaction.client.connectionCache.find( + const guildConnected = (await getAllConnections())?.find( (c) => c.serverId === originalMsg.serverId && c.hubId === originalMsg.hub?.id, ); @@ -124,9 +125,7 @@ export default class MessageInfo extends BaseCommand { const author = await interaction.client.users.fetch(originalMsg.authorId); const server = await interaction.client.fetchGuild(originalMsg.serverId); - const guildConnected = interaction.client.connectionCache.find( - (c) => c.serverId === server?.id, - ); + const guildConnected = (await getAllConnections())?.find((c) => c.serverId === server?.id); if (interaction.isButton()) { // component builders taken from the original message @@ -349,7 +348,8 @@ export default class MessageInfo extends BaseCommand { const reason = interaction.fields.getTextInputValue('reason'); const message = await interaction.channel?.messages.fetch(messageId).catch(() => null); const content = message?.content || message?.embeds[0].description || undefined; - const attachmentUrl = content?.match(REGEX.STATIC_IMAGE_URL)?.at(0) ?? message?.embeds[0]?.image?.url; + const attachmentUrl = + content?.match(REGEX.STATIC_IMAGE_URL)?.at(0) ?? message?.embeds[0]?.image?.url; await sendHubReport(messageInDb.originalMsg.hub.id, interaction.client, { userId: authorId, diff --git a/src/commands/slash/Information/stats.ts b/src/commands/slash/Information/stats.ts index e50a745dc..1a3e8801f 100644 --- a/src/commands/slash/Information/stats.ts +++ b/src/commands/slash/Information/stats.ts @@ -29,7 +29,6 @@ export default class Stats extends BaseCommand { await interaction.deferReply(); const { originalMessages, hubs } = db; - const totalConnections = interaction.client.connectionCache.size; const totalHubs = await hubs?.count(); const totalNetworkMessages = await originalMessages.count(); @@ -74,7 +73,6 @@ export default class Stats extends BaseCommand { name: 'Hub Stats', value: stripIndents` Total Hubs: ${totalHubs} - Total Connected: ${totalConnections} Messages (Today): ${totalNetworkMessages}`, inline: false, }, diff --git a/src/commands/slash/Information/vote.ts b/src/commands/slash/Information/vote.ts index e377afba6..8b005486b 100644 --- a/src/commands/slash/Information/vote.ts +++ b/src/commands/slash/Information/vote.ts @@ -17,7 +17,7 @@ export default class Vote extends BaseCommand { }; async execute(interaction: ChatInputCommandInteraction) { const { locale } = interaction.user; - const userData = await db.userData.findFirst({ where: { userId: interaction.user.id } }); + const userData = await db.userData.findFirst({ where: { id: interaction.user.id } }); const voteCount = String(userData?.voteCount ?? 0); const embed = new EmbedBuilder() diff --git a/src/commands/slash/Main/blacklist/index.ts b/src/commands/slash/Main/blacklist/index.ts index 373d7a717..1d553e20f 100644 --- a/src/commands/slash/Main/blacklist/index.ts +++ b/src/commands/slash/Main/blacklist/index.ts @@ -226,15 +226,15 @@ export default class BlacklistCommand extends BaseCommand { private async searchBlacklistedServers(hubId: string, nameOrId: string) { const allServers = await db.blacklistedServers.findMany({ where: { - hubs: { some: { hubId } }, + blacklistedFrom: { some: { hubId } }, OR: [ { serverName: { mode: 'insensitive', contains: nameOrId } }, - { serverId: { mode: 'insensitive', contains: nameOrId } }, + { id: { mode: 'insensitive', contains: nameOrId } }, ], }, take: 25, }); - return allServers.map(({ serverName, serverId }) => ({ name: serverName, value: serverId })); + return allServers.map(({ serverName, id }) => ({ name: serverName, value: id })); } private async searchBlacklistedUsers(hubId: string, nameOrId: string) { @@ -243,15 +243,15 @@ export default class BlacklistCommand extends BaseCommand { blacklistedFrom: { some: { hubId } }, OR: [ { username: { mode: 'insensitive', contains: nameOrId } }, - { userId: { mode: 'insensitive', contains: nameOrId } }, + { id: { mode: 'insensitive', contains: nameOrId } }, ], }, take: 25, }); return filteredUsers.map((user) => ({ - name: user.username ?? `Unknown User - ${user.userId}`, - value: user.userId, + name: user.username ?? `Unknown User - ${user.id}`, + value: user.id, })); } diff --git a/src/commands/slash/Main/blacklist/list.ts b/src/commands/slash/Main/blacklist/list.ts index e5d232d44..c69616dbb 100644 --- a/src/commands/slash/Main/blacklist/list.ts +++ b/src/commands/slash/Main/blacklist/list.ts @@ -8,8 +8,8 @@ import { supportedLocaleCodes, t } from '../../../../utils/Locale.js'; import { Prisma, blacklistedServers, userData } from '@prisma/client'; // Type guard functions -function isServerType(list: blacklistedServers | userData): list is blacklistedServers { - return list && 'serverId' in list; +function isUserType(list: blacklistedServers | userData): list is userData { + return list && 'username' in list; } export default class ListBlacklists extends BlacklistCommand { @@ -45,7 +45,7 @@ export default class ListBlacklists extends BlacklistCommand { const hubId = hubInDb.id; const list = blacklistType === 'server' - ? await db.blacklistedServers.findMany({ where: { hubs: { some: { hubId } } } }) + ? await db.blacklistedServers.findMany({ where: { blacklistedFrom: { some: { hubId } } } }) : await db.userData.findMany({ where: { blacklistedFrom: { some: { hubId } } } }); const options = { LIMIT: 5, iconUrl: hubInDb.iconUrl }; const embeds = await this.buildEmbeds(interaction, list, hubId, options); @@ -62,13 +62,10 @@ export default class ListBlacklists extends BlacklistCommand { const embeds: EmbedBuilder[] = []; const fields = []; let counter = 0; - const type = isServerType(list[0]) ? 'server' : 'user'; + const type = isUserType(list[0]) ? 'user' : 'server'; for (const data of list) { - const hubData = isServerType(data) - ? data.hubs.find((d) => d.hubId === hubId) - : data.blacklistedFrom.find((d) => d.hubId === hubId); - + const hubData = data.blacklistedFrom.find((d) => d.hubId === hubId); const moderator = hubData?.moderatorId ? await interaction.client.users.fetch(hubData.moderatorId).catch(() => null) : null; @@ -110,11 +107,11 @@ export default class ListBlacklists extends BlacklistCommand { }, ) { return { - name: isServerType(data) ? data.serverName : data.username ?? 'Unknown User.', + name: (isUserType(data) ? data.username : data.serverName) ?? 'Unknown User.', value: t( { phrase: `blacklist.list.${type}`, locale }, { - id: isServerType(data) ? data.serverId : data.userId, + id: data.id, moderator: moderator ? `@${moderator.username} (${moderator.id})` : 'Unknown', reason: `${hubData?.reason}`, expires: !hubData?.expires diff --git a/src/commands/slash/Main/blacklist/server.ts b/src/commands/slash/Main/blacklist/server.ts index 8540cfb34..b6d13da4f 100644 --- a/src/commands/slash/Main/blacklist/server.ts +++ b/src/commands/slash/Main/blacklist/server.ts @@ -1,14 +1,13 @@ -import { captureException } from '@sentry/node'; -import { ChatInputCommandInteraction, EmbedBuilder, time } from 'discord.js'; -import { checkIfStaff, simpleEmbed } from '../../../../utils/Utils.js'; -import { emojis } from '../../../../utils/Constants.js'; import db from '../../../../utils/Db.js'; import BlacklistCommand from './index.js'; -import BlacklistManager from '../../../../managers/BlacklistManager.js'; import parse from 'parse-duration'; import Logger from '../../../../utils/Logger.js'; +import { captureException } from '@sentry/node'; +import { ChatInputCommandInteraction, EmbedBuilder, time } from 'discord.js'; +import { checkIfStaff, simpleEmbed } from '../../../../utils/Utils.js'; +import { emojis } from '../../../../utils/Constants.js'; import { t } from '../../../../utils/Locale.js'; -import { logBlacklist, logUnblacklist } from '../../../../utils/HubLogger/ModLogs.js'; +import { logBlacklist, logServerUnblacklist } from '../../../../utils/HubLogger/ModLogs.js'; import { deleteConnections } from '../../../../utils/ConnectedList.js'; export default class UserBlacklist extends BlacklistCommand { @@ -38,16 +37,16 @@ export default class UserBlacklist extends BlacklistCommand { return; } - const { blacklistManager } = interaction.client; + const { serverBlacklists } = interaction.client; const subCommandGroup = interaction.options.getSubcommandGroup(); const serverId = interaction.options.getString('server', true); if (subCommandGroup === 'add') { const reason = interaction.options.getString('reason', true); const duration = parse(`${interaction.options.getString('duration')}`); - const expires = duration ? new Date(Date.now() + duration) : undefined; + const expires = duration ? new Date(Date.now() + duration) : null; - const serverInBlacklist = await BlacklistManager.fetchServerBlacklist(hubInDb.id, serverId); + const serverInBlacklist = await serverBlacklists.fetchBlacklist(hubInDb.id, serverId); if (serverInBlacklist) { await interaction.followUp({ embeds: [ @@ -77,13 +76,11 @@ export default class UserBlacklist extends BlacklistCommand { } try { - await blacklistManager.addServerBlacklist( - server, - hubInDb.id, + await serverBlacklists.addBlacklist(server, hubInDb.id, { reason, - interaction.user.id, expires, - ); + moderatorId: interaction.user.id, + }); } catch (err) { Logger.error(err); @@ -101,8 +98,6 @@ export default class UserBlacklist extends BlacklistCommand { return; } - if (expires) blacklistManager.scheduleRemoval('server', serverId, hubInDb.id, expires); - const successEmbed = new EmbedBuilder() .setDescription( t( @@ -127,8 +122,8 @@ export default class UserBlacklist extends BlacklistCommand { await interaction.followUp({ embeds: [successEmbed] }); // notify the server that they have been blacklisted - await blacklistManager - .notifyBlacklist('server', serverId, { hubId: hubInDb.id, expires, reason }) + await serverBlacklists + .sendNotification({ target: { id: serverId }, hubId: hubInDb.id, expires, reason }) .catch(() => null); // delete all connections from db so they can't reconnect to the hub @@ -143,7 +138,7 @@ export default class UserBlacklist extends BlacklistCommand { }); } else if (subCommandGroup === 'remove') { - const result = await blacklistManager.removeBlacklist('server', hubInDb.id, serverId); + const result = await serverBlacklists.removeBlacklist(hubInDb.id, serverId); if (!result) { await interaction.followUp( t( @@ -163,9 +158,8 @@ export default class UserBlacklist extends BlacklistCommand { ); // send log to hub's log channel - await logUnblacklist(hubInDb.id, { - type: 'user', - targetId: serverId, + await logServerUnblacklist(interaction.client, hubInDb.id, { + serverId, mod: interaction.user, }); } diff --git a/src/commands/slash/Main/blacklist/user.ts b/src/commands/slash/Main/blacklist/user.ts index a9c7ffe98..b0847d63a 100644 --- a/src/commands/slash/Main/blacklist/user.ts +++ b/src/commands/slash/Main/blacklist/user.ts @@ -1,13 +1,12 @@ import { ChatInputCommandInteraction, EmbedBuilder, time } from 'discord.js'; import db from '../../../../utils/Db.js'; import BlacklistCommand from './index.js'; -import BlacklistManager from '../../../../managers/BlacklistManager.js'; import parse from 'parse-duration'; import { emojis } from '../../../../utils/Constants.js'; import { checkIfStaff, simpleEmbed } from '../../../../utils/Utils.js'; +import { logBlacklist } from '../../../../utils/HubLogger/ModLogs.js'; import { t } from '../../../../utils/Locale.js'; import Logger from '../../../../utils/Logger.js'; -import { logBlacklist, logUnblacklist } from '../../../../utils/HubLogger/ModLogs.js'; export default class Server extends BlacklistCommand { async execute(interaction: ChatInputCommandInteraction): Promise { @@ -40,7 +39,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.blacklistManager; + const { userManager } = interaction.client; if (subcommandGroup === 'add') { // get ID if user inputted a @ mention @@ -72,8 +71,15 @@ export default class Server extends BlacklistCommand { ); return; } + else if (user.id === interaction.user.id) { + await interaction.followUp({ + content: ' Nuh uh! You\'re stuck with us.', + ephemeral: true, + }); + return; + } - const userInBlacklist = await BlacklistManager.fetchUserBlacklist(hubInDb.id, userOpt); + const userInBlacklist = await userManager.fetchBlacklist(hubInDb.id, userOpt); if (userInBlacklist) { await interaction.followUp( t( @@ -84,17 +90,14 @@ export default class Server extends BlacklistCommand { return; } - const expires = duration ? new Date(Date.now() + duration) : undefined; - await blacklistManager.addUserBlacklist( - hubInDb.id, - user.id, + const expires = duration ? new Date(Date.now() + duration) : null; + await userManager.addBlacklist({ id: user.id, name: user.username }, hubInDb.id, { reason, - interaction.user.id, + moderatorId: interaction.user.id, expires, - ); - if (expires) blacklistManager.scheduleRemoval('user', user.id, hubInDb.id, expires); - await blacklistManager - .notifyBlacklist('user', user.id, { hubId: hubInDb.id, expires, reason }) + }); + await userManager + .sendNotification({ target: user, hubId: hubInDb.id, expires, reason }) .catch(Logger.error); const successEmbed = new EmbedBuilder() @@ -130,7 +133,8 @@ export default class Server extends BlacklistCommand { } else if (subcommandGroup === 'remove') { // remove the blacklist - const result = await blacklistManager.removeBlacklist('user', hubInDb.id, userId); + const result = await userManager.removeBlacklist(hubInDb.id, userId); + if (!result) { await interaction.followUp( t( @@ -148,13 +152,7 @@ export default class Server extends BlacklistCommand { ), ); if (user) { - // send log to hub's log channel - await logUnblacklist(hubInDb.id, { - type: 'user', - targetId: user.id, - mod: interaction.user, - reason, - }); + await userManager.logUnblacklist(hubInDb.id, user.id, { mod: interaction.user, reason }); } } } diff --git a/src/commands/slash/Main/hub/browse.ts b/src/commands/slash/Main/hub/browse.ts index c0e3b2aaf..3bf88bdda 100644 --- a/src/commands/slash/Main/hub/browse.ts +++ b/src/commands/slash/Main/hub/browse.ts @@ -18,7 +18,6 @@ import { } from 'discord.js'; import db from '../../../../utils/Db.js'; import Hub from './index.js'; -import BlacklistManager from '../../../../managers/BlacklistManager.js'; import { hubs } from '@prisma/client'; import { colors, emojis } from '../../../../utils/Constants.js'; import { paginate } from '../../../../utils/Pagination.js'; @@ -34,7 +33,7 @@ import { RegisterInteractionHandler } from '../../../../decorators/Interaction.j import { stripIndents } from 'common-tags'; import { t } from '../../../../utils/Locale.js'; import { logJoinToHub } from '../../../../utils/HubLogger/JoinLeave.js'; -import { connectChannel } from '../../../../utils/ConnectedList.js'; +import { connectChannel, getAllConnections } from '../../../../utils/ConnectedList.js'; export default class Browse extends Hub { async execute(interaction: ChatInputCommandInteraction): Promise { @@ -96,11 +95,7 @@ export default class Browse extends Hub { orderBy: { messageId: 'desc' }, }); - return Browse.createHubListingsEmbed( - hub, - connections, - lastMessage?.createdAt, - ); + return Browse.createHubListingsEmbed(hub, connections, lastMessage?.createdAt); }), ); @@ -205,10 +200,13 @@ export default class Browse extends Hub { }); return; } - const { fetchUserBlacklist, fetchServerBlacklist } = BlacklistManager; - const userBlacklisted = await fetchUserBlacklist(hubDetails.id, interaction.user.id); - const serverBlacklisted = await fetchServerBlacklist(hubDetails.id, interaction.guildId); + const { userManager, serverBlacklists } = interaction.client; + const userBlacklisted = await userManager.fetchBlacklist(hubDetails.id, interaction.user.id); + const serverBlacklisted = await serverBlacklists.fetchBlacklist( + hubDetails.id, + interaction.guildId, + ); if (userBlacklisted || serverBlacklisted) { const phrase = userBlacklisted ? 'errors.userBlacklisted' : 'errors.serverBlacklisted'; @@ -363,7 +361,10 @@ export default class Browse extends Hub { } else if (onboardingCompleted === 'in-progress') { await interaction.update({ - content: t({ phrase: 'network.onboarding.inProgress', locale }, { channel: `${channel}`, emoji: emojis.dnd_anim }), + content: t( + { phrase: 'network.onboarding.inProgress', locale }, + { channel: `${channel}`, emoji: emojis.dnd_anim }, + ), embeds: [], components: [], }); @@ -394,10 +395,11 @@ export default class Browse extends Hub { components: [], }); - const totalConnections = interaction.client.connectionCache.reduce( - (total, c) => total + (c.hubId === hubDetails.id && c.connected ? 1 : 0), - 0, - ); + const totalConnections = + (await getAllConnections())?.reduce( + (total, c) => total + (c.hubId === hubDetails.id && c.connected ? 1 : 0), + 0, + ) ?? 0; // announce await sendToHub(hubDetails.id, { diff --git a/src/commands/slash/Main/hub/create.ts b/src/commands/slash/Main/hub/create.ts index 15fcd2f30..50df3e119 100644 --- a/src/commands/slash/Main/hub/create.ts +++ b/src/commands/slash/Main/hub/create.ts @@ -23,7 +23,7 @@ export default class Create extends Hub { async execute(interaction: ChatInputCommandInteraction) { const { locale } = interaction.user; - const isOnCooldown = this.getRemainingCooldown(interaction); + const isOnCooldown = await this.getRemainingCooldown(interaction); if (isOnCooldown) { await this.sendCooldownError(interaction, isOnCooldown); return; diff --git a/src/commands/slash/Main/hub/join.ts b/src/commands/slash/Main/hub/join.ts index c3154e6fc..e4a961727 100644 --- a/src/commands/slash/Main/hub/join.ts +++ b/src/commands/slash/Main/hub/join.ts @@ -1,15 +1,14 @@ +import db from '../../../../utils/Db.js'; +import Hub from './index.js'; import { ChannelType, ChatInputCommandInteraction } from 'discord.js'; import { emojis } from '../../../../utils/Constants.js'; -import Hub from './index.js'; -import db from '../../../../utils/Db.js'; -import BlacklistManager from '../../../../managers/BlacklistManager.js'; import { hubs } from '@prisma/client'; import { simpleEmbed, getOrCreateWebhook, sendToHub } from '../../../../utils/Utils.js'; import { showOnboarding } from '../../../../scripts/network/onboarding.js'; import { stripIndents } from 'common-tags'; import { t } from '../../../../utils/Locale.js'; import { logJoinToHub } from '../../../../utils/HubLogger/JoinLeave.js'; -import { connectChannel } from '../../../../utils/ConnectedList.js'; +import { connectChannel, getAllConnections } from '../../../../utils/ConnectedList.js'; export default class JoinSubCommand extends Hub { async execute(interaction: ChatInputCommandInteraction): Promise { @@ -17,7 +16,7 @@ export default class JoinSubCommand extends Hub { const locale = interaction.user.locale; - // FIXME: Change later + // NOTE: Change later const hubName = interaction.options.getString('hub') ?? 'InterChat Central'; const invite = interaction.options.getString('invite'); const channel = interaction.options.getChannel('channel', true, [ @@ -83,14 +82,14 @@ export default class JoinSubCommand extends Hub { } else { hub = await db.hubs.findFirst({ where: { name: hubName, private: false } }); + } - if (!hub) { - await interaction.reply({ - embeds: [simpleEmbed(t({ phrase: 'hub.notFound', locale }, { emoji: emojis.no }))], - ephemeral: true, - }); - return; - } + if (!hub) { + await interaction.reply({ + embeds: [simpleEmbed(t({ phrase: 'hub.notFound', locale }, { emoji: emojis.no }))], + ephemeral: true, + }); + return; } // actual code starts here @@ -116,11 +115,9 @@ export default class JoinSubCommand extends Hub { return; } - const userBlacklisted = await BlacklistManager.fetchUserBlacklist(hub.id, interaction.user.id); - const serverBlacklisted = await BlacklistManager.fetchServerBlacklist( - hub.id, - interaction.guildId, - ); + const { userManager, serverBlacklists } = interaction.client; + const userBlacklisted = await userManager.fetchBlacklist(hub.id, interaction.user.id); + const serverBlacklisted = await serverBlacklists.fetchBlacklist(hub.id, interaction.guildId); if (userBlacklisted || serverBlacklisted) { await interaction.reply({ @@ -141,7 +138,10 @@ export default class JoinSubCommand extends Hub { await interaction.reply({ embeds: [ simpleEmbed( - t({ phrase: 'network.onboarding.inProgress', locale }, { channel: `${channel}`, emoji: emojis.dnd_anim }), + t( + { phrase: 'network.onboarding.inProgress', locale }, + { channel: `${channel}`, emoji: emojis.dnd_anim }, + ), ), ], ephemeral: true, @@ -183,10 +183,11 @@ export default class JoinSubCommand extends Hub { components: [], }); - const totalConnections = interaction.client.connectionCache.reduce( - (total, c) => total + (c.hubId === hub.id && c.connected ? 1 : 0), - 0, - ); + const totalConnections = + (await getAllConnections())?.reduce( + (total, c) => total + (c.hubId === hub.id && c.connected ? 1 : 0), + 0, + ) ?? 0; // announce await sendToHub(hub.id, { diff --git a/src/commands/slash/Main/hub/servers.ts b/src/commands/slash/Main/hub/servers.ts index 160d5c135..fc2772cbd 100644 --- a/src/commands/slash/Main/hub/servers.ts +++ b/src/commands/slash/Main/hub/servers.ts @@ -3,9 +3,8 @@ import Hub from './index.js'; import { colors, emojis } from '../../../../utils/Constants.js'; import { paginate } from '../../../../utils/Pagination.js'; import db from '../../../../utils/Db.js'; -import { simpleEmbed } from '../../../../utils/Utils.js'; +import { resolveEval, simpleEmbed } from '../../../../utils/Utils.js'; import { t } from '../../../../utils/Locale.js'; -import SuperClient from '../../../../core/Client.js'; export default class Servers extends Hub { async execute(interaction: ChatInputCommandInteraction): Promise { @@ -108,7 +107,7 @@ export default class Servers extends Hub { { context: { connection } }, ); - const evalRes = SuperClient.resolveEval(evalArr); + const evalRes = resolveEval(evalArr); const value = t( { phrase: 'hub.servers.connectionInfo', locale }, diff --git a/src/commands/slash/Main/set/language.ts b/src/commands/slash/Main/set/language.ts index 7b952443c..0ac91a4e4 100644 --- a/src/commands/slash/Main/set/language.ts +++ b/src/commands/slash/Main/set/language.ts @@ -19,10 +19,10 @@ export default class SetLanguage extends Set { return; } - const { id: userId, username } = interaction.user; + const { id, username } = interaction.user; await db.userData.upsert({ - where: { userId: interaction.user.id }, - create: { userId, locale, username }, + where: { id }, + create: { id, locale, username }, update: { locale }, }); diff --git a/src/commands/slash/Staff/ban.ts b/src/commands/slash/Staff/ban.ts index fd4d99f24..acdc5ebd1 100644 --- a/src/commands/slash/Staff/ban.ts +++ b/src/commands/slash/Staff/ban.ts @@ -6,7 +6,8 @@ import { import BaseCommand from '../../../core/BaseCommand.js'; import db from '../../../utils/Db.js'; import { simpleEmbed } from '../../../utils/Utils.js'; -import { DeveloperIds, emojis } from '../../../utils/Constants.js'; +import { emojis } from '../../../utils/Constants.js'; +import Logger from '../../../utils/Logger.js'; export default class Ban extends BaseCommand { readonly staffOnly = true; @@ -29,8 +30,6 @@ export default class Ban extends BaseCommand { ], }; override async execute(interaction: ChatInputCommandInteraction): Promise { - if (!DeveloperIds.includes(interaction.user.id)) return; - const user = interaction.options.getUser('user', true); const reason = interaction.options.getString('reason', true); @@ -43,7 +42,7 @@ export default class Ban extends BaseCommand { } const alreadyBanned = await db.userData.findFirst({ - where: { userId: user.id, banMeta: { isNot: null } }, + where: { id: user.id, banMeta: { isNot: null } }, }); if (alreadyBanned) { @@ -54,19 +53,19 @@ export default class Ban extends BaseCommand { } await db.userData.upsert({ - where: { userId: user.id }, + where: { id: user.id }, create: { - userId: user.id, + id: user.id, username: user.username, viewedNetworkWelcome: false, voteCount: 0, banMeta: { reason }, }, - update: { - banMeta: { reason }, - }, + update: { banMeta: { reason } }, }); + Logger.info(`User ${user.username} (${user.id}) banned by ${interaction.user.username}.`); + await interaction.reply({ embeds: [ simpleEmbed( diff --git a/src/commands/slash/Staff/find/server.ts b/src/commands/slash/Staff/find/server.ts index 5a3fb284d..25939e296 100644 --- a/src/commands/slash/Staff/find/server.ts +++ b/src/commands/slash/Staff/find/server.ts @@ -24,7 +24,7 @@ export default class Server extends Find { include: { hub: true }, }); - const guildBlacklisted = await db.blacklistedServers.count({ where: { serverId: guild.id } }); + const guildBlacklisted = await db.blacklistedServers.count({ where: { id: guild.id } }); const guildBoostLevel = GuildPremiumTier[guild.premiumTier]; const guildHubs = diff --git a/src/commands/slash/Staff/find/user.ts b/src/commands/slash/Staff/find/user.ts index 572c684b3..a92a15b88 100644 --- a/src/commands/slash/Staff/find/user.ts +++ b/src/commands/slash/Staff/find/user.ts @@ -22,7 +22,12 @@ export default class Server extends Find { return; } - const userInBlacklist = await db.blacklistedUsers?.findFirst({ where: { userId: user.id } }); + const userData = await db.userData?.findFirst({ where: { id: user.id } }); + const blacklistedFrom = userData?.blacklistedFrom.map( + async (bl) => (await db.hubs.findFirst({ where: { id: bl.hubId } }))?.name, + ); + const blacklistedFromStr = + blacklistedFrom && blacklistedFrom.length > 0 ? blacklistedFrom.join(', ') : 'None/'; const serversOwned = user.client.guilds.cache .filter((guild) => guild.ownerId === user.id) @@ -31,10 +36,7 @@ export default class Server extends Find { where: { ownerId: user.id }, }); const numServersOwned = serversOwned.length > 0 ? serversOwned.join(', ') : 'None'; - const numHubOwned = - hubsOwned.length > 0 - ? hubsOwned.map((hub) => hub.name).join(', ') - : 'None'; + const numHubOwned = hubsOwned.length > 0 ? hubsOwned.map((hub) => hub.name).join(', ') : 'None'; const embed = new EmbedBuilder() .setAuthor({ name: user.username, iconURL: user.avatarURL()?.toString() }) @@ -56,7 +58,9 @@ export default class Server extends Find { name: 'Network', value: stripIndents` > ${emojis.chat_icon} **Hubs Owned:** ${numHubOwned} - > ${emojis.delete} **Blacklisted:** ${userInBlacklist ? 'Yes' : 'No'}`, + > ${emojis.delete} **Blacklisted From:** ${blacklistedFromStr} + > ${emojis.deleteDanger_icon} **Banned:** ${userData?.banMeta?.reason ? 'Yes' : 'No'} + `, }, ]); diff --git a/src/commands/slash/Staff/purge.ts b/src/commands/slash/Staff/purge.ts index 335a5016b..c55512594 100644 --- a/src/commands/slash/Staff/purge.ts +++ b/src/commands/slash/Staff/purge.ts @@ -10,9 +10,8 @@ import db from '../../../utils/Db.js'; import BaseCommand from '../../../core/BaseCommand.js'; import { stripIndents } from 'common-tags'; import { emojis } from '../../../utils/Constants.js'; -import { simpleEmbed, msToReadable, deleteMsgsFromDb, handleError } from '../../../utils/Utils.js'; +import { simpleEmbed, msToReadable, deleteMsgsFromDb, handleError, resolveEval } from '../../../utils/Utils.js'; import { broadcastedMessages } from '@prisma/client'; -import SuperClient from '../../../core/Client.js'; const limitOpt: APIApplicationCommandBasicOption = { type: ApplicationCommandOptionType.Integer, @@ -246,7 +245,7 @@ export default class Purge extends BaseCommand { { context: { channelId: network.channelId, messagesInDb } }, ); - return SuperClient.resolveEval(evalRes) || []; + return resolveEval(evalRes) || []; } catch (e) { handleError(e); diff --git a/src/commands/slash/Staff/unban.ts b/src/commands/slash/Staff/unban.ts index 95e1b2d04..6e28c7dfb 100644 --- a/src/commands/slash/Staff/unban.ts +++ b/src/commands/slash/Staff/unban.ts @@ -25,7 +25,7 @@ export default class Unban extends BaseCommand { override async execute(interaction: ChatInputCommandInteraction): Promise { const user = interaction.options.getUser('user', true); const alreadyBanned = await db.userData.findFirst({ - where: { userId: user.id, banMeta: { isSet: true } }, + where: { id: user.id, banMeta: { isSet: true } }, }); if (!alreadyBanned) { @@ -36,17 +36,15 @@ export default class Unban extends BaseCommand { } await db.userData.upsert({ - where: { userId: user.id }, + where: { id: user.id }, create: { - userId: user.id, + id: user.id, username: user.username, viewedNetworkWelcome: false, voteCount: 0, banMeta: { set: null }, }, - update: { - banMeta: { set: null }, - }, + update: { banMeta: { set: null } }, }); await interaction.reply({ diff --git a/src/core/BaseBlacklistManager.ts b/src/core/BaseBlacklistManager.ts new file mode 100644 index 000000000..76c0a6ce7 --- /dev/null +++ b/src/core/BaseBlacklistManager.ts @@ -0,0 +1,68 @@ +import db from '../utils/Db.js'; +import Factory from './Factory.js'; +import { colors, emojis } from '../utils/Constants.js'; +import { hubBlacklist, Prisma } from '@prisma/client'; +import { EmbedBuilder, Snowflake, User } from 'discord.js'; +import { getAllDocuments, serializeCache } from '../utils/db/cacheUtils.js'; + +interface BlacklistEntity { + id: string; + blacklistedFrom: hubBlacklist[]; +} + +export default abstract class BaseBlacklistManager extends Factory { + protected abstract modelName: Prisma.ModelName; + + protected abstract fetchEntityFromDb(hubId: string, entityId: string): Promise; + public abstract logUnblacklist( + hubId: string, + id: string, + opts: { mod: User; reason?: string }, + ): Promise; + + public abstract sendNotification(opts: { + target: { id: Snowflake }; + hubId: string; + expires: Date | null; + reason?: string; + }): Promise; + public abstract removeBlacklist(hubId: string, id: string): Promise; + public abstract addBlacklist( + entity: { id: Snowflake; name: string }, + hubId: string, + { + reason, + moderatorId, + expires, + }: { reason: string; moderatorId: Snowflake; expires: Date | null }, + ): Promise; + + public async getAllBlacklists() { + return serializeCache(await getAllDocuments(`${this.modelName}:*`)); + } + + public async fetchBlacklist(hubId: string, entityId: string) { + const cache = serializeCache(await db.cache.get(`${this.modelName}:entityId`)); + + const data = cache?.blacklistedFrom.find((h) => h.hubId === hubId) + ? cache + : await this.fetchEntityFromDb(hubId, entityId); + + return data; + } + + protected buildNotifEmbed(description: string, opts: { expires: Date | null; reason?: string }) { + const expireString = opts.expires + ? `` + : 'Never'; + + return new EmbedBuilder() + .setTitle(`${emojis.blobFastBan} Blacklist Notification`) + .setDescription(description) + .setColor(colors.interchatBlue) + .setFields( + { name: 'Reason', value: opts.reason ?? 'No reason provided.', inline: true }, + { name: 'Expires', value: expireString, inline: true }, + ); + } +} diff --git a/src/core/BaseCommand.ts b/src/core/BaseCommand.ts index 5d06dc590..6abe74780 100644 --- a/src/core/BaseCommand.ts +++ b/src/core/BaseCommand.ts @@ -36,14 +36,14 @@ export default abstract class BaseCommand { async autocomplete?(interaction: AutocompleteInteraction): Promise; async checkAndSetCooldown(interaction: RepliableInteraction): Promise { - const remainingCooldown = this.getRemainingCooldown(interaction); + const remainingCooldown = await this.getRemainingCooldown(interaction); if (remainingCooldown) { await this.sendCooldownError(interaction, remainingCooldown); return true; } - this.setUserCooldown(interaction); + await this.setUserCooldown(interaction); return false; } @@ -62,21 +62,22 @@ export default abstract class BaseCommand { }); } - getRemainingCooldown(interaction: RepliableInteraction): number { + async getRemainingCooldown(interaction: RepliableInteraction): Promise { let remainingCooldown: number | undefined; + const { commandCooldowns } = interaction.client; if (interaction.isChatInputCommand()) { const subcommand = interaction.options.getSubcommand(false); const subcommandGroup = interaction.options.getSubcommandGroup(false); - remainingCooldown = interaction.client.commandCooldowns?.getRemainingCooldown( + remainingCooldown = await commandCooldowns.getRemainingCooldown( `${interaction.user.id}-${interaction.commandName}${ subcommandGroup ? `-${subcommandGroup}` : '' }${subcommand ? `-${subcommand}` : ''}`, ); } else if (interaction.isContextMenuCommand()) { - remainingCooldown = interaction.client.commandCooldowns?.getRemainingCooldown( + remainingCooldown = await commandCooldowns.getRemainingCooldown( `${interaction.user.id}-${interaction.commandName}`, ); } @@ -84,22 +85,22 @@ export default abstract class BaseCommand { return remainingCooldown || 0; } - setUserCooldown(interaction: RepliableInteraction): void { + async setUserCooldown(interaction: RepliableInteraction): Promise { if (!this.cooldown) return; + const { commandCooldowns } = interaction.client; + if (interaction.isChatInputCommand()) { const subcommand = interaction.options.getSubcommand(false); const subcommandGroup = interaction.options.getSubcommandGroup(false); + const id = `${interaction.user.id}-${interaction.commandName}${ + subcommandGroup ? `-${subcommandGroup}` : '' + }${subcommand ? `-${subcommand}` : ''}`; - interaction.client.commandCooldowns?.setCooldown( - `${interaction.user.id}-${interaction.commandName}${ - subcommandGroup ? `-${subcommandGroup}` : '' - }${subcommand ? `-${subcommand}` : ''}`, - this.cooldown, - ); + await commandCooldowns.setCooldown(id, this.cooldown); } else if (interaction.isContextMenuCommand()) { - interaction.client.commandCooldowns?.setCooldown( + await commandCooldowns.setCooldown( `${interaction.user.id}-${interaction.commandName}`, this.cooldown, ); diff --git a/src/core/Client.ts b/src/core/Client.ts index d08aae9ee..660a8e8b0 100644 --- a/src/core/Client.ts +++ b/src/core/Client.ts @@ -1,5 +1,8 @@ import Scheduler from '../services/SchedulerService.js'; +import UserDbManager from '../managers/UserDbManager.js'; +import CooldownService from '../services/CooldownService.js'; import loadCommandFiles from '../utils/LoadCommands.js'; +import ServerBlacklistManager from '../managers/ServerBlacklistManager.js'; import { Client, IntentsBitField, @@ -11,26 +14,19 @@ import { WebhookClient, ActivityType, } from 'discord.js'; -import { - connectionCache as _connectionCache, - messageTimestamps, - storeMsgTimestamps, - syncConnectionCache, -} from '../utils/ConnectedList.js'; +import { getAllConnections } from '../utils/ConnectedList.js'; import { ClusterClient, getInfo } from 'discord-hybrid-sharding'; import { commandsMap, interactionsMap } from './BaseCommand.js'; -import CooldownService from '../services/CooldownService.js'; -import BlacklistManager from '../managers/BlacklistManager.js'; import { RemoveMethods } from '../typings/index.js'; import { loadLocales } from '../utils/Locale.js'; import { PROJECT_VERSION } from '../utils/Constants.js'; +import { resolveEval } from '../utils/Utils.js'; import 'dotenv/config'; export default class SuperClient extends Client { // A static instance of the SuperClient class to be used globally. public static instance: SuperClient; - private _connectionCachePopulated = false; private readonly scheduler = new Scheduler(); readonly description = 'The only cross-server chatting bot you\'ll ever need.'; @@ -40,10 +36,10 @@ export default class SuperClient extends Client { readonly webhooks = new Collection(); readonly reactionCooldowns = new Collection(); - readonly connectionCache = _connectionCache; readonly cluster = new ClusterClient(this); - readonly blacklistManager = new BlacklistManager(this.scheduler); + readonly userManager = new UserDbManager(this); + readonly serverBlacklists = new ServerBlacklistManager(this); readonly commandCooldowns = new CooldownService(); constructor() { @@ -76,6 +72,7 @@ export default class SuperClient extends Client { IntentsBitField.Flags.GuildWebhooks, ], presence: { + status: 'invisible', activities: [ { state: '🔗 Watching over 700+ cross-server chats', @@ -101,26 +98,11 @@ export default class SuperClient extends Client { // load commands await loadCommandFiles(); - await syncConnectionCache(); - this._connectionCachePopulated = true; - - this.scheduler.addRecurringTask('populateConnectionCache', 60_000 * 5, syncConnectionCache); - this.scheduler.addRecurringTask('storeMsgTimestamps', 60 * 1_000, () => { - // store network message timestamps to connectedList every minute - storeMsgTimestamps(messageTimestamps); - messageTimestamps.clear(); - }); + await getAllConnections({ connected: true }); await this.login(process.env.TOKEN); } - public get cachePopulated() { - return this._connectionCachePopulated; - } - - static resolveEval = (value: T[]) => - value?.find((res) => Boolean(res)) as RemoveMethods | undefined; - /** * Fetches a guild by its ID from the cache. * @param guildId The ID of the guild to fetch. @@ -132,7 +114,7 @@ export default class SuperClient extends Client { { guildId, context: guildId }, )) as Guild[]; - return fetch ? SuperClient.resolveEval(fetch) : undefined; + return fetch ? resolveEval(fetch) : undefined; } getScheduler(): Scheduler { diff --git a/src/managers/BlacklistManager.ts b/src/managers/BlacklistManager.ts deleted file mode 100644 index cf799a1ac..000000000 --- a/src/managers/BlacklistManager.ts +++ /dev/null @@ -1,249 +0,0 @@ -import db from '../utils/Db.js'; -import Scheduler from '../services/SchedulerService.js'; -import SuperClient from '../core/Client.js'; -import { blacklistedServers, userData } from '@prisma/client'; -import { EmbedBuilder, Guild, Snowflake } from 'discord.js'; -import { emojis, colors } from '../utils/Constants.js'; -import { logUnblacklist } from '../utils/HubLogger/ModLogs.js'; -import { RemoveMethods } from '../typings/index.js'; - -export default class BlacklistManager { - private scheduler: Scheduler; - - constructor(scheduler: Scheduler) { - this.scheduler = scheduler; - } - - /** - * Remove a user or server from the blacklist. - * @param type The type of blacklist to remove. - * @param hubId The hub ID to remove the blacklist from. - * @param userOrServerId The user or server ID to remove from the blacklist. - * @returns The updated blacklist. - */ - async removeBlacklist( - type: 'user' | 'server', - hubId: string, - serverId: Snowflake, - ): Promise; - async removeBlacklist( - type: 'server', - hubId: string, - serverId: Snowflake, - ): Promise; - async removeBlacklist(type: 'user', hubId: string, userId: Snowflake): Promise; - async removeBlacklist(type: 'user' | 'server', hubId: string, id: Snowflake) { - this.scheduler.stopTask(`blacklist_${type}-${id}`); - if (type === 'user') { - const where = { userId: id, blacklistedFrom: { some: { hubId } } }; - const notInBlacklist = await db.userData.findFirst({ where }); - if (!notInBlacklist) return null; - - return await db.userData.update({ - where, - data: { blacklistedFrom: { deleteMany: { where: { hubId } } } }, - }); - } - else { - const where = { serverId: id, hubs: { some: { hubId } } }; - const notInBlacklist = await db.blacklistedServers.findFirst({ where }); - if (!notInBlacklist) return null; - - return await db.blacklistedServers.update({ - where, - data: { hubs: { deleteMany: { where: { hubId } } } }, - }); - } - } - - /** - * Schedule the removal of a user or server from the blacklist. - * @param type The type of blacklist to remove. (user/server) - * @param id The user or server ID to remove from the blacklist. - * @param hubId The hub ID to remove the blacklist from. - * @param expires The date or milliseconds to wait before removing the blacklist. - */ - scheduleRemoval( - type: 'user' | 'server', - id: Snowflake, - hubId: string, - expires: Date | number, - ): void { - const name = `unblacklist_${type}-${id}`; - if (this.scheduler.taskNames.includes(name)) this.scheduler.stopTask(name); - - const execute = async () => { - if (SuperClient.instance?.user) { - await logUnblacklist(hubId, { - type, - targetId: id, - mod: SuperClient.instance.user, - reason: 'Blacklist duration expired.', - }).catch(() => null); - } - - await this.removeBlacklist(type, hubId, id); - }; - - this.scheduler.addTask(name, expires, execute); - } - - /** - * Notify a user or server that they have been blacklisted. - * @param type The type of blacklist to notify. (user/server) - * @param id The user or server ID to notify. - * @param hubId The hub ID to notify. - * @param expires The date after which the blacklist expires. - * @param reason The reason for the blacklist. - */ - async notifyBlacklist( - type: 'user' | 'server', - id: Snowflake, - opts: { - hubId: string; - expires?: Date; - reason?: string; - }, - ): Promise { - const hub = await db.hubs.findUnique({ where: { id: opts.hubId } }); - const expireString = opts.expires - ? `` - : 'Never'; - const embed = new EmbedBuilder() - .setTitle(`${emojis.blobFastBan} Blacklist Notification`) - .setColor(colors.interchatBlue) - .setFields( - { name: 'Reason', value: opts.reason ?? 'No reason provided.', inline: true }, - { name: 'Expires', value: expireString, inline: true }, - ); - - if (type === 'user') { - embed.setDescription(`You have been blacklisted from talking in hub **${hub?.name}**.`); - const user = await SuperClient.instance.users.fetch(id); - await user.send({ embeds: [embed] }).catch(() => null); - } - else { - embed.setDescription( - `This server has been blacklisted from talking in hub **${hub?.name}**.`, - ); - const serverConnected = await db.connectedList.findFirst({ - where: { serverId: id, hubId: opts.hubId }, - }); - - if (!serverConnected) return; - - await SuperClient.instance.cluster.broadcastEval( - async (client, ctx) => { - const channel = await client.channels.fetch(ctx.channelId).catch(() => null); - if (!channel?.isTextBased()) return; - - await channel.send({ embeds: [ctx.embed] }).catch(() => null); - }, - { context: { channelId: serverConnected.channelId, embed: embed.toJSON() } }, - ); - } - } - - /** - * Fetch a user blacklist from the database. - * @param hubId The hub ID to fetch the blacklist from. - * @param userId The ID of the blacklisted user. - */ - static async fetchUserBlacklist(hubId: string, userId: string) { - const userBlacklisted = await db.userData.findFirst({ - where: { userId, blacklistedFrom: { some: { hubId } } }, - }); - return userBlacklisted; - } - - /** - * Fetch a server blacklist from the database. - * @param hubId The hub ID to fetch the blacklist from. - * @param serverId The ID of the blacklisted serverId. - */ - static async fetchServerBlacklist(hubId: string, serverId: string) { - const userBlacklisted = await db.blacklistedServers.findFirst({ - where: { serverId, hubs: { some: { hubId } } }, - }); - return userBlacklisted; - } - - /** - * Add a user to the blacklist. - * @param hubId The ID of the hub to add the blacklist to. - * @param userId The ID of the user to blacklist. - * @param reason The reason for the blacklist. - * @param expires The date or milliseconds after which the blacklist will expire. - * @returns The created blacklist. - */ - async addUserBlacklist( - hubId: string, - userId: Snowflake, - reason: string, - moderatorId: Snowflake, - expires?: Date | number, - ) { - const client = SuperClient.instance; - const user = await client.users.fetch(userId); - if (typeof expires === 'number') expires = new Date(Date.now() + expires); - - const dbUser = await db.userData.findFirst({ where: { userId: user.id } }); - - const hubs = dbUser?.blacklistedFrom.filter((i) => i.hubId !== hubId) || []; - hubs?.push({ expires: expires ?? null, reason, hubId, moderatorId }); - - const updatedUser = await db.userData.upsert({ - where: { - userId: user.id, - }, - update: { - username: user.username, - blacklistedFrom: { set: hubs }, - }, - create: { - userId: user.id, - username: user.username, - blacklistedFrom: hubs, - }, - }); - - return updatedUser; - } - - /** - * Add a server to the blacklist. - * @param server The ID or instance of the server to blacklist. - * @param hubId The ID of the hub to add the blacklist to. - * @param reason The reason for the blacklist. - * @param expires The date after which the blacklist will expire. - * @returns The created blacklist. - */ - async addServerBlacklist( - server: RemoveMethods | Snowflake, - hubId: string, - reason: string, - moderatorId: Snowflake, - expires?: Date, - ) { - const guild = - typeof server === 'string' ? await SuperClient.instance.fetchGuild(server) : server; - if (!guild) return null; - - const dbGuild = await db.blacklistedServers.upsert({ - where: { - serverId: guild.id, - }, - update: { - serverName: guild.name, - hubs: { push: { hubId, expires, reason, moderatorId } }, - }, - create: { - serverId: guild.id, - serverName: guild.name, - hubs: [{ hubId, expires, reason, moderatorId }], - }, - }); - - return dbGuild; - } -} diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index 8c5412f04..116a8c28e 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -5,14 +5,17 @@ import { EmbedBuilder, Guild, User, - GuildChannel, - GuildTextBasedChannel, HexColorString, Message, MessageReaction, PartialUser, Interaction, Client, + ForumChannel, + MediaChannel, + NewsChannel, + TextChannel, + VoiceChannel, } from 'discord.js'; import { checkIfStaff, @@ -20,11 +23,9 @@ import { getUserLocale, handleError, simpleEmbed, - wait, } from '../utils/Utils.js'; import db from '../utils/Db.js'; import Logger from '../utils/Logger.js'; -import SuperClient from '../core/Client.js'; import GatewayEvent from '../decorators/GatewayEvent.js'; import sendBroadcast from '../scripts/network/sendBroadcast.js'; import storeMessageData from '../scripts/network/storeMessageData.js'; @@ -41,7 +42,11 @@ import { addReaction, updateReactions } from '../scripts/reaction/actions.js'; import { checkBlacklists } from '../scripts/reaction/helpers.js'; import { CustomID } from '../utils/CustomID.js'; import { logGuildLeaveToHub } from '../utils/HubLogger/JoinLeave.js'; -import { deleteConnections, modifyConnection } from '../utils/ConnectedList.js'; +import { + deleteConnections, + getAllConnections, + modifyConnection, +} from '../utils/ConnectedList.js'; export default abstract class EventManager { @GatewayEvent('ready') @@ -85,6 +90,7 @@ export default abstract class EventManager { ); const { userBlacklisted, serverBlacklisted } = await checkBlacklists( + user.client, originalMsg.hub.id, reaction.message.guildId, user.id, @@ -117,11 +123,11 @@ export default abstract class EventManager { } @GatewayEvent('webhooksUpdate') - static async onWebhooksUpdate(channel: GuildChannel) { - if (!channel.isTextBased()) return; - + static async onWebhooksUpdate( + channel: NewsChannel | TextChannel | VoiceChannel | ForumChannel | MediaChannel, + ) { try { - const connection = channel.client.connectionCache.find( + const connection = (await getAllConnections())?.find( (c) => c.connected && (c.channelId === channel.id || c.parentId === channel.id), ); @@ -137,16 +143,16 @@ export default abstract class EventManager { // disconnect the channel await modifyConnection({ id: connection.id }, { connected: false }); - const client = SuperClient.instance; - // send an alert to the channel - const networkChannel = channel.isTextBased() - ? channel - : ((await client.channels.fetch(connection.channelId)) as GuildTextBasedChannel); - - await networkChannel.send( - t({ phrase: 'misc.webhookNoLongerExists', locale: 'en' }, { emoji: emojis.info }), - ); + const networkChannel = connection.parentId + ? await channel.client.channels.fetch(connection.channelId) + : channel; + + if (networkChannel?.isTextBased()) { + await networkChannel.send( + t({ phrase: 'misc.webhookNoLongerExists', locale: 'en' }, { emoji: emojis.info }), + ); + } } } catch (error) { @@ -237,42 +243,43 @@ export default abstract class EventManager { static async onMessageCreate(message: Message): Promise { if (message.author.bot || message.system || message.webhookId || !message.inGuild()) return; - const { connectionCache, cachePopulated } = message.client; + // const { cachePopulated } = message.client; - if (!cachePopulated) { - Logger.debug('[InterChat]: Connection cache not populated, 5 secs until retry...'); - await wait(5000); + // if (!cachePopulated) { + // Logger.debug('[InterChat]: Connection cache not populated, 5 secs until retry...'); + // await wait(5000); - EventManager.onMessageCreate(message); - return; - } + // EventManager.onMessageCreate(message); + // return; + // } // check if the message was sent in a network channel - const connection = connectionCache.get(message.channel.id); - if (!connection?.connected) return; + const allConnections = await getAllConnections(); + const connection = allConnections?.find((c) => c.channelId === message.channel.id && c.connected); + if (!allConnections || !connection) return; const hub = await db.hubs.findFirst({ where: { id: connection.hubId } }); if (!hub) return; const settings = new HubSettingsBitField(hub.settings); - const hubConnections = connectionCache.filter( + const hubConnections = allConnections.filter( (con) => con.hubId === connection.hubId && con.connected && con.channelId !== message.channel.id, ); - let userData = await db.userData.findFirst({ where: { userId: message.author.id } }); + let userData = await db.userData.findFirst({ where: { id: message.author.id } }); if (!userData?.viewedNetworkWelcome) { userData = await db.userData.upsert({ - where: { userId: message.author.id }, + where: { id: message.author.id }, create: { - userId: message.author.id, + id: message.author.id, username: message.author.username, viewedNetworkWelcome: true, }, update: { viewedNetworkWelcome: true }, }); - await sendWelcomeMsg(message, hubConnections.size.toString(), hub.name); + await sendWelcomeMsg(message, hubConnections.length.toString(), hub.name); } // set locale for the user @@ -318,7 +325,7 @@ export default abstract class EventManager { static async onInteractionCreate(interaction: Interaction): Promise { try { const { commands, interactions } = interaction.client; - const userData = await db.userData.findFirst({ where: { userId: interaction.user.id } }); + const userData = await db.userData.findFirst({ where: { id: interaction.user.id } }); interaction.user.locale = getUserLocale(userData); if (userData?.banMeta?.reason) { diff --git a/src/managers/ServerBlacklistManager.ts b/src/managers/ServerBlacklistManager.ts new file mode 100644 index 000000000..c49040348 --- /dev/null +++ b/src/managers/ServerBlacklistManager.ts @@ -0,0 +1,101 @@ +import db from '../utils/Db.js'; +import BaseBlacklistManager from '../core/BaseBlacklistManager.js'; +import { blacklistedServers, hubBlacklist, Prisma } from '@prisma/client'; +import { Snowflake, User } from 'discord.js'; +import { logServerUnblacklist } from '../utils/HubLogger/ModLogs.js'; +import { getAllConnections } from '../utils/ConnectedList.js'; + +export default class ServerBlacklisManager extends BaseBlacklistManager { + protected modelName: Prisma.ModelName = 'blacklistedServers'; + + protected override async fetchEntityFromDb(hubId: string, entityId: string) { + return await db.blacklistedServers.findFirst({ + where: { id: entityId, blacklistedFrom: { some: { hubId } } }, + }); + } + public override async logUnblacklist( + hubId: string, + serverId: string, + { mod, reason }: { mod: User; reason?: string }, + ) { + await logServerUnblacklist(this.client, hubId, { serverId, mod, reason }); + } + + /** + * Add a server to the blacklist. + * @param server The ID or instance of the server to blacklist. + * @param hubId The ID of the hub to add the blacklist to. + * @param reason The reason for the blacklist. + * @param expires The date after which the blacklist will expire. + * @returns The created blacklist. + */ + public override async addBlacklist( + server: { id: Snowflake; name: string }, + hubId: string, + { + reason, + moderatorId, + expires, + }: { reason: string; moderatorId: Snowflake; expires: Date | null }, + ) { + const blacklistedFrom: hubBlacklist = { hubId, expires, reason, moderatorId }; + const createdRes = await db.blacklistedServers.upsert({ + where: { id: server.id }, + update: { serverName: server.name, blacklistedFrom: { push: blacklistedFrom } }, + create: { id: server.id, serverName: server.name, blacklistedFrom: [blacklistedFrom] }, + }); + return createdRes; + } + + public override async removeBlacklist(hubId: string, id: Snowflake) { + const where = { id, blacklistedFrom: { some: { hubId } } }; + const notInBlacklist = await db.blacklistedServers.findFirst({ where }); + if (!notInBlacklist) return null; + + const res = await db.blacklistedServers.update({ + where, + data: { blacklistedFrom: { deleteMany: { where: { hubId } } } }, + }); + return res; + } + /** + * Notify a user or server that they have been blacklisted. + * @param type The type of blacklist to notify. (user/server) + * @param id The user or server ID to notify. + * @param hubId The hub ID to notify. + * @param expires The date after which the blacklist expires. + * @param reason The reason for the blacklist. + */ + async sendNotification(opts: { + target: { id: Snowflake }; + hubId: string; + expires: Date | null; + reason?: string; + }): Promise { + const hub = await db.hubs.findUnique({ where: { id: opts.hubId } }); + const embed = this.buildNotifEmbed( + `This server has been blacklisted from talking in hub **${hub?.name}**.`, + { expires: opts.expires, reason: opts.reason }, + ); + + const serverConnected = + (await getAllConnections())?.find( + (con) => con.serverId === opts.target.id && con.hubId === opts.hubId, + ) ?? + (await db.connectedList.findFirst({ + where: { serverId: opts.target.id, hubId: opts.hubId }, + })); + + if (!serverConnected) return; + + await this.client.cluster.broadcastEval( + async (_client, ctx) => { + const channel = await _client.channels.fetch(ctx.channelId).catch(() => null); + if (!channel?.isTextBased()) return; + + await channel.send({ embeds: [ctx.embed] }).catch(() => null); + }, + { context: { channelId: serverConnected.channelId, embed: embed.toJSON() } }, + ); + } +} diff --git a/src/managers/UserDbManager.ts b/src/managers/UserDbManager.ts new file mode 100644 index 000000000..5e9907e76 --- /dev/null +++ b/src/managers/UserDbManager.ts @@ -0,0 +1,96 @@ +import db from '../utils/Db.js'; +import BaseBlacklistManager from '../core/BaseBlacklistManager.js'; +import { Prisma, userData } from '@prisma/client'; +import { Snowflake, User } from 'discord.js'; +import { logUserUnblacklist } from '../utils/HubLogger/ModLogs.js'; + +export default class UserDbManager extends BaseBlacklistManager { + protected modelName: Prisma.ModelName = 'userData'; + + protected override async fetchEntityFromDb(hubId: string, id: string) { + return await db.userData.findFirst({ where: { id, blacklistedFrom: { some: { hubId } } } }); + } + + public override async logUnblacklist( + hubId: string, + userId: string, + { mod, reason }: { mod: User; reason?: string }, + ) { + await logUserUnblacklist(this.client, hubId, { userId, mod, reason }); + } + + /** + * Add a user to the blacklist. + * @param hubId The ID of the hub to add the blacklist to. + * @param userId The ID of the user to blacklist. + * @param reason The reason for the blacklist. + * @param expires The date or milliseconds after which the blacklist will expire. + * @returns The created blacklist. + */ + async addBlacklist( + user: { id: Snowflake; name: string }, + hubId: string, + { + reason, + moderatorId, + expires, + }: { reason: string; moderatorId: Snowflake; expires: Date | null }, + ) { + if (typeof expires === 'number') expires = new Date(Date.now() + expires); + + const dbUser = await db.userData.findFirst({ where: { id: user.id } }); + + const hubs = dbUser?.blacklistedFrom.filter((i) => i.hubId !== hubId) || []; + hubs?.push({ expires: expires ?? null, reason, hubId, moderatorId }); + + const updatedUser = await db.userData.upsert({ + where: { id: user.id }, + update: { username: user.name, blacklistedFrom: { set: hubs } }, + create: { id: user.id, username: user.name, blacklistedFrom: hubs }, + }); + return updatedUser; + } + + /** + * Remove a user or server from the blacklist. + * @param hubId The hub ID to remove the blacklist from. + * @param userOrServerId The user or server ID to remove from the blacklist. + * @returns The updated blacklist. + */ + async removeBlacklist(hubId: string, userId: Snowflake) { + const where = { id: userId, blacklistedFrom: { some: { hubId } } }; + const notInBlacklist = await db.userData.findFirst({ where }); + if (!notInBlacklist) return null; + + const deletedRes = await db.userData.update({ + where, + data: { blacklistedFrom: { deleteMany: { where: { hubId } } } }, + }); + + return deletedRes; + } + + /** + * Notify a user or server that they have been blacklisted. + * @param type The type of blacklist to notify. (user/server) + * @param id The user or server ID to notify. + * @param hubId The hub ID to notify. + * @param expires The date after which the blacklist expires. + * @param reason The reason for the blacklist. + */ + async sendNotification(opts: { + target: User; + hubId: string; + expires: Date | null; + reason?: string; + }): Promise { + const hub = await db.hubs.findUnique({ where: { id: opts.hubId } }); + // TODO: Make this method also handle misc notifications, not only blacklists + const embed = this.buildNotifEmbed( + `You have been blacklisted from talking in hub **${hub?.name}**`, + { expires: opts.expires, reason: opts.reason }, + ); + + await opts.target.send({ embeds: [embed] }).catch(() => null); + } +} diff --git a/src/managers/VoteManager.ts b/src/managers/VoteManager.ts index 0d75081d5..837c32e55 100644 --- a/src/managers/VoteManager.ts +++ b/src/managers/VoteManager.ts @@ -4,9 +4,9 @@ import { WebhookPayload } from '@top-gg/sdk'; import { stripIndents } from 'common-tags'; import { ClusterManager } from 'discord-hybrid-sharding'; import { WebhookClient, userMention, EmbedBuilder } from 'discord.js'; -import { badgeEmojis, LINKS, SUPPORT_SERVER_ID, VOTER_ROLE_ID } from '../utils/Constants.js'; +import { badgeEmojis, LINKS, VOTER_ROLE_ID } from '../utils/Constants.js'; import { getOrdinalSuffix, getUsername, modifyUserRole } from '../utils/Utils.js'; -import { EventEmitter } from 'events'; +import EventEmitter from 'events'; export type TopggEvents = { vote: WebhookPayload; @@ -24,8 +24,8 @@ export class VoteManager extends EventEmitter { this.scheduler.addRecurringTask('removeVoterRole', 60 * 60 * 1_000, async () => { const expiredVotes = await db.userData.findMany({ where: { lastVoted: { lt: new Date() } } }); for (const vote of expiredVotes) { - this.emit('voteExpired', vote.userId); - await this.removeVoterRole(vote.userId); + this.emit('voteExpired', vote.id); + await this.removeVoterRole(vote.id); } }); } @@ -42,25 +42,17 @@ export class VoteManager extends EventEmitter { return super.once(event, listener); } - async getUserVoteCount(userId: string) { - const user = await db.userData.findUnique({ where: { userId } }); + async getUserVoteCount(id: string) { + const user = await db.userData.findUnique({ where: { id } }); return user?.voteCount ?? 0; } async incrementUserVote(userId: string, username?: string) { const lastVoted = new Date(); return await db.userData.upsert({ - where: { userId }, - create: { - userId, - username, - lastVoted, - voteCount: 1, - }, - update: { - lastVoted, - voteCount: { increment: 1 }, - }, + where: { id: userId }, + create: { id: userId, username, lastVoted, voteCount: 1 }, + update: { lastVoted, voteCount: { increment: 1 } }, }); } @@ -92,9 +84,9 @@ export class VoteManager extends EventEmitter { } async addVoterRole(userId: string) { - await modifyUserRole(this.cluster, 'add', userId, SUPPORT_SERVER_ID, VOTER_ROLE_ID); + await modifyUserRole(this.cluster, 'add', { userId, roleId: VOTER_ROLE_ID }); } async removeVoterRole(userId: string) { - await modifyUserRole(this.cluster, 'remove', userId, SUPPORT_SERVER_ID, VOTER_ROLE_ID); + await modifyUserRole(this.cluster, 'remove', { userId, roleId: VOTER_ROLE_ID }); } } diff --git a/src/scripts/hub/manage.ts b/src/scripts/hub/manage.ts index 65b11c58d..83e65948f 100644 --- a/src/scripts/hub/manage.ts +++ b/src/scripts/hub/manage.ts @@ -54,7 +54,7 @@ export const hubEmbed = async (hub: hubs & { connections: connectedList[] }) => where: { blacklistedFrom: { some: { hubId: hub.id } } }, }); const hubBlacklistedServers = await db.blacklistedServers.count({ - where: { hubs: { some: { hubId: hub.id } } }, + where: { blacklistedFrom: { some: { hubId: hub.id } } }, }); return new EmbedBuilder() diff --git a/src/scripts/network/runChecks.ts b/src/scripts/network/runChecks.ts index dde37d8dc..08659e638 100644 --- a/src/scripts/network/runChecks.ts +++ b/src/scripts/network/runChecks.ts @@ -41,9 +41,9 @@ export const isCaughtSpam = async ( ) => { const antiSpamResult = runAntiSpam(message.author, 3); if (!antiSpamResult) return false; - /* FIXME: Don't use { addUserBlacklist, notifyBlacklist } it makes the methods lose their "this" property + /* NOTE: Don't use { addUserBlacklist, notifyBlacklist } it makes the methods lose their "this" property better to not have a class like this at all tbh */ - const blacklistManager = message.client.blacklistManager; + const { userManager } = message.client; if (settings.has('SpamFilter') && antiSpamResult.infractions >= 3) { const expires = new Date(Date.now() + 60 * 5000); @@ -51,13 +51,12 @@ export const isCaughtSpam = async ( const target = message.author; const mod = message.client.user; - await blacklistManager.addUserBlacklist(hubId, target.id, reason, mod.id, 60 * 5000); - blacklistManager.scheduleRemoval('user', target.id, hubId, 60 * 5000); - - await blacklistManager - .notifyBlacklist('user', target.id, { hubId, expires, reason }) - .catch(() => null); - + await userManager.addBlacklist({ id: target.id, name: target.username }, hubId, { + reason, + expires, + moderatorId: mod.id, + }); + await userManager.sendNotification({ target, hubId, expires, reason }).catch(() => null); await logBlacklist(hubId, message.client, { target, mod, reason, expires }).catch(() => null); } diff --git a/src/scripts/network/sendBroadcast.ts b/src/scripts/network/sendBroadcast.ts index 404050511..f6f0b9b3f 100644 --- a/src/scripts/network/sendBroadcast.ts +++ b/src/scripts/network/sendBroadcast.ts @@ -1,5 +1,4 @@ import { - Collection, EmbedBuilder, HexColorString, Message, @@ -29,7 +28,7 @@ type BroadcastOpts = { export default ( message: Message, hub: hubs, - allConnected: Collection, + allConnected: connectedList[], settings: HubSettingsBitField, opts: BroadcastOpts, ) => { diff --git a/src/scripts/network/sendMessage.ts b/src/scripts/network/sendMessage.ts index a43a06b2c..4cc919be1 100644 --- a/src/scripts/network/sendMessage.ts +++ b/src/scripts/network/sendMessage.ts @@ -55,7 +55,6 @@ export const specialSendMessage = async ( } } - // console.log(data); const res = await fetch(primaryUrl, { method: 'POST', body: JSON.stringify({ webhookUrl, data: { ...data, ...embed } }), diff --git a/src/scripts/network/storeMessageData.ts b/src/scripts/network/storeMessageData.ts index 3c40642e8..7b742f66b 100644 --- a/src/scripts/network/storeMessageData.ts +++ b/src/scripts/network/storeMessageData.ts @@ -1,7 +1,7 @@ import db from '../../utils/Db.js'; import { originalMessages } from '@prisma/client'; import { APIMessage, Message } from 'discord.js'; -import { messageTimestamps, modifyConnections } from '../../utils/ConnectedList.js'; +import { modifyConnections } from '../../utils/ConnectedList.js'; import { NetworkAPIError, isNetworkApiError } from './helpers.js'; import Logger from '../../utils/Logger.js'; @@ -59,7 +59,8 @@ export default async ( } // store message timestamps to push to db later - messageTimestamps.set(message.channel.id, message.createdAt); + await db.cache.set(`msgTimestamp:${message.channelId}`, message.createdAt.toString()); + // disconnect network if, webhook does not exist/bot cannot access webhook if (invalidWebhookURLs.length > 0) { await modifyConnections({ webhookURL: { in: invalidWebhookURLs } }, { connected: false }); diff --git a/src/scripts/reaction/helpers.ts b/src/scripts/reaction/helpers.ts index 0f6784b88..280c5ae7e 100644 --- a/src/scripts/reaction/helpers.ts +++ b/src/scripts/reaction/helpers.ts @@ -1,4 +1,4 @@ -import BlacklistManager from '../../managers/BlacklistManager.js'; +import { Client } from 'discord.js'; /** * Checks if a user or server is blacklisted in a given hub. @@ -7,9 +7,15 @@ import BlacklistManager from '../../managers/BlacklistManager.js'; * @param userId - The ID of the user to check for blacklist. * @returns An object containing whether the user and/or server is blacklisted in the hub. */ -export const checkBlacklists = async (hubId: string, guildId: string, userId: string) => { - const userBlacklisted = await BlacklistManager.fetchUserBlacklist(hubId, userId); - const guildBlacklisted = await BlacklistManager.fetchUserBlacklist(hubId, guildId); +export const checkBlacklists = async ( + client: Client, + hubId: string, + guildId: string, + userId: string, +) => { + const userBlacklisted = await client.userManager.fetchBlacklist(hubId, userId); + const guildBlacklisted = await client.serverBlacklists.fetchBlacklist(hubId, guildId); + if (userBlacklisted || guildBlacklisted) { return { userBlacklisted, serverBlacklisted: guildBlacklisted }; } diff --git a/src/scripts/tasks/syncBotlistStats.ts b/src/scripts/tasks/syncBotlistStats.ts deleted file mode 100644 index 53f191a02..000000000 --- a/src/scripts/tasks/syncBotlistStats.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Api } from '@top-gg/sdk'; -import Logger from '../../utils/Logger.js'; -import { ClusterManager } from 'discord-hybrid-sharding'; -import 'dotenv/config'; - -export const topgg = new Api(process.env.TOPGG_API_KEY as string); - -export default async (manager: ClusterManager) => { - const count = (await manager.fetchClientValues('guilds.cache.size')) as number[]; - const serverCount = count.reduce((p, n) => p + n, 0); - const { totalShards: shardCount } = manager; - - try { - await topgg.postStats({ serverCount, shardCount }); - - Logger.info(`Updated top.gg stats with ${serverCount} guilds and ${shardCount} shards`); - } - catch (e) { - Logger.error('Error updating top.gg stats', e); - } -}; diff --git a/src/scripts/tasks/updateBlacklists.ts b/src/scripts/tasks/updateBlacklists.ts deleted file mode 100644 index 72fc9688b..000000000 --- a/src/scripts/tasks/updateBlacklists.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { blacklistedServers, userData } from '@prisma/client'; -import BlacklistManager from '../../managers/BlacklistManager.js'; -import Scheduler from '../../services/SchedulerService.js'; - -export default async (blacklists: (blacklistedServers | userData)[], scheduler: Scheduler) => { - if (blacklists.length === 0) return; - - const blacklistManager = new BlacklistManager(scheduler); - for (const blacklist of blacklists) { - const blacklistedFrom = 'hubs' in blacklist ? blacklist.hubs : blacklist.blacklistedFrom; - for (const { hubId, expires } of blacklistedFrom) { - if (!expires) continue; - - if (expires < new Date()) { - if ('serverId' in blacklist) { - await blacklistManager.removeBlacklist('server', hubId, blacklist.serverId); - } - else { - await blacklistManager.removeBlacklist('user', hubId, blacklist.userId); - } - continue; - } - - blacklistManager.scheduleRemoval( - 'serverId' in blacklist ? 'server' : 'user', - 'serverId' in blacklist ? blacklist.serverId : blacklist.userId, - hubId, - expires, - ); - } - } -}; diff --git a/src/services/CooldownService.ts b/src/services/CooldownService.ts index 1555c104e..81edb54b6 100644 --- a/src/services/CooldownService.ts +++ b/src/services/CooldownService.ts @@ -1,42 +1,34 @@ -import { Collection } from 'discord.js'; +import db from '../utils/Db.js'; /** Manage and store individual cooldowns */ export default class CooldownService { - private readonly cooldowns: Collection; + readonly prefix = 'cooldown'; - constructor() { - this.cooldowns = new Collection(); - - setInterval(() => { - this.cooldowns.forEach((expires, key) => { - if (expires < Date.now()) this.cooldowns.delete(key); - }); - }, 1000); + private getKey(id: string) { + return `${this.prefix}:${id}`; } - /** * Set a cooldown * @param id A unique id for the cooldown * @param ms The duration of the cooldown in milliseconds */ - public setCooldown(id: string, ms: number): void { - this.cooldowns.set(id, Date.now() + ms); + public async setCooldown(id: string, ms: number) { + await db.cache.set(this.getKey(id), Date.now() + ms, 'PX', ms); } /** Get a cooldown */ - public getCooldown(id: string) { - return this.cooldowns.get(id); + public async getCooldown(id: string) { + return parseInt(await db.cache.get(this.getKey(id)) || '0'); } /** Delete a cooldown */ - public deleteCooldown(id: string): void { - this.cooldowns.delete(id); + public async deleteCooldown(id: string) { + await db.cache.del(this.getKey(id)); } /** Get the remaining cooldown in milliseconds */ - public getRemainingCooldown(id: string): number { - const cooldown = this.getCooldown(id); - if (!cooldown) return 0; - return cooldown - Date.now(); + public async getRemainingCooldown(id: string) { + const cooldown = await this.getCooldown(id); + return cooldown ? cooldown - Date.now() : 0; } } diff --git a/src/scripts/tasks/deleteExpiredInvites.ts b/src/tasks/deleteExpiredInvites.ts similarity index 84% rename from src/scripts/tasks/deleteExpiredInvites.ts rename to src/tasks/deleteExpiredInvites.ts index 890469eac..fd95cd5dc 100644 --- a/src/scripts/tasks/deleteExpiredInvites.ts +++ b/src/tasks/deleteExpiredInvites.ts @@ -1,4 +1,4 @@ -import db from '../../utils/Db.js'; +import db from '../utils/Db.js'; export default async () => { const olderThan1h = new Date(Date.now() - 60 * 60 * 1_000); diff --git a/src/scripts/tasks/pauseIdleConnections.ts b/src/tasks/pauseIdleConnections.ts similarity index 87% rename from src/scripts/tasks/pauseIdleConnections.ts rename to src/tasks/pauseIdleConnections.ts index f2a5332b6..9bbef0795 100644 --- a/src/scripts/tasks/pauseIdleConnections.ts +++ b/src/tasks/pauseIdleConnections.ts @@ -1,12 +1,12 @@ -import db from '../../utils/Db.js'; -import Logger from '../../utils/Logger.js'; +import db from '../utils/Db.js'; +import Logger from '../utils/Logger.js'; import { ClusterManager } from 'discord-hybrid-sharding'; -import { modifyConnection } from '../../utils/ConnectedList.js'; +import { modifyConnection } from '../utils/ConnectedList.js'; import { APIActionRowComponent, APIButtonComponent, Snowflake } from 'discord.js'; -import { buildConnectionButtons } from '../network/components.js'; -import { simpleEmbed } from '../../utils/Utils.js'; +import { buildConnectionButtons } from '../scripts/network/components.js'; +import { simpleEmbed } from '../utils/Utils.js'; import { stripIndents } from 'common-tags'; -import { emojis } from '../../utils/Constants.js'; +import { emojis } from '../utils/Constants.js'; import 'dotenv/config'; export default async (manager: ClusterManager) => { diff --git a/src/tasks/storeMsgTimestamps.ts b/src/tasks/storeMsgTimestamps.ts new file mode 100644 index 000000000..764224374 --- /dev/null +++ b/src/tasks/storeMsgTimestamps.ts @@ -0,0 +1,14 @@ +import { modifyConnection } from '../utils/ConnectedList.js'; +import db from '../utils/Db.js'; +import { serializeCache, getAllDocuments } from '../utils/db/cacheUtils.js'; + +export default async () => { + const data = serializeCache<{ channelId: string; lastActive: Date }>( + await getAllDocuments('msgTimestamp:*'), + ); + + data?.forEach(async ({ lastActive, channelId }) => { + await modifyConnection({ channelId }, { lastActive }); + await db.cache.del(`msgTimestamp:${channelId}`); + }); +}; diff --git a/src/tasks/syncBotlistStats.ts b/src/tasks/syncBotlistStats.ts new file mode 100644 index 000000000..62297cd24 --- /dev/null +++ b/src/tasks/syncBotlistStats.ts @@ -0,0 +1,18 @@ +import { Api } from '@top-gg/sdk'; +import Logger from '../utils/Logger.js'; +import 'dotenv/config'; + +export const topgg = new Api(process.env.TOPGG_API_KEY as string); + +export default async ({ serverCount, shardCount }: { serverCount: number; shardCount: number }) => { + await topgg + .postStats({ serverCount, shardCount }) + .then((data) => { + Logger.info( + `Updated top.gg stats with ${data.serverCount} guilds and ${data.shardCount} shards`, + ); + }) + .catch((e) => { + Logger.error('Error updating top.gg stats', e); + }); +}; diff --git a/src/tasks/updateBlacklists.ts b/src/tasks/updateBlacklists.ts new file mode 100644 index 000000000..02a481e4a --- /dev/null +++ b/src/tasks/updateBlacklists.ts @@ -0,0 +1,38 @@ +import { blacklistedServers, userData } from '@prisma/client'; +import { ClusterManager } from 'discord-hybrid-sharding'; +import { Client } from 'discord.js'; + +export default async (manager: ClusterManager) => { + await manager.broadcastEval( + async (_client) => { + const client = _client as unknown as Client; + const allUsers = await client.userManager.getAllBlacklists(); + const allServers = await client.serverBlacklists.getAllBlacklists(); + + const checkAndUnblacklist = (entities: (userData | blacklistedServers)[] | null) => { + entities?.forEach((entity) => { + entity?.blacklistedFrom.forEach(async (bl) => { + if (bl.expires && new Date(String(bl.expires)) <= new Date()) { + if (client.user) { + await client.userManager.logUnblacklist(bl.hubId, entity.id, { + mod: client.user, + reason: 'Blacklist expired for user.', + }); + } + const upd = await client.userManager.removeBlacklist(bl.hubId, entity.id); + + console.log( + `Updated blacklist for entity ${entity.id}. Total entities: ${entities?.length}. Updated: ${upd}`, + ); + } + }); + }); + }; + + checkAndUnblacklist(allServers); + checkAndUnblacklist(allUsers); + }, + { shard: 0 }, + ); +}; + diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts index b8ca036bf..f9e13749c 100644 --- a/src/typings/index.d.ts +++ b/src/typings/index.d.ts @@ -1,13 +1,12 @@ -import { ClusterClient } from 'discord-hybrid-sharding'; -import { Collection, Snowflake } from 'discord.js'; import Scheduler from '../services/SchedulerService.ts'; import BaseCommand from '../core/BaseCommand.ts'; -import BlacklistManager from '../managers/BlacklistManager.ts'; -import CommandManager from '../managers/CommandManager.ts'; import CooldownService from '../services/CooldownService.ts'; -import { supportedLocaleCodes } from '../utils/Locale.ts'; +import UserDbManager from '../managers/UserDbManager.ts'; +import ServerBlacklisManager from '../managers/ServerBlacklistManager.ts'; +import { ClusterClient } from 'discord-hybrid-sharding'; import { InteractionFunction } from '../decorators/Interaction.ts'; -import { connectionCache } from '../utils/ConnectedList.ts'; +import { supportedLocaleCodes } from '../utils/Locale.ts'; +import { Collection, Snowflake } from 'discord.js'; type RemoveMethods = { [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : RemoveMethods; @@ -24,16 +23,11 @@ declare module 'discord.js' { readonly commandCooldowns: CooldownService; readonly reactionCooldowns: Collection; readonly cluster: ClusterClient; - readonly webhooks: Collection; - readonly connectionCache: typeof connectionCache; + readonly userManager: UserDbManager; + readonly serverBlacklists: ServerBlacklisManager; fetchGuild(guildId: Snowflake): Promise | undefined>; getScheduler(): Scheduler; - - get cachePopulated(): boolean; - - commandManager: CommandManager; - blacklistManager: BlacklistManager; } export interface User { diff --git a/src/utils/ConnectedList.ts b/src/utils/ConnectedList.ts index 7e4f08406..d41c4689e 100644 --- a/src/utils/ConnectedList.ts +++ b/src/utils/ConnectedList.ts @@ -1,63 +1,47 @@ import db from './Db.js'; -import Logger from './Logger.js'; -import { Prisma, connectedList } from '@prisma/client'; -import { Collection } from 'discord.js'; -import { captureException } from '@sentry/node'; +import { connectedList, Prisma } from '@prisma/client'; +import { getAllDocuments, serializeCache } from './db/cacheUtils.js'; -/** 📡 Contains all the **connected** channels from all hubs. */ -export const connectionCache = new Collection(); -export const messageTimestamps = new Collection(); - -export const syncConnectionCache = async ( - where: Prisma.connectedListWhereInput = { connected: true }, -) => { - const start = performance.now(); - Logger.debug('[InterChat]: Started populating connection cache.'); - - const connections = await db.connectedList.findMany({ where }); - - if (connectionCache.size > 0) { - connectionCache.forEach((c) => { - if (!connections.some(({ channelId }) => channelId === c.channelId)) { - connectionCache.delete(c.channelId); - } - }); - } +export const fetchConnection = async (where: Prisma.connectedListWhereUniqueInput) => { + await db.connectedList.findFirst({ where }); +}; +export const getConnection = async (channelId: string) => { + const cache = serializeCache(await db.cache.get(`connectedList:${channelId}`)); - // populate all at once without time delay - connections.forEach((c) => connectionCache.set(c.channelId, c)); + return cache ? cache : await fetchConnection({ channelId }); +}; - const end = performance.now(); - Logger.debug( - `[InterChat]: Connection cache populated with ${connectionCache.size} entries. Took ${end - start}ms.`, - ); +/** + * + * @param where Specify filter to force fetch from the db + */ +export const getAllConnections = async (where?: Prisma.connectedListWhereInput) => { + if (where) return await db.connectedList.findMany({ where }); + return serializeCache(await getAllDocuments('connectedList:*')); }; export const deleteConnection = async (where: Prisma.connectedListWhereUniqueInput) => { - const del = await db.connectedList.delete({ where }); - connectionCache.delete(del.channelId); + await db.connectedList.delete({ where }); }; export const deleteConnections = async (where: Prisma.connectedListWhereInput) => { - await db.connectedList.deleteMany({ where }); - await syncConnectionCache().catch(captureException); + const items = await db.connectedList.findMany({ where }); + if (items.length === 0) return null; + else if (items.length === 1) return await deleteConnection({ id: items[0].id }); + + await db.connectedList.deleteMany({ where: { id: { in: items.map((i) => i.id) } } }); + await getAllConnections({ connected: true }); }; export const connectChannel = async (data: Prisma.connectedListCreateInput) => { - const connection = await db.connectedList.create({ data }); - - connectionCache.set(connection.channelId, connection); - return connection; + return await db.connectedList.create({ data }); }; export const modifyConnection = async ( where: Prisma.connectedListWhereUniqueInput, data: Prisma.connectedListUpdateInput, ) => { - const connection = await db.connectedList.update({ where, data }).catch(() => null); - if (connection) connectionCache.set(connection.channelId, connection); - - return connection; + return await db.connectedList.update({ where, data }).catch(() => null); }; export const modifyConnections = async ( @@ -65,12 +49,5 @@ export const modifyConnections = async ( data: Prisma.connectedListUpdateInput, ) => { await db.connectedList.updateMany({ where, data }); - await syncConnectionCache(where).catch(captureException); -}; - -export const storeMsgTimestamps = (data: Collection): void => { - data.forEach( - async (lastActive, channelId) => - await modifyConnection({ channelId }, { lastActive }), - ); + await getAllConnections(where); }; diff --git a/src/utils/Db.ts b/src/utils/Db.ts index 8b51ccbff..153450d73 100644 --- a/src/utils/Db.ts +++ b/src/utils/Db.ts @@ -1,5 +1,16 @@ import { PrismaClient } from '@prisma/client'; +import { Redis } from 'ioredis'; +import prismaCacheExtension from './db/prismaCacheExtension.js'; -const db = new PrismaClient(); +const redis = new Redis(); +const db = new PrismaClient().$extends({ + name: '[Cache Middleware]', + client: { cache: redis }, + query: { + $allModels: { + $allOperations: prismaCacheExtension, + }, + }, +}); export default db; diff --git a/src/utils/HubLogger/JoinLeave.ts b/src/utils/HubLogger/JoinLeave.ts index 2a701d492..95f31522c 100644 --- a/src/utils/HubLogger/JoinLeave.ts +++ b/src/utils/HubLogger/JoinLeave.ts @@ -3,6 +3,7 @@ import { Guild, EmbedBuilder } from 'discord.js'; import { emojis, colors } from '../Constants.js'; import { sendLog } from './Default.js'; import { fetchHub } from '../Utils.js'; +import { getAllConnections } from '../ConnectedList.js'; export const logJoinToHub = async ( hubId: string, @@ -37,7 +38,7 @@ export const logGuildLeaveToHub = async (hubId: string, server: Guild) => { if (!hub?.logChannels?.joinLeaves) return; const owner = await server.client.users.fetch(server.ownerId).catch(() => null); - const totalConnections = server.client.connectionCache.reduce( + const totalConnections = (await getAllConnections())?.reduce( (total, c) => total + (c.hubId === hub.id && c.connected ? 1 : 0), 0, ); diff --git a/src/utils/HubLogger/ModLogs.ts b/src/utils/HubLogger/ModLogs.ts index bbc076df7..66b5e0042 100644 --- a/src/utils/HubLogger/ModLogs.ts +++ b/src/utils/HubLogger/ModLogs.ts @@ -1,10 +1,8 @@ import { stripIndents } from 'common-tags'; import { User, EmbedBuilder, Snowflake, Client, codeBlock } from 'discord.js'; -import BlacklistManager from '../../managers/BlacklistManager.js'; import { emojis, colors } from '../Constants.js'; -import { fetchHub, toTitleCase } from '../Utils.js'; +import { fetchHub, resolveEval } from '../Utils.js'; import { sendLog } from './Default.js'; -import SuperClient from '../../core/Client.js'; import { hubs } from '@prisma/client'; /** @@ -21,7 +19,7 @@ export const logBlacklist = async ( target: User | Snowflake; mod: User; reason: string; - expires?: Date; + expires: Date | null; }, ) => { const { target: _target, mod, reason, expires } = opts; @@ -35,7 +33,7 @@ export const logBlacklist = async ( let target; if (typeof _target === 'string') { - target = SuperClient.resolveEval( + target = resolveEval( await client.cluster.broadcastEval( (c, guildId) => { const guild = c.guilds.cache.get(guildId); @@ -82,39 +80,61 @@ export const logBlacklist = async ( await sendLog(opts.mod.client, hub.logChannels.modLogs, embed); }; -export const logUnblacklist = async ( +export const logServerUnblacklist = async ( + client: Client, hubId: string, - opts: { - type: 'user' | 'server'; - targetId: string; - mod: User; - reason?: string; - }, + opts: { serverId: string; mod: User | { id: Snowflake; username: string }; reason?: string }, ) => { const hub = await fetchHub(hubId); - if (!hub?.logChannels?.modLogs) return; + const blacklisted = await client.serverBlacklists.fetchBlacklist(hubId, opts.serverId); + const blacklistData = blacklisted?.blacklistedFrom.find((data) => data.hubId === hubId); - let name: string | undefined; - let blacklisted; - let originalReason: string | undefined; + if (!blacklisted || !blacklistData || !hub?.logChannels?.modLogs) return; - if (opts.type === 'user') { - blacklisted = await BlacklistManager.fetchUserBlacklist(hub.id, opts.targetId); - const user = await opts.mod.client.users.fetch(opts.targetId).catch(() => null); + const embed = new EmbedBuilder() + .setAuthor({ name: `Server ${blacklisted.serverName} unblacklisted` }) + .setDescription( + stripIndents` + ${emojis.dotBlue} **Server:** ${blacklisted.serverName} (${blacklisted.id}) + ${emojis.dotBlue} **Moderator:** ${opts.mod.username} (${opts.mod.id}) + ${emojis.dotBlue} **Hub:** ${hub?.name} + `, + ) + .addFields( + { + name: 'Unblacklist Reason', + value: opts.reason ?? 'No reason provided.', + inline: true, + }, + { name: 'Blacklisted For', value: blacklistData.reason ?? 'Unknown', inline: true }, + ) + .setColor(colors.interchatBlue) + .setFooter({ + text: `Unblacklisted by: ${opts.mod.username}`, + iconURL: opts.mod instanceof User ? opts.mod.displayAvatarURL() : undefined, + }); - name = user?.username ?? `${blacklisted?.username}`; - originalReason = blacklisted?.blacklistedFrom.find((h) => h.hubId === hub.id)?.reason; - } - else { - blacklisted = await BlacklistManager.fetchServerBlacklist(hub.id, opts.targetId); - name = blacklisted?.serverName; - } + await sendLog(client, hub.logChannels.modLogs, embed); +}; + +export const logUserUnblacklist = async ( + client: Client, + hubId: string, + opts: { userId: string; mod: User | { id: Snowflake; username: string }; reason?: string }, +) => { + const hub = await fetchHub(hubId); + const blacklisted = await client.userManager.fetchBlacklist(hubId, opts.userId); + if (!blacklisted || !hub?.logChannels?.modLogs) return; + + const user = await client.users.fetch(opts.userId).catch(() => null); + const name = user?.username ?? `${blacklisted?.username}`; + const originalReason = blacklisted?.blacklistedFrom.find((h) => h.hubId === hub.id)?.reason; const embed = new EmbedBuilder() - .setAuthor({ name: `${toTitleCase(opts.type)} ${name} unblacklisted` }) + .setAuthor({ name: `User ${name} unblacklisted` }) .setDescription( stripIndents` - ${emojis.dotBlue} **User:** ${name} (${opts.targetId}) + ${emojis.dotBlue} **User:** ${name} (${opts.userId}) ${emojis.dotBlue} **Moderator:** ${opts.mod.username} (${opts.mod.id}) ${emojis.dotBlue} **Hub:** ${hub?.name} `, @@ -130,10 +150,10 @@ export const logUnblacklist = async ( .setColor(colors.interchatBlue) .setFooter({ text: `Unblacklisted by: ${opts.mod.username}`, - iconURL: opts.mod.displayAvatarURL(), + iconURL: opts.mod instanceof User ? opts.mod.displayAvatarURL() : undefined, }); - await sendLog(opts.mod.client, hub.logChannels.modLogs, embed); + await sendLog(client, hub.logChannels.modLogs, embed); }; export const logMsgDelete = async ( diff --git a/src/utils/HubLogger/Report.ts b/src/utils/HubLogger/Report.ts index ec56656ac..d1d65e48d 100644 --- a/src/utils/HubLogger/Report.ts +++ b/src/utils/HubLogger/Report.ts @@ -9,9 +9,8 @@ import { User, Client, } from 'discord.js'; -import SuperClient from '../../core/Client.js'; import { emojis } from '../Constants.js'; -import { fetchHub } from '../Utils.js'; +import { fetchHub, resolveEval } from '../Utils.js'; import { sendLog } from './Default.js'; export type ReportEvidenceOpts = { @@ -51,7 +50,7 @@ const genJumpLink = async ( if (!messageInDb) return null; // fetch the reports server ID from the log channel's ID - const reportsServerId = SuperClient.resolveEval( + const reportsServerId = resolveEval( await client.cluster.broadcastEval( async (cl, channelId) => { const channel = (await cl.channels diff --git a/src/utils/RandomComponents.ts b/src/utils/RandomComponents.ts index 02dd72331..3dc9b40da 100644 --- a/src/utils/RandomComponents.ts +++ b/src/utils/RandomComponents.ts @@ -55,6 +55,7 @@ export abstract class RandomComponents { } const { userBlacklisted, serverBlacklisted } = await checkBlacklists( + interaction.client, messageInDb.originalMsg.hub.id, interaction.guildId, interaction.user.id, @@ -210,7 +211,9 @@ export abstract class RandomComponents { await interaction.update({ embeds: [ - simpleEmbed(`### ${emojis.tick} Connection Resumed\nConnection has been resumed. Have fun chatting!`), + simpleEmbed( + `### ${emojis.tick} Connection Resumed\nConnection has been resumed. Have fun chatting!`, + ), ], components: [], }); diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 997a985c3..1a0945156 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -3,7 +3,6 @@ import Logger from './Logger.js'; import toLower from 'lodash/toLower.js'; import Scheduler from '../services/SchedulerService.js'; import startCase from 'lodash/startCase.js'; -import SuperClient from '../core/Client.js'; import { ActionRow, ApplicationCommand, @@ -47,6 +46,10 @@ import { CustomID } from './CustomID.js'; import { ClusterManager } from 'discord-hybrid-sharding'; import { deleteConnection, deleteConnections } from './ConnectedList.js'; import { userData } from '@prisma/client'; +import { RemoveMethods } from '../typings/index.js'; + +export const resolveEval = (value: T[]) => + value?.find((res) => Boolean(res)) as RemoveMethods | undefined; /** Convert milliseconds to a human readable time (eg: 1d 2h 3m 4s) */ export const msToReadable = (milliseconds: number) => { @@ -92,12 +95,9 @@ export const hasVoted = async (userId: Snowflake): Promise => { return Boolean(res.voted); }; -export const userVotedToday = async (userId: Snowflake): Promise => { +export const userVotedToday = async (id: Snowflake): Promise => { const res = await db.userData.findFirst({ - where: { - userId, - lastVoted: { gte: new Date(Date.now() - 60 * 60 * 24 * 1000) }, - }, + where: { id, lastVoted: { gte: new Date(Date.now() - 60 * 60 * 24 * 1000) } }, }); return Boolean(res?.lastVoted); @@ -384,13 +384,13 @@ export const getOrdinalSuffix = (num: number) => { return 'th'; }; -export const getDbUser = async (userId: Snowflake) => { - return await db.userData.findFirst({ where: { userId } }); +export const getDbUser = async (id: Snowflake) => { + return await db.userData.findFirst({ where: { id } }); }; export const getUsername = async (client: ClusterManager, userId: Snowflake) => { if (client) { - const username = SuperClient.resolveEval( + const username = resolveEval( await client.broadcastEval( async (c, ctx) => { const user = await c.users.fetch(ctx.userId).catch(() => null); @@ -409,9 +409,11 @@ export const getUsername = async (client: ClusterManager, userId: Snowflake) => export const modifyUserRole = async ( cluster: ClusterManager, action: 'add' | 'remove', - userId: Snowflake, - roleId: Snowflake, - guildId: Snowflake = SUPPORT_SERVER_ID, + { + userId, + roleId, + guildId = SUPPORT_SERVER_ID, + }: { userId: Snowflake; roleId: Snowflake; guildId?: Snowflake }, ) => { await cluster.broadcastEval( async (client, ctx) => { @@ -545,3 +547,7 @@ export const encryptMessage = (string: string, key: Buffer) => { encrypted += cipher.final('hex'); return `${iv.toString('hex')}:${encrypted}`; }; + +export const getTagOrUsername = (username: string, discrim: string) => { + return discrim !== '0' ? `${username}#${discrim}` : username; +}; diff --git a/src/utils/db/cacheUtils.ts b/src/utils/db/cacheUtils.ts new file mode 100644 index 000000000..047e3b7b5 --- /dev/null +++ b/src/utils/db/cacheUtils.ts @@ -0,0 +1,64 @@ +import { Prisma } from '@prisma/client'; +import db from '../Db.js'; +import Logger from '../Logger.js'; + +export const getCacheKey = (prefix: string, id: string) => { + return `${prefix}:${id}`; +}; + +export const cacheData = async (key: string, value: string, model: Prisma.ModelName) => { + // expires after 1 hour + await db.cache.set(getCacheKey(model, key), value, 'EX', 3600).catch((e) => { + Logger.error('Failed to set cache: ', e); + }); +}; + +export const parseKey = (key: string) => { + const [id, model] = key.split(':'); + return { id, model }; +}; + +export const invalidateCacheForModel = async (model: string) => { + const allCacheKeys = await db.cache.keys('*'); + allCacheKeys.forEach(async (key) => { + if (parseKey(key).model === model) { + await db.cache.del(getCacheKey(model, key)); + } + }); +}; + +export function serializeCache(data: string | null): K | null; +export function serializeCache(data: string | (string | null)[] | null): K[] | null; +export function serializeCache(data: string | (string | null)[] | null) { + if (!data) return null; + + if (!Array.isArray(data)) return JSON.parse(data); + else if (data.length > 0) return data.map((v) => (v ? JSON.parse(v) : undefined)).filter(Boolean); + return null; +} + +export const traverseCursor = async ( + result: [cursor: string, elements: string[]], + match: string, + start: number, +) => { + const cursor = parseInt(result[0]); + if (Number.isNaN(cursor) || cursor === 0) return result; + + const _newRes = await db.cache.scan(start, 'MATCH', match, 'COUNT', cursor); + + result[0] = _newRes[0]; + result[1].push(..._newRes[1]); + return result; +}; + +export const getAllDocuments = async (match: string) => { + const start = performance.now(); + const firstIter = await db.cache.scan(0, 'MATCH', match, 'COUNT', 100); + const keys = (await traverseCursor(firstIter, match, 100))[1]; + const result = (await Promise.all(keys.map(async (key) => await db.cache.get(key)))).filter( + Boolean, + ) as string[]; + Logger.info(`Took ${performance.now() - start}ms for ${result.length} keys.`); + return result; +}; \ No newline at end of file diff --git a/src/utils/db/prismaCacheExtension.ts b/src/utils/db/prismaCacheExtension.ts new file mode 100644 index 000000000..fffabbea9 --- /dev/null +++ b/src/utils/db/prismaCacheExtension.ts @@ -0,0 +1,56 @@ +import db from '../Db.js'; +import { Prisma } from '@prisma/client'; +import { cacheData, getCacheKey, invalidateCacheForModel } from './cacheUtils.js'; + +type CacheKeyT = { + model: Prisma.ModelName; + operation: Prisma.PrismaAction | 'aggregateRaw'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: any; + query: (args: unknown) => Promise; +}; + +export default async ({ model, operation, args, query }: CacheKeyT) => { + const isReadWriteOperation = [ + 'create', + 'createMany', + 'update', + 'upsert', + 'findUnique', + 'findMany', + 'findFirst', + 'count', + ].includes(operation); + + // Execute the query + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: any = await query(args); + if (!result) return result; + + if (isReadWriteOperation) { + // Cache the result in Redis for 1 hour + if (Array.isArray(result)) { + if (result.length > 0) { + result.forEach((r) => cacheData(r.id, JSON.stringify(r), model)); + } + } + else { + cacheData(result.id, JSON.stringify(result), model); + } + } + + else if (operation === 'delete') { + await db.cache.del(getCacheKey(model, result.id)); + } + // Invalidate everything related to that model + else if (operation === 'deleteMany') { + invalidateCacheForModel(model); + } + else if (operation === 'updateMany') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (db[model] as any).findMany({ where: args.where }).then(() => null); + } + + // always return the result regardless of which operation it was + return result; +}; diff --git a/yarn.lock b/yarn.lock index 1f353b038..3b34eb506 100644 --- a/yarn.lock +++ b/yarn.lock @@ -277,6 +277,13 @@ __metadata: languageName: node linkType: hard +"@ioredis/commands@npm:^1.1.1": + version: 1.2.0 + resolution: "@ioredis/commands@npm:1.2.0" + checksum: 10c0/a5d3c29dd84d8a28b7c67a441ac1715cbd7337a7b88649c0f17c345d89aa218578d2b360760017c48149ef8a70f44b051af9ac0921a0622c2b479614c4f65b36 + languageName: node + linkType: hard + "@mapbox/node-pre-gyp@npm:1.0.9": version: 1.0.9 resolution: "@mapbox/node-pre-gyp@npm:1.0.9" @@ -901,9 +908,9 @@ __metadata: languageName: node linkType: hard -"@stylistic/eslint-plugin-js@npm:2.2.1, @stylistic/eslint-plugin-js@npm:^2.2.1": - version: 2.2.1 - resolution: "@stylistic/eslint-plugin-js@npm:2.2.1" +"@stylistic/eslint-plugin-js@npm:2.3.0, @stylistic/eslint-plugin-js@npm:^2.3.0": + version: 2.3.0 + resolution: "@stylistic/eslint-plugin-js@npm:2.3.0" dependencies: "@types/eslint": "npm:^8.56.10" acorn: "npm:^8.11.3" @@ -911,61 +918,61 @@ __metadata: espree: "npm:^10.0.1" peerDependencies: eslint: ">=8.40.0" - checksum: 10c0/aec3abbb0d5c5442af06c567bd8c2ecdfeb4a8cc4c91193a8430af6084f2478f7e19403f12b9a4b40b91a2120cc62f35c378abc8e368f1ef78ddcf9e5786ccf2 + checksum: 10c0/41edf0a6ac4643145d4ccb939b24e0227e790eb4efc7ebc4d8bb68bb99ff6406af154a3384b12147c4b34aa6c2bed506703ef8d82fb4c73ca7aa978fa4fed922 languageName: node linkType: hard -"@stylistic/eslint-plugin-jsx@npm:2.2.1": - version: 2.2.1 - resolution: "@stylistic/eslint-plugin-jsx@npm:2.2.1" +"@stylistic/eslint-plugin-jsx@npm:2.3.0": + version: 2.3.0 + resolution: "@stylistic/eslint-plugin-jsx@npm:2.3.0" dependencies: - "@stylistic/eslint-plugin-js": "npm:^2.2.1" + "@stylistic/eslint-plugin-js": "npm:^2.3.0" "@types/eslint": "npm:^8.56.10" estraverse: "npm:^5.3.0" picomatch: "npm:^4.0.2" peerDependencies: eslint: ">=8.40.0" - checksum: 10c0/653f6b22fc04324afee8d37ce82179db9e5b66dee22809e259030dec23c6ed5bd4fc0f8e9e5979495d30f0ffabe729f489b081b416e53e09a5e854234ebae91b + checksum: 10c0/6aec33ed4c62f4a8dadc8ef00d7220ed3ef7f0b3f12d4afc9932aafd2d4502789c5d32b111ddc6391d220a2855cc5225070ae0c09c1680761f016dbeea94e676 languageName: node linkType: hard -"@stylistic/eslint-plugin-plus@npm:2.2.1": - version: 2.2.1 - resolution: "@stylistic/eslint-plugin-plus@npm:2.2.1" +"@stylistic/eslint-plugin-plus@npm:2.3.0": + version: 2.3.0 + resolution: "@stylistic/eslint-plugin-plus@npm:2.3.0" dependencies: "@types/eslint": "npm:^8.56.10" "@typescript-eslint/utils": "npm:^7.12.0" peerDependencies: eslint: "*" - checksum: 10c0/2115e92b2dca9598ca1b4c484fb60e7b5bbc082f7fb02b24e29cd88337e9fd6a69154c975878ca6ff468e91d0b8ab00abb7224127d0772e53ca2ee254c160f02 + checksum: 10c0/709975a01bfccfbd25690fa12a9a5d15685e9c31e09ff3bda0f573a94411ada81c2511a91c48cd638b32c7a5fb0a9283d5430f7bb1cf97a93ff5ea458f312c56 languageName: node linkType: hard -"@stylistic/eslint-plugin-ts@npm:2.2.1": - version: 2.2.1 - resolution: "@stylistic/eslint-plugin-ts@npm:2.2.1" +"@stylistic/eslint-plugin-ts@npm:2.3.0": + version: 2.3.0 + resolution: "@stylistic/eslint-plugin-ts@npm:2.3.0" dependencies: - "@stylistic/eslint-plugin-js": "npm:2.2.1" + "@stylistic/eslint-plugin-js": "npm:2.3.0" "@types/eslint": "npm:^8.56.10" "@typescript-eslint/utils": "npm:^7.12.0" peerDependencies: eslint: ">=8.40.0" - checksum: 10c0/8d7610ec69602daf309e774a442ec6a52efdacc41d50de088b28fd5520ef104d4c98e9cf56372baab314659bd5b592ab4efd9364f18696ab7e0081cc5192b3cb + checksum: 10c0/df8a334eccdbf4af291e40e0b14f07ff897778a653836c2c2e7f7f8e41608b0e151f0c9cf11863ff610bb6139a183a2cb465557dd09ae4b5957a8eee974c089b languageName: node linkType: hard -"@stylistic/eslint-plugin@npm:^2.2.1": - version: 2.2.1 - resolution: "@stylistic/eslint-plugin@npm:2.2.1" +"@stylistic/eslint-plugin@npm:^2.3.0": + version: 2.3.0 + resolution: "@stylistic/eslint-plugin@npm:2.3.0" dependencies: - "@stylistic/eslint-plugin-js": "npm:2.2.1" - "@stylistic/eslint-plugin-jsx": "npm:2.2.1" - "@stylistic/eslint-plugin-plus": "npm:2.2.1" - "@stylistic/eslint-plugin-ts": "npm:2.2.1" + "@stylistic/eslint-plugin-js": "npm:2.3.0" + "@stylistic/eslint-plugin-jsx": "npm:2.3.0" + "@stylistic/eslint-plugin-plus": "npm:2.3.0" + "@stylistic/eslint-plugin-ts": "npm:2.3.0" "@types/eslint": "npm:^8.56.10" peerDependencies: eslint: ">=8.40.0" - checksum: 10c0/c4da02773951f9fcfc33e533e9c7aa23294fc07e70e15199ab3ecedc410261d25b68d49e86dc6a9d7ea6b769d16dfac5ca30034c7b70700405c9eb0eda073f29 + checksum: 10c0/17a82df97a0714406eab76cf42399ec37b47937df0e29ce2254622e734490234d770acf21c086f70415816a0ec3dc9a2da36a0aa18c8596bd9ec82a1ec8bc867 languageName: node linkType: hard @@ -1287,10 +1294,10 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:^4.17.5": - version: 4.17.5 - resolution: "@types/lodash@npm:4.17.5" - checksum: 10c0/55924803ed853e72261512bd3eaf2c5b16558c3817feb0a3125ef757afe46e54b86f33d1960e40b7606c0ddab91a96f47966bf5e6006b7abfd8994c13b04b19b +"@types/lodash@npm:^4.17.6": + version: 4.17.6 + resolution: "@types/lodash@npm:4.17.6" + checksum: 10c0/3b197ac47af9443fee8c4719c5ffde527d7febc018b827d44a6bc2523c728c7adfdd25196fdcfe3eed827993e0c41a917d0da6e78938b18b2be94164789f1117 languageName: node linkType: hard @@ -1350,12 +1357,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.14.2": - version: 20.14.2 - resolution: "@types/node@npm:20.14.2" +"@types/node@npm:^20.14.9": + version: 20.14.9 + resolution: "@types/node@npm:20.14.9" dependencies: undici-types: "npm:~5.26.4" - checksum: 10c0/2d86e5f2227aaa42212e82ea0affe72799111b888ff900916376450b02b09b963ca888b20d9c332d8d2b833ed4781987867a38eaa2e4863fa8439071468b0a6f + checksum: 10c0/911ffa444dc032897f4a23ed580c67903bd38ea1c5ec99b1d00fa10b83537a3adddef8e1f29710cbdd8e556a61407ed008e06537d834e48caf449ce59f87d387 languageName: node linkType: hard @@ -1485,15 +1492,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:7.13.0": - version: 7.13.0 - resolution: "@typescript-eslint/eslint-plugin@npm:7.13.0" +"@typescript-eslint/eslint-plugin@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/eslint-plugin@npm:7.15.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:7.13.0" - "@typescript-eslint/type-utils": "npm:7.13.0" - "@typescript-eslint/utils": "npm:7.13.0" - "@typescript-eslint/visitor-keys": "npm:7.13.0" + "@typescript-eslint/scope-manager": "npm:7.15.0" + "@typescript-eslint/type-utils": "npm:7.15.0" + "@typescript-eslint/utils": "npm:7.15.0" + "@typescript-eslint/visitor-keys": "npm:7.15.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -1504,25 +1511,25 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/00a69d029713252c03490e0a9c49c9136d99c9c1888dd3570b1e044c9a740b59c2e488849beda654d6fc0a69e2549445c16d443bcf5832c66b7a4472b42826ae + checksum: 10c0/7ed4ef8355cb60f02ed603673ef749928a001931c534960d1f3f9f9b8092f4abd7ec1e80a33b4c38efb6e8e66c902583bd56a4c4d6ccbd870677a40680a7d1f5 languageName: node linkType: hard -"@typescript-eslint/parser@npm:7.13.0": - version: 7.13.0 - resolution: "@typescript-eslint/parser@npm:7.13.0" +"@typescript-eslint/parser@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/parser@npm:7.15.0" dependencies: - "@typescript-eslint/scope-manager": "npm:7.13.0" - "@typescript-eslint/types": "npm:7.13.0" - "@typescript-eslint/typescript-estree": "npm:7.13.0" - "@typescript-eslint/visitor-keys": "npm:7.13.0" + "@typescript-eslint/scope-manager": "npm:7.15.0" + "@typescript-eslint/types": "npm:7.15.0" + "@typescript-eslint/typescript-estree": "npm:7.15.0" + "@typescript-eslint/visitor-keys": "npm:7.15.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/8cf58116d6577c9459db3e3047e337dc41d914bf222a33b20e149515d037e09e6171fbac5af02b66aa6fbad81dd492fa5b7bcd44aaf659d4e9b02ab23100f955 + checksum: 10c0/8dcad9b84e2cbf89afea97ee7f690f91b487eed21d01997126f98cb7dd56d3b6c98c7ecbdbeda35904af521c4ed746c47887e908f8a1e2148d47c05b491d7b9d languageName: node linkType: hard @@ -1536,12 +1543,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:7.13.0": - version: 7.13.0 - resolution: "@typescript-eslint/type-utils@npm:7.13.0" +"@typescript-eslint/scope-manager@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/scope-manager@npm:7.15.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:7.13.0" - "@typescript-eslint/utils": "npm:7.13.0" + "@typescript-eslint/types": "npm:7.15.0" + "@typescript-eslint/visitor-keys": "npm:7.15.0" + checksum: 10c0/781ec31a07ab7f0bdfb07dd271ef6553aa98f8492f1b3a67c65d178c94d590f4fd2e0916450f2446f1da2fbe007f3454c360ccb25f4d69612f782eb499f400ab + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/type-utils@npm:7.15.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:7.15.0" + "@typescript-eslint/utils": "npm:7.15.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.3.0" peerDependencies: @@ -1549,7 +1566,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/240e9b34e8602444cd234b84c9e3e52c565e3141a4942751f597c38cee48f7cb43c42a093d219ac6404dca2e74b54d2a8121fe66cbc59f404cb0ec2adecd8520 + checksum: 10c0/06189eb05d741f05977bbc029c6ac46edd566e0136f2f2c22429fd5f2be1224e2d9135b7021bc686871bfaec9c05a5c9990a321762d3abd06e457486956326ba languageName: node linkType: hard @@ -1560,6 +1577,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/types@npm:7.15.0" + checksum: 10c0/935387b21d9fdff65de86f6350cdda1f0614e269324f3a4f0a2ca1b0d72ef4b1d40c7de2f3a20a6f8c83edca6507bfbac3168c860625859e59fc455c80392bed + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:7.13.0": version: 7.13.0 resolution: "@typescript-eslint/typescript-estree@npm:7.13.0" @@ -1579,7 +1603,40 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:7.13.0, @typescript-eslint/utils@npm:^7.12.0": +"@typescript-eslint/typescript-estree@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.15.0" + dependencies: + "@typescript-eslint/types": "npm:7.15.0" + "@typescript-eslint/visitor-keys": "npm:7.15.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/0d6e61cb36c4612147ceea796c2bdbb65fca59170d9d768cff314146c5564253a058cbcb9e251722cd76c92a90c257e1210a69f8d4377c8002f211c574d18d24 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/utils@npm:7.15.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:7.15.0" + "@typescript-eslint/types": "npm:7.15.0" + "@typescript-eslint/typescript-estree": "npm:7.15.0" + peerDependencies: + eslint: ^8.56.0 + checksum: 10c0/26aced17976cee0aa39a79201f68b384bbce1dc96e1c70d0e5f790e1e5655b1b1ddb2afd9eaf3fce9a48c0fb69daecd37a99fdbcdbf1cb58c65ae89ecac88a2c + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:^7.12.0": version: 7.13.0 resolution: "@typescript-eslint/utils@npm:7.13.0" dependencies: @@ -1603,6 +1660,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.15.0" + dependencies: + "@typescript-eslint/types": "npm:7.15.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 10c0/7509f01c8cd2126a38213bc735a77aa7e976340af0d664be5b2ccd01b8211724b2ea129e33bfd32fe5feac848b7b68ca55bb533f6ccfeec1d2f26a91240489b9 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -2144,6 +2211,13 @@ __metadata: languageName: node linkType: hard +"cluster-key-slot@npm:^1.1.0": + version: 1.1.2 + resolution: "cluster-key-slot@npm:1.1.2" + checksum: 10c0/d7d39ca28a8786e9e801eeb8c770e3c3236a566625d7299a47bb71113fb2298ce1039596acb82590e598c52dbc9b1f088c8f587803e697cb58e1867a95ff94d3 + languageName: node + linkType: hard + "color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -2729,6 +2803,13 @@ __metadata: languageName: node linkType: hard +"denque@npm:^2.1.0": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 10c0/f9ef81aa0af9c6c614a727cb3bd13c5d7db2af1abf9e6352045b86e85873e629690f6222f4edd49d10e4ccf8f078bbeec0794fafaf61b659c0589d0c511ec363 + languageName: node + linkType: hard + "depd@npm:2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -3996,14 +4077,14 @@ __metadata: dependencies: "@prisma/client": "npm:^5.15.0" "@sentry/node": "npm:^8.9.2" - "@stylistic/eslint-plugin": "npm:^2.2.1" + "@stylistic/eslint-plugin": "npm:^2.3.0" "@tensorflow/tfjs-node": "npm:^4.20.0" "@top-gg/sdk": "npm:^3.1.6" "@types/common-tags": "npm:^1.8.4" "@types/express": "npm:^4.17.21" "@types/js-yaml": "npm:^4.0.9" - "@types/lodash": "npm:^4.17.5" - "@types/node": "npm:^20.14.2" + "@types/lodash": "npm:^4.17.6" + "@types/node": "npm:^20.14.9" "@types/source-map-support": "npm:^0.5.10" common-tags: "npm:^1.8.2" cz-conventional-changelog: "npm:^3.3.0" @@ -4014,6 +4095,7 @@ __metadata: express: "npm:^4.19.2" google-translate-api-x: "npm:^10.6.8" husky: "npm:^9.0.11" + ioredis: "npm:^5.4.1" js-yaml: "npm:^4.1.0" lint-staged: "npm:^15.2.7" lodash: "npm:^4.17.21" @@ -4025,12 +4107,29 @@ __metadata: source-map-support: "npm:^0.5.21" standard-version: "npm:^9.5.0" tsc-watch: "npm:^6.2.0" - typescript: "npm:^5.4.5" - typescript-eslint: "npm:^7.13.0" + typescript: "npm:^5.5.3" + typescript-eslint: "npm:^7.15.0" winston: "npm:^3.13.0" languageName: unknown linkType: soft +"ioredis@npm:^5.4.1": + version: 5.4.1 + resolution: "ioredis@npm:5.4.1" + dependencies: + "@ioredis/commands": "npm:^1.1.1" + cluster-key-slot: "npm:^1.1.0" + debug: "npm:^4.3.4" + denque: "npm:^2.1.0" + lodash.defaults: "npm:^4.2.0" + lodash.isarguments: "npm:^3.1.0" + redis-errors: "npm:^1.2.0" + redis-parser: "npm:^3.0.0" + standard-as-callback: "npm:^2.1.0" + checksum: 10c0/5d28b7c89a3cab5b76d75923d7d4ce79172b3a1ca9be690133f6e8e393a7a4b4ffd55513e618bbb5504fed80d9e1395c9d9531a7c5c5c84aa4c4e765cca75456 + languageName: node + linkType: hard + "ipaddr.js@npm:1.9.1": version: 1.9.1 resolution: "ipaddr.js@npm:1.9.1" @@ -4420,6 +4519,20 @@ __metadata: languageName: node linkType: hard +"lodash.defaults@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.defaults@npm:4.2.0" + checksum: 10c0/d5b77aeb702caa69b17be1358faece33a84497bcca814897383c58b28a2f8dfc381b1d9edbec239f8b425126a3bbe4916223da2a576bb0411c2cefd67df80707 + languageName: node + linkType: hard + +"lodash.isarguments@npm:^3.1.0": + version: 3.1.0 + resolution: "lodash.isarguments@npm:3.1.0" + checksum: 10c0/5e8f95ba10975900a3920fb039a3f89a5a79359a1b5565e4e5b4310ed6ebe64011e31d402e34f577eca983a1fc01ff86c926e3cbe602e1ddfc858fdd353e62d8 + languageName: node + linkType: hard + "lodash.ismatch@npm:^4.4.0": version: 4.4.0 resolution: "lodash.ismatch@npm:4.4.0" @@ -5619,6 +5732,22 @@ __metadata: languageName: node linkType: hard +"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": + version: 1.2.0 + resolution: "redis-errors@npm:1.2.0" + checksum: 10c0/5b316736e9f532d91a35bff631335137a4f974927bb2fb42bf8c2f18879173a211787db8ac4c3fde8f75ed6233eb0888e55d52510b5620e30d69d7d719c8b8a7 + languageName: node + linkType: hard + +"redis-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "redis-parser@npm:3.0.0" + dependencies: + redis-errors: "npm:^1.0.0" + checksum: 10c0/ee16ac4c7b2a60b1f42a2cdaee22b005bd4453eb2d0588b8a4939718997ae269da717434da5d570fe0b05030466eeb3f902a58cf2e8e1ca058bf6c9c596f632f + languageName: node + linkType: hard + "regenerator-runtime@npm:^0.13.5": version: 0.13.11 resolution: "regenerator-runtime@npm:0.13.11" @@ -6093,6 +6222,13 @@ __metadata: languageName: node linkType: hard +"standard-as-callback@npm:^2.1.0": + version: 2.1.0 + resolution: "standard-as-callback@npm:2.1.0" + checksum: 10c0/012677236e3d3fdc5689d29e64ea8a599331c4babe86956bf92fc5e127d53f85411c5536ee0079c52c43beb0026b5ce7aa1d834dd35dd026e82a15d1bcaead1f + languageName: node + linkType: hard + "standard-version@npm:^9.5.0": version: 9.5.0 resolution: "standard-version@npm:9.5.0" @@ -6495,39 +6631,39 @@ __metadata: languageName: node linkType: hard -"typescript-eslint@npm:^7.13.0": - version: 7.13.0 - resolution: "typescript-eslint@npm:7.13.0" +"typescript-eslint@npm:^7.15.0": + version: 7.15.0 + resolution: "typescript-eslint@npm:7.15.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:7.13.0" - "@typescript-eslint/parser": "npm:7.13.0" - "@typescript-eslint/utils": "npm:7.13.0" + "@typescript-eslint/eslint-plugin": "npm:7.15.0" + "@typescript-eslint/parser": "npm:7.15.0" + "@typescript-eslint/utils": "npm:7.15.0" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/ad067868ede764d411f4933285faca0d41c7e3ca439d7aac032ed78db7703e9842f5bbad4344004fa876a3577cda3c56b0716897f94a0d1aec00a90d6c0d3990 + checksum: 10c0/98293831f7557831b80143b0e717d2a61dca289d637ef464da524880fab2ea62fb61dd952707c571719914c1565942504db2b4ccfe7178a48971e69f270c1abc languageName: node linkType: hard -"typescript@npm:^5.4.5": - version: 5.4.5 - resolution: "typescript@npm:5.4.5" +"typescript@npm:^5.5.3": + version: 5.5.3 + resolution: "typescript@npm:5.5.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/2954022ada340fd3d6a9e2b8e534f65d57c92d5f3989a263754a78aba549f7e6529acc1921913560a4b816c46dce7df4a4d29f9f11a3dc0d4213bb76d043251e + checksum: 10c0/f52c71ccbc7080b034b9d3b72051d563601a4815bf3e39ded188e6ce60813f75dbedf11ad15dd4d32a12996a9ed8c7155b46c93a9b9c9bad1049766fe614bbdd languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.4.5#optional!builtin": - version: 5.4.5 - resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=5adc0c" +"typescript@patch:typescript@npm%3A^5.5.3#optional!builtin": + version: 5.5.3 + resolution: "typescript@patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=b45daf" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/db2ad2a16ca829f50427eeb1da155e7a45e598eec7b086d8b4e8ba44e5a235f758e606d681c66992230d3fc3b8995865e5fd0b22a2c95486d0b3200f83072ec9 + checksum: 10c0/5a437c416251334deeaf29897157032311f3f126547cfdc4b133768b606cb0e62bcee733bb97cf74c42fe7268801aea1392d8e40988cdef112e9546eba4c03c5 languageName: node linkType: hard