From 59cc44a197c74f09046f97839711f405bdcf68de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aura=20Rom=C3=A1n?= Date: Thu, 28 Dec 2023 11:03:19 +0100 Subject: [PATCH] refactor: many bugfixes --- ormconfig.js | 3 +- src/.env.development | 10 - src/commands/Admin/roleset.ts | 8 +- src/commands/Fun/dice.ts | 2 +- src/commands/Games/hungergames.ts | 30 +-- src/commands/General/invite.ts | 46 ++--- .../manage-command-auto-delete.ts | 7 +- .../Configuration/manage-command-channel.ts | 7 +- .../Configuration/manage-reaction-roles.ts | 7 +- src/commands/Management/Helpers/guild-info.ts | 2 +- src/commands/Management/Helpers/role-info.ts | 8 +- .../Management/Members/stickyRoles.ts | 7 +- .../Management/Message Filters/filter.ts | 7 +- src/commands/Management/permissionNodes.ts | 7 +- src/commands/Moderation/Management/history.ts | 5 +- .../Moderation/Management/moderations.ts | 6 +- src/commands/Moderation/Utilities/case.ts | 5 +- src/commands/Moderation/lockdown.ts | 6 +- src/commands/System/Admin/eval.ts | 10 +- src/commands/Twitch/twitchsubscription.ts | 7 +- src/lib/api/utils.ts | 2 +- src/lib/database/database.config.ts | 9 +- .../entities/GuildSubscriptionEntity.ts | 4 +- .../entities/TwitchSubscriptionEntity.ts | 4 +- src/lib/games/base/BaseReactionController.ts | 14 +- src/lib/games/connect-four/ConnectFourGame.ts | 4 +- src/lib/games/tic-tac-toe/TicTacToeGame.ts | 2 +- .../moderation/managers/ModerationManager.ts | 4 +- src/lib/util/Security/ModerationActions.ts | 8 +- src/lib/util/Security/RegexCreator.ts | 24 +-- src/listeners/commands/_shared.ts | 193 ++++++++++++++++++ src/listeners/commands/messageCommandError.ts | 186 +---------------- .../commands/messageSubcommandError.ts | 9 + .../guilds/members/rawMemberRemoveNotify.ts | 8 +- 34 files changed, 360 insertions(+), 301 deletions(-) create mode 100644 src/listeners/commands/_shared.ts create mode 100644 src/listeners/commands/messageSubcommandError.ts diff --git a/ormconfig.js b/ormconfig.js index 006b7b92db6..d163349652f 100644 --- a/ormconfig.js +++ b/ormconfig.js @@ -1,2 +1 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires -module.exports = require('./dist/lib/database/database.config').config; +export default (await import('./dist/lib/database/database.config.js')).AppDataConfig; diff --git a/src/.env.development b/src/.env.development index 7f8cb44b0b8..2d341604e97 100644 --- a/src/.env.development +++ b/src/.env.development @@ -1,23 +1,15 @@ -CLIENT_NAME='Skyra' CLIENT_VERSION='6.3.0-dev [Sapphire Edition]' CLIENT_PREFIX='sd!' CLIENT_REGEX_PREFIX='' -CLIENT_OWNERS='' CLIENT_ID='' -SHARDS='"auto"' CLIENT_PRESENCE_NAME='sd!help' -CLIENT_PRESENCE_TYPE='LISTENING' API_ENABLED=true API_ORIGIN='http://127.0.0.1:3000' API_PREFIX='/' API_PORT=8282 -OAUTH_COOKIE='SKYRA_AUTH' -OAUTH_DOMAIN_OVERWRITE='127.0.0.1' -OAUTH_REDIRECT_URI='http://127.0.0.1:3000/oauth/callback' -OAUTH_SCOPE='identify guilds' OAUTH_SECRET='' TWITCH_CALLBACK='http://localhost/twitch/event_sub_verify/' @@ -38,8 +30,6 @@ INFLUX_ORG_ANALYTICS_BUCKET='analytics' WEBHOOK_ERROR_ID='648663047615021058' WEBHOOK_ERROR_TOKEN='' -WORKER_COUNT=2 - # Tokens DISCORD_TOKEN='' BOTLIST_SPACE_TOKEN='' diff --git a/src/commands/Admin/roleset.ts b/src/commands/Admin/roleset.ts index 9e282b53e43..7be395a7f17 100644 --- a/src/commands/Admin/roleset.ts +++ b/src/commands/Admin/roleset.ts @@ -12,7 +12,13 @@ import { send } from '@sapphire/plugin-editable-commands'; detailedDescription: LanguageKeys.Commands.Admin.RoleSetExtended, permissionLevel: PermissionLevels.Administrator, runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [{ name: 'add' }, { name: 'remove' }, { name: 'reset' }, { name: 'list' }, { name: 'auto', default: true }] + subcommands: [ + { name: 'add', messageRun: 'add' }, + { name: 'remove', messageRun: 'remove' }, + { name: 'reset', messageRun: 'reset' }, + { name: 'list', messageRun: 'list' }, + { name: 'auto', messageRun: 'auto', default: true } + ] }) export class UserCommand extends SkyraSubcommand { // This subcommand will always ADD roles in to a existing set OR it will create a new set if that set does not exist diff --git a/src/commands/Fun/dice.ts b/src/commands/Fun/dice.ts index e2e763994b4..3c1fb8a064e 100644 --- a/src/commands/Fun/dice.ts +++ b/src/commands/Fun/dice.ts @@ -32,7 +32,7 @@ export class UserCommand extends SkyraCommand { private readonly kDice20TrailRegExp = /([+-x*])\s*(\d+)/g; public override async messageRun(message: Message, args: SkyraCommand.Args) { - const amountOrDice = await args.pick('integer', { minimum: 1, maximum: 1024 }).catch(() => args.rest('string')); + const amountOrDice = args.finished ? 6 : await args.pick('integer', { minimum: 1, maximum: 1024 }).catch(() => args.rest('string')); const content = args.t(LanguageKeys.Commands.Fun.DiceOutput, { result: this.roll(amountOrDice) }); return send(message, content); } diff --git a/src/commands/Games/hungergames.ts b/src/commands/Games/hungergames.ts index 4f581629c03..08ed2d1e553 100644 --- a/src/commands/Games/hungergames.ts +++ b/src/commands/Games/hungergames.ts @@ -81,8 +81,8 @@ export class UserCommand extends SkyraCommand { const events = game.bloodbath ? LanguageKeys.Commands.Games.HungerGamesBloodbath : game.sun - ? LanguageKeys.Commands.Games.HungerGamesDay - : LanguageKeys.Commands.Games.HungerGamesNight; + ? LanguageKeys.Commands.Games.HungerGamesDay + : LanguageKeys.Commands.Games.HungerGamesNight; // Main logic of the game const { results, deaths } = this.makeResultEvents( @@ -168,8 +168,8 @@ export class UserCommand extends SkyraCommand { const headerKey = game.bloodbath ? LanguageKeys.Commands.Games.HungerGamesResultHeaderBloodbath : game.sun - ? LanguageKeys.Commands.Games.HungerGamesResultHeaderSun - : LanguageKeys.Commands.Games.HungerGamesResultHeaderMoon; + ? LanguageKeys.Commands.Games.HungerGamesResultHeaderSun + : LanguageKeys.Commands.Games.HungerGamesResultHeaderMoon; const header = t(headerKey, { game }); const death = deaths.length @@ -251,18 +251,18 @@ export class UserCommand extends SkyraCommand { // If there are more than 16 tributes, perform a large blood bath return game.tributes.size >= 16 ? // For 16 people, 4 die, 36 -> 6, and so on keeps the game interesting. - // If it's in bloodbath, perform 50% more deaths. - Math.ceil(Math.sqrt(game.tributes.size) * (game.bloodbath ? 1.5 : 1)) + // If it's in bloodbath, perform 50% more deaths. + Math.ceil(Math.sqrt(game.tributes.size) * (game.bloodbath ? 1.5 : 1)) : // If there are more than 7 tributes, proceed to kill them in 4 or more. - game.tributes.size > 7 - ? // If it's a bloodbath, perform mass death (12 -> 7), else eliminate 4. - game.bloodbath - ? Math.ceil(Math.min(game.tributes.size - 3, Math.sqrt(game.tributes.size) * 2)) - : 4 - : // If there are 4 tributes, eliminate 2, else 1 (3 -> 2, 2 -> 1) - game.tributes.size === 4 - ? 2 - : 1; + game.tributes.size > 7 + ? // If it's a bloodbath, perform mass death (12 -> 7), else eliminate 4. + game.bloodbath + ? Math.ceil(Math.min(game.tributes.size - 3, Math.sqrt(game.tributes.size) * 2)) + : 4 + : // If there are 4 tributes, eliminate 2, else 1 (3 -> 2, 2 -> 1) + game.tributes.size === 4 + ? 2 + : 1; } } diff --git a/src/commands/General/invite.ts b/src/commands/General/invite.ts index 670e97957ac..c660478face 100644 --- a/src/commands/General/invite.ts +++ b/src/commands/General/invite.ts @@ -52,29 +52,29 @@ export class UserCommand extends SkyraCommand { permissions: shouldNotAddPermissions ? 0n : PermissionFlagsBits.AddReactions | - PermissionFlagsBits.AttachFiles | - PermissionFlagsBits.BanMembers | - PermissionFlagsBits.ChangeNickname | - PermissionFlagsBits.CreatePrivateThreads | - PermissionFlagsBits.CreatePublicThreads | - PermissionFlagsBits.DeafenMembers | - PermissionFlagsBits.EmbedLinks | - PermissionFlagsBits.KickMembers | - PermissionFlagsBits.ManageChannels | - PermissionFlagsBits.ManageGuildExpressions | - PermissionFlagsBits.ManageGuild | - PermissionFlagsBits.ManageMessages | - PermissionFlagsBits.ManageNicknames | - PermissionFlagsBits.ManageRoles | - PermissionFlagsBits.ManageThreads | - PermissionFlagsBits.MoveMembers | - PermissionFlagsBits.MuteMembers | - PermissionFlagsBits.ReadMessageHistory | - PermissionFlagsBits.SendMessages | - PermissionFlagsBits.SendMessagesInThreads | - PermissionFlagsBits.UseExternalEmojis | - PermissionFlagsBits.UseExternalStickers | - PermissionFlagsBits.ViewChannel + PermissionFlagsBits.AttachFiles | + PermissionFlagsBits.BanMembers | + PermissionFlagsBits.ChangeNickname | + PermissionFlagsBits.CreatePrivateThreads | + PermissionFlagsBits.CreatePublicThreads | + PermissionFlagsBits.DeafenMembers | + PermissionFlagsBits.EmbedLinks | + PermissionFlagsBits.KickMembers | + PermissionFlagsBits.ManageChannels | + PermissionFlagsBits.ManageGuildExpressions | + PermissionFlagsBits.ManageGuild | + PermissionFlagsBits.ManageMessages | + PermissionFlagsBits.ManageNicknames | + PermissionFlagsBits.ManageRoles | + PermissionFlagsBits.ManageThreads | + PermissionFlagsBits.MoveMembers | + PermissionFlagsBits.MuteMembers | + PermissionFlagsBits.ReadMessageHistory | + PermissionFlagsBits.SendMessages | + PermissionFlagsBits.SendMessagesInThreads | + PermissionFlagsBits.UseExternalEmojis | + PermissionFlagsBits.UseExternalStickers | + PermissionFlagsBits.ViewChannel }); } } diff --git a/src/commands/Management/Configuration/manage-command-auto-delete.ts b/src/commands/Management/Configuration/manage-command-auto-delete.ts index 9977c1925bb..9e6d9df4a41 100644 --- a/src/commands/Management/Configuration/manage-command-auto-delete.ts +++ b/src/commands/Management/Configuration/manage-command-auto-delete.ts @@ -15,7 +15,12 @@ import { codeBlock } from '@sapphire/utilities'; detailedDescription: LanguageKeys.Commands.Management.ManageCommandAutoDeleteExtended, permissionLevel: PermissionLevels.Administrator, runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [{ name: 'add' }, { name: 'remove' }, { name: 'reset' }, { name: 'show', default: true }] + subcommands: [ + { name: 'add', messageRun: 'add' }, + { name: 'remove', messageRun: 'remove' }, + { name: 'reset', messageRun: 'reset' }, + { name: 'show', messageRun: 'show', default: true } + ] }) export class UserCommand extends SkyraSubcommand { public async add(message: GuildMessage, args: SkyraSubcommand.Args) { diff --git a/src/commands/Management/Configuration/manage-command-channel.ts b/src/commands/Management/Configuration/manage-command-channel.ts index 0b6787092eb..b0a0bcc8cf6 100644 --- a/src/commands/Management/Configuration/manage-command-channel.ts +++ b/src/commands/Management/Configuration/manage-command-channel.ts @@ -12,7 +12,12 @@ import { send } from '@sapphire/plugin-editable-commands'; detailedDescription: LanguageKeys.Commands.Management.ManageCommandChannelExtended, permissionLevel: PermissionLevels.Administrator, runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [{ name: 'add' }, { name: 'remove' }, { name: 'reset' }, { name: 'show', default: true }] + subcommands: [ + { name: 'add', messageRun: 'add' }, + { name: 'remove', messageRun: 'remove' }, + { name: 'reset', messageRun: 'reset' }, + { name: 'show', messageRun: 'show', default: true } + ] }) export class UserCommand extends SkyraSubcommand { public async add(message: GuildMessage, args: SkyraSubcommand.Args) { diff --git a/src/commands/Management/Configuration/manage-reaction-roles.ts b/src/commands/Management/Configuration/manage-reaction-roles.ts index 2a04f7d0d71..fb81e5bb03f 100644 --- a/src/commands/Management/Configuration/manage-reaction-roles.ts +++ b/src/commands/Management/Configuration/manage-reaction-roles.ts @@ -18,7 +18,12 @@ import { EmbedBuilder, PermissionFlagsBits, type Guild } from 'discord.js'; detailedDescription: LanguageKeys.Commands.Management.ManageReactionRolesExtended, permissionLevel: PermissionLevels.Administrator, runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [{ name: 'add' }, { name: 'remove' }, { name: 'reset' }, { name: 'show', default: true }] + subcommands: [ + { name: 'add', messageRun: 'add' }, + { name: 'remove', messageRun: 'remove' }, + { name: 'reset', messageRun: 'reset' }, + { name: 'show', messageRun: 'show', default: true } + ] }) export class UserCommand extends SkyraSubcommand { public async add(message: GuildMessage, args: SkyraSubcommand.Args) { diff --git a/src/commands/Management/Helpers/guild-info.ts b/src/commands/Management/Helpers/guild-info.ts index da995c10a39..90d4f549342 100644 --- a/src/commands/Management/Helpers/guild-info.ts +++ b/src/commands/Management/Helpers/guild-info.ts @@ -168,7 +168,7 @@ export class UserCommand extends SkyraCommand { ? args.t(LanguageKeys.Commands.Management.GuildInfoChannelsAfkChannelText, { afkChannel: guild.afkChannelId, afkTime: guild.afkTimeout / 60 - }) + }) : `**${args.t(LanguageKeys.Globals.None)}**` }); } diff --git a/src/commands/Management/Helpers/role-info.ts b/src/commands/Management/Helpers/role-info.ts index 87cc580a10b..c6ad32b8f6a 100644 --- a/src/commands/Management/Helpers/role-info.ts +++ b/src/commands/Management/Helpers/role-info.ts @@ -25,10 +25,10 @@ export class UserCommand extends SkyraCommand { const permissionsString = PermissionsBits.has(permissions, PermissionFlagsBits.Administrator) ? args.t(LanguageKeys.Commands.Management.RoleInfoAll) : permissions > 0n - ? PermissionsBits.toArray(permissions) - .map((name) => `+ ${args.t(`permissions:${name}`)}`) - .join('\n') - : args.t(LanguageKeys.Commands.Management.RoleInfoNoPermissions); + ? PermissionsBits.toArray(permissions) + .map((name) => `+ ${args.t(`permissions:${name}`)}`) + .join('\n') + : args.t(LanguageKeys.Commands.Management.RoleInfoNoPermissions); const description = args.t(LanguageKeys.Commands.Management.RoleInfoData, { role, diff --git a/src/commands/Management/Members/stickyRoles.ts b/src/commands/Management/Members/stickyRoles.ts index 33f57e6934e..402ae4592d7 100644 --- a/src/commands/Management/Members/stickyRoles.ts +++ b/src/commands/Management/Members/stickyRoles.ts @@ -13,7 +13,12 @@ import { PermissionFlagsBits } from 'discord.js'; permissionLevel: PermissionLevels.Administrator, requiredClientPermissions: [PermissionFlagsBits.ManageRoles], runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [{ name: 'add' }, { name: 'remove' }, { name: 'reset' }, { name: 'show', default: true }] + subcommands: [ + { name: 'add', messageRun: 'add' }, + { name: 'remove', messageRun: 'remove' }, + { name: 'reset', messageRun: 'reset' }, + { name: 'show', messageRun: 'show', default: true } + ] }) export class UserCommand extends SkyraSubcommand { public async add(message: GuildMessage, args: SkyraSubcommand.Args) { diff --git a/src/commands/Management/Message Filters/filter.ts b/src/commands/Management/Message Filters/filter.ts index 8753fbd0b1f..688ba967c0e 100644 --- a/src/commands/Management/Message Filters/filter.ts +++ b/src/commands/Management/Message Filters/filter.ts @@ -13,7 +13,12 @@ import { remove as removeConfusables } from 'confusables'; detailedDescription: LanguageKeys.Commands.Management.FilterExtended, permissionLevel: PermissionLevels.Administrator, runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [{ name: 'add' }, { name: 'remove' }, { name: 'reset' }, { name: 'show', default: true }] + subcommands: [ + { name: 'add', messageRun: 'add' }, + { name: 'remove', messageRun: 'remove' }, + { name: 'reset', messageRun: 'reset' }, + { name: 'show', messageRun: 'show', default: true } + ] }) export class UserCommand extends SkyraSubcommand { public async add(message: GuildMessage, args: SkyraSubcommand.Args) { diff --git a/src/commands/Management/permissionNodes.ts b/src/commands/Management/permissionNodes.ts index 0100d6fb36d..4dc25d39bfe 100644 --- a/src/commands/Management/permissionNodes.ts +++ b/src/commands/Management/permissionNodes.ts @@ -14,7 +14,12 @@ import { RESTJSONErrorCodes, Role, type GuildMember } from 'discord.js'; permissionLevel: PermissionLevels.Administrator, description: LanguageKeys.Commands.Management.PermissionNodesDescription, detailedDescription: LanguageKeys.Commands.Management.PermissionNodesExtended, - subcommands: [{ name: 'add' }, { name: 'remove' }, { name: 'reset' }, { name: 'show', default: true }], + subcommands: [ + { name: 'add', messageRun: 'add' }, + { name: 'remove', messageRun: 'remove' }, + { name: 'reset', messageRun: 'reset' }, + { name: 'show', messageRun: 'show', default: true } + ], runIn: [CommandOptionsRunTypeEnum.GuildAny] }) export class UserCommand extends SkyraSubcommand { diff --git a/src/commands/Moderation/Management/history.ts b/src/commands/Moderation/Management/history.ts index ef7336cd1f5..fbaea834701 100644 --- a/src/commands/Moderation/Management/history.ts +++ b/src/commands/Moderation/Management/history.ts @@ -21,7 +21,10 @@ const COLORS = [0x80f31f, 0xa5de0b, 0xc7c101, 0xe39e03, 0xf6780f, 0xfe5326, 0xfb detailedDescription: LanguageKeys.Commands.Moderation.HistoryExtended, permissionLevel: PermissionLevels.Moderator, runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [{ name: 'details' }, { name: 'overview', default: true }] + subcommands: [ + { name: 'details', messageRun: 'details' }, + { name: 'overview', messageRun: 'overview', default: true } + ] }) export class UserCommand extends SkyraSubcommand { public override messageRun(message: GuildMessage, args: SkyraSubcommand.Args, context: SkyraSubcommand.RunContext) { diff --git a/src/commands/Moderation/Management/moderations.ts b/src/commands/Moderation/Management/moderations.ts index 62dbd540b01..2d0af830908 100644 --- a/src/commands/Moderation/Management/moderations.ts +++ b/src/commands/Moderation/Management/moderations.ts @@ -27,12 +27,12 @@ const enum Type { runIn: [CommandOptionsRunTypeEnum.GuildAny], subcommands: [ { name: 'mute', messageRun: 'mutes' }, - { name: 'mutes' }, + { name: 'mutes', messageRun: 'mutes' }, { name: 'warning', messageRun: 'warnings' }, - { name: 'warnings' }, + { name: 'warnings', messageRun: 'warnings' }, { name: 'warn', messageRun: 'warnings' }, { name: 'warns', messageRun: 'warnings' }, - { name: 'all', default: true } + { name: 'all', messageRun: 'all', default: true } ] }) ) diff --git a/src/commands/Moderation/Utilities/case.ts b/src/commands/Moderation/Utilities/case.ts index 88143b934e3..e8d56bdf7f8 100644 --- a/src/commands/Moderation/Utilities/case.ts +++ b/src/commands/Moderation/Utilities/case.ts @@ -13,7 +13,10 @@ import { PermissionFlagsBits } from 'discord.js'; permissionLevel: PermissionLevels.Moderator, requiredClientPermissions: [PermissionFlagsBits.EmbedLinks], runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [{ name: 'delete' }, { name: 'show', default: true }] + subcommands: [ + { name: 'delete', messageRun: 'delete' }, + { name: 'show', messageRun: 'show', default: true } + ] }) export class UserCommand extends SkyraSubcommand { public async show(message: GuildMessage, args: SkyraSubcommand.Args) { diff --git a/src/commands/Moderation/lockdown.ts b/src/commands/Moderation/lockdown.ts index 40082d78241..f78b78d9da4 100644 --- a/src/commands/Moderation/lockdown.ts +++ b/src/commands/Moderation/lockdown.ts @@ -18,7 +18,11 @@ import { PermissionFlagsBits, type Role } from 'discord.js'; permissionLevel: PermissionLevels.Moderator, requiredClientPermissions: [PermissionFlagsBits.ManageChannels, PermissionFlagsBits.ManageRoles], runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [{ name: 'lock' }, { name: 'unlock' }, { name: 'auto', default: true }] + subcommands: [ + { name: 'lock', messageRun: 'lock' }, + { name: 'unlock', messageRun: 'unlock' }, + { name: 'auto', messageRun: 'auto', default: true } + ] }) export class UserCommand extends SkyraSubcommand { public override messageRun(message: GuildMessage, args: SkyraSubcommand.Args, context: SkyraSubcommand.RunContext) { diff --git a/src/commands/System/Admin/eval.ts b/src/commands/System/Admin/eval.ts index e61d06bfd41..6e615473d76 100644 --- a/src/commands/System/Admin/eval.ts +++ b/src/commands/System/Admin/eval.ts @@ -171,11 +171,11 @@ export class UserCommand extends SkyraCommand { result instanceof Error ? result.stack : args.getFlags('json') - ? JSON.stringify(result, null, 4) - : inspect(result, { - depth: Number(args.getOption('depth') ?? 0) || 0, - showHidden: args.getFlags('showHidden', 'hidden') - }); + ? JSON.stringify(result, null, 4) + : inspect(result, { + depth: Number(args.getOption('depth') ?? 0) || 0, + showHidden: args.getFlags('showHidden', 'hidden') + }); } return { success, diff --git a/src/commands/Twitch/twitchsubscription.ts b/src/commands/Twitch/twitchsubscription.ts index d93c488abf1..70cfb16ff62 100644 --- a/src/commands/Twitch/twitchsubscription.ts +++ b/src/commands/Twitch/twitchsubscription.ts @@ -20,7 +20,12 @@ import { EmbedBuilder, PermissionFlagsBits, type Guild } from 'discord.js'; permissionLevel: PermissionLevels.Administrator, requiredClientPermissions: [PermissionFlagsBits.EmbedLinks], runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [{ name: 'add' }, { name: 'remove' }, { name: 'reset' }, { name: 'show', default: true }] + subcommands: [ + { name: 'add', messageRun: 'add' }, + { name: 'remove', messageRun: 'remove' }, + { name: 'reset', messageRun: 'reset' }, + { name: 'show', messageRun: 'show', default: true } + ] }) export class UserCommand extends SkyraSubcommand { public async add(message: GuildMessage, args: SkyraSubcommand.Args) { diff --git a/src/lib/api/utils.ts b/src/lib/api/utils.ts index 86723b3a396..282516433a4 100644 --- a/src/lib/api/utils.ts +++ b/src/lib/api/utils.ts @@ -119,7 +119,7 @@ async function transformGuild(client: Client, userId: string, data: RESTAPIParti vanityURLCode: null, verificationLevel: GuildVerificationLevel.None, verified: false - } + } : flattenGuild(guild); return { diff --git a/src/lib/database/database.config.ts b/src/lib/database/database.config.ts index f278f60877c..ee3635d422f 100644 --- a/src/lib/database/database.config.ts +++ b/src/lib/database/database.config.ts @@ -1,12 +1,13 @@ // Config must be the first to be loaded, as it sets the env: import '#root/config'; + // Import everything else: import { envParseBoolean, envParseInteger, envParseString } from '@skyra/env-utilities'; import { fileURLToPath } from 'node:url'; -import { DataSource, type DataSourceOptions } from 'typeorm'; +import { DataSource } from 'typeorm'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; -export const config: DataSourceOptions = { +export const AppDataConfig = new DataSource({ type: 'postgres', host: envParseString('PGSQL_DATABASE_HOST'), port: envParseInteger('PGSQL_DATABASE_PORT'), @@ -17,6 +18,6 @@ export const config: DataSourceOptions = { migrations: [fileURLToPath(new URL('migrations/*.js', import.meta.url))], namingStrategy: new SnakeNamingStrategy(), logging: envParseBoolean('TYPEORM_DEBUG_LOGS', false) -}; +}); -export const connect = () => new DataSource(config).initialize(); +export const connect = () => AppDataConfig.initialize(); diff --git a/src/lib/database/entities/GuildSubscriptionEntity.ts b/src/lib/database/entities/GuildSubscriptionEntity.ts index 362bf311f3b..8296ca928e4 100644 --- a/src/lib/database/entities/GuildSubscriptionEntity.ts +++ b/src/lib/database/entities/GuildSubscriptionEntity.ts @@ -1,5 +1,5 @@ import { TwitchSubscriptionEntity } from '#lib/database/entities/TwitchSubscriptionEntity'; -import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; +import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, type Relation } from 'typeorm'; @Entity('guild_subscription', { schema: 'public' }) export class GuildSubscriptionEntity extends BaseEntity { @@ -8,7 +8,7 @@ export class GuildSubscriptionEntity extends BaseEntity { @ManyToOne(() => TwitchSubscriptionEntity, (twitchSubscription) => twitchSubscription.id, { cascade: true, eager: true }) @JoinColumn() - public subscription!: TwitchSubscriptionEntity; + public subscription!: Relation; @PrimaryColumn('varchar', { length: 19 }) public channelId!: string; diff --git a/src/lib/database/entities/TwitchSubscriptionEntity.ts b/src/lib/database/entities/TwitchSubscriptionEntity.ts index ca90407f006..403f7b41256 100644 --- a/src/lib/database/entities/TwitchSubscriptionEntity.ts +++ b/src/lib/database/entities/TwitchSubscriptionEntity.ts @@ -1,6 +1,6 @@ import { GuildSubscriptionEntity } from '#lib/database/entities/GuildSubscriptionEntity'; import { TwitchEventSubTypes } from '#lib/types'; -import { BaseEntity, Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { BaseEntity, Column, Entity, Index, OneToMany, PrimaryGeneratedColumn, type Relation } from 'typeorm'; @Index(['streamerId', 'subscriptionType'], { unique: true }) @Entity('twitch_subscriptions', { schema: 'public' }) @@ -18,5 +18,5 @@ export class TwitchSubscriptionEntity extends BaseEntity { public subscriptionType!: TwitchEventSubTypes; @OneToMany(() => GuildSubscriptionEntity, (guildSubscription) => guildSubscription.subscription) - public guildSubscription!: GuildSubscriptionEntity[]; + public guildSubscription!: Relation[]; } diff --git a/src/lib/games/base/BaseReactionController.ts b/src/lib/games/base/BaseReactionController.ts index 43400f158e9..d3a3d51dbf3 100644 --- a/src/lib/games/base/BaseReactionController.ts +++ b/src/lib/games/base/BaseReactionController.ts @@ -3,7 +3,7 @@ import { BaseController } from '#lib/games/base/BaseController'; import type { BaseReactionGame } from '#lib/games/base/BaseReactionGame'; import { Events } from '#lib/types'; import type { LLRCData } from '#utils/LongLivingReactionCollector'; -import { getEmojiString } from '#utils/functions'; +import { getEmojiString, type SerializedEmoji } from '#utils/functions'; import { cast } from '#utils/util'; import { container } from '@sapphire/framework'; import { DiscordAPIError, RESTJSONErrorCodes } from 'discord.js'; @@ -24,9 +24,7 @@ export abstract class BaseReactionController extends BaseController { if (data.userId !== this.userId) return; if (data.messageId !== game.message.id) return; - const emoji = this.resolveCollectedData(data); - if (!emoji) return; - + const emoji = getEmojiString(data.emoji); if (game.reactions.includes(emoji) && this.resolveCollectedValidity(emoji)) { void this.removeEmoji(data, emoji, this.userId); game.listener.setListener(null); @@ -43,13 +41,9 @@ export abstract class BaseReactionController extends BaseController { protected abstract resolveCollectedValidity(collected: string): boolean; - protected resolveCollectedData(reaction: LLRCData): string | null { - return getEmojiString(reaction.emoji); - } - - protected async removeEmoji(reaction: LLRCData, emoji: string, userId: string): Promise { + protected async removeEmoji(reaction: LLRCData, emoji: SerializedEmoji, userId: string): Promise { try { - await api().channels.deleteUserMessageReaction(reaction.channel.id, reaction.messageId, emoji, userId); + await api().channels.deleteUserMessageReaction(reaction.channel.id, reaction.messageId, decodeURIComponent(emoji), userId); } catch (error) { if (error instanceof DiscordAPIError) { if (error.code === RESTJSONErrorCodes.UnknownMessage || error.code === RESTJSONErrorCodes.UnknownEmoji) return; diff --git a/src/lib/games/connect-four/ConnectFourGame.ts b/src/lib/games/connect-four/ConnectFourGame.ts index c8d88abe286..79a134668fc 100644 --- a/src/lib/games/connect-four/ConnectFourGame.ts +++ b/src/lib/games/connect-four/ConnectFourGame.ts @@ -57,7 +57,7 @@ export class ConnectFourGame extends BaseReactionGame { : this.t(LanguageKeys.Commands.Games.TicTacToeWinner, { winner: this.player.name, board: this.renderBoard() - }); + }); } protected renderOnUpdateOrStart(): string { @@ -186,7 +186,7 @@ export class ConnectFourGame extends BaseReactionGame { [x, y + 1], [x, y + 2], [x, y + 3] - ] as [number, number][]) + ] as [number, number][]) : null; } diff --git a/src/lib/games/tic-tac-toe/TicTacToeGame.ts b/src/lib/games/tic-tac-toe/TicTacToeGame.ts index c7fb697b429..c199cb58d50 100644 --- a/src/lib/games/tic-tac-toe/TicTacToeGame.ts +++ b/src/lib/games/tic-tac-toe/TicTacToeGame.ts @@ -32,7 +32,7 @@ export class TicTacToeGame extends BaseReactionGame { : this.t(LanguageKeys.Commands.Games.TicTacToeWinner, { winner: this.player.name, board: this.renderBoard() - }); + }); } protected renderOnUpdateOrStart(): string { diff --git a/src/lib/moderation/managers/ModerationManager.ts b/src/lib/moderation/managers/ModerationManager.ts index e49d5239f5b..6c188b18dc2 100644 --- a/src/lib/moderation/managers/ModerationManager.ts +++ b/src/lib/moderation/managers/ModerationManager.ts @@ -85,8 +85,8 @@ export class ModerationManager extends Collection { ? curr : prev : curr.createdTimestamp > prev.createdTimestamp - ? curr - : prev + ? curr + : prev : prev, null ); diff --git a/src/lib/util/Security/ModerationActions.ts b/src/lib/util/Security/ModerationActions.ts index 45ea5f0eed5..2c34ab09894 100644 --- a/src/lib/util/Security/ModerationActions.ts +++ b/src/lib/util/Security/ModerationActions.ts @@ -261,13 +261,13 @@ export class ModerationActions { this.guild, nickname ? LanguageKeys.Commands.Moderation.ActionSetNicknameSet : LanguageKeys.Commands.Moderation.ActionSetNicknameRemoved, { reason: moderationLog.reason } - ) + ) : resolveKey( this.guild, nickname ? LanguageKeys.Commands.Moderation.ActionSetNicknameNoReasonSet : LanguageKeys.Commands.Moderation.ActionSetNicknameNoReasonRemoved - )); + )); await api().guilds.editMember(this.guild.id, rawOptions.userId, { nick: nickname }, { reason }); await this.cancelLastLogTaskFromUser(options.userId, TypeCodes.SetNickname); @@ -662,8 +662,8 @@ export class ModerationActions { ? LanguageKeys.Commands.Moderation.ModerationDmDescriptionWithReasonWithDuration : LanguageKeys.Commands.Moderation.ModerationDmDescriptionWithReason : entry.duration - ? LanguageKeys.Commands.Moderation.ModerationDmDescriptionWithDuration - : LanguageKeys.Commands.Moderation.ModerationDmDescription; + ? LanguageKeys.Commands.Moderation.ModerationDmDescriptionWithDuration + : LanguageKeys.Commands.Moderation.ModerationDmDescription; const t = await fetchT(this.guild); const description = t(descriptionKey, { diff --git a/src/lib/util/Security/RegexCreator.ts b/src/lib/util/Security/RegexCreator.ts index 62268bab1c8..58a60d77ce6 100644 --- a/src/lib/util/Security/RegexCreator.ts +++ b/src/lib/util/Security/RegexCreator.ts @@ -54,17 +54,17 @@ export function processWordBoundaries(word: string) { return starts ? // Starts and end? - ends + ends ? // Starts and ends - WordBoundary.Both + WordBoundary.Both : // Only starts - WordBoundary.Start + WordBoundary.Start : // Ends? - ends - ? // Ends with wildcard - WordBoundary.End - : // Does not have wildcards - WordBoundary.None; + ends + ? // Ends with wildcard + WordBoundary.End + : // Does not have wildcards + WordBoundary.None; } export function processWordPatternsWithGroups(word: string) { @@ -81,13 +81,13 @@ export function processGroup(group: string) { onMatch: (match) => match[1] === match[2] ? // and a === - - match[1] === '-' + match[1] === '-' ? // then optimize to - - '\\-' + '\\-' : // else optimize to a- - `${processLetter(match[1])}\\-` + `${processLetter(match[1])}\\-` : // otherwise a-b - `${processLetter(match[1])}-${processLetter(match[2])}`, + `${processLetter(match[1])}-${processLetter(match[2])}`, outMatch: (match) => [...match].map(processLetter).join('') }); diff --git a/src/listeners/commands/_shared.ts b/src/listeners/commands/_shared.ts new file mode 100644 index 00000000000..db1bcec9cba --- /dev/null +++ b/src/listeners/commands/_shared.ts @@ -0,0 +1,193 @@ +import { LanguageKeys } from '#lib/i18n/languageKeys'; +import { fetchT, translate } from '#lib/i18n/translate'; +import type { SkyraArgs } from '#lib/structures'; +import { OWNERS } from '#root/config'; +import { Colors, ZeroWidthSpace, rootFolder } from '#utils/constants'; +import { sendTemporaryMessage } from '#utils/functions'; +import { EmbedBuilder } from '@discordjs/builders'; +import { ArgumentError, Command, Events, UserError, container, type MessageCommandErrorPayload } from '@sapphire/framework'; +import type { TFunction } from '@sapphire/plugin-i18next'; +import type { MessageSubcommandErrorPayload } from '@sapphire/plugin-subcommands'; +import { codeBlock, cutText, type NonNullObject } from '@sapphire/utilities'; +import { captureException } from '@sentry/node'; +import { envIsDefined } from '@skyra/env-utilities'; +import { DiscordAPIError, HTTPError, RESTJSONErrorCodes, Routes, type Message } from 'discord.js'; + +const ignoredCodes = [RESTJSONErrorCodes.UnknownChannel, RESTJSONErrorCodes.UnknownMessage]; + +export async function handleCommandError(error: unknown, payload: MessageCommandErrorPayload | MessageSubcommandErrorPayload) { + const { message, command } = payload; + let t: TFunction; + let parameters: string; + if ('args' in payload) { + t = (payload.args as SkyraArgs).t; + parameters = payload.parameters; + } else { + t = await fetchT({ guild: message.guild, channel: message.channel, user: message.author }); + parameters = message.content.slice(payload.context.commandPrefix.length + payload.context.commandName.length).trim(); + } + + // If the error was a string or an UserError, send it to the user: + if (!(error instanceof Error)) return stringError(message, t, String(error)); + if (error instanceof ArgumentError) return argumentError(message, t, error); + if (error instanceof UserError) return userError(message, t, error); + + const { client, logger } = container; + // If the error was an AbortError or an Internal Server Error, tell the user to re-try: + if (error.name === 'AbortError' || error.message === 'Internal Server Error') { + logger.warn(`${getWarnError(message)} (${message.author.id}) | ${error.constructor.name}`); + return sendTemporaryMessage(message, t(LanguageKeys.System.DiscordAbortError)); + } + + // Extract useful information about the DiscordAPIError + if (error instanceof DiscordAPIError) { + if (isSilencedError(message, error)) return; + client.emit(Events.Error, error); + } else { + logger.warn(`${getWarnError(message)} (${message.author.id}) | ${error.constructor.name}`); + } + + // Send a detailed message: + await sendErrorChannel(message, command, parameters, error); + + // Emit where the error was emitted + logger.fatal(`[COMMAND] ${command.location.full}\n${error.stack || error.message}`); + try { + await sendTemporaryMessage(message, generateUnexpectedErrorMessage(message, command, t, error)); + } catch (err) { + client.emit(Events.Error, err); + } + + return undefined; +} + +function isSilencedError(message: Message, error: DiscordAPIError) { + return ( + // If it's an unknown channel or an unknown message, ignore: + ignoredCodes.includes(error.code as number) || + // If it's a DM message reply after a block, ignore: + isDirectMessageReplyAfterBlock(message, error) + ); +} + +function isDirectMessageReplyAfterBlock(message: Message, error: DiscordAPIError) { + // When sending a message to a user who has blocked the bot, Discord replies with 50007 "Cannot send messages to this user": + if (error.code !== RESTJSONErrorCodes.CannotSendMessagesToThisUser) return false; + + // If it's not a Direct Message, return false: + if (message.guild !== null) return false; + + // If the query was made to the message's channel, then it was a DM response: + return error.url === Routes.channelMessages(message.channel.id); +} + +const sentry = envIsDefined('SENTRY_URL'); +function generateUnexpectedErrorMessage(message: Message, command: Command, t: TFunction, error: Error) { + if (OWNERS.includes(message.author.id)) return codeBlock('js', error.stack!); + if (!sentry) return t(LanguageKeys.Events.Errors.UnexpectedError); + + try { + const report = captureException(error, { tags: { command: command.name } }); + return t(LanguageKeys.Events.Errors.UnexpectedErrorWithContext, { report }); + } catch (error) { + container.client.emit(Events.Error, error); + return t(LanguageKeys.Events.Errors.UnexpectedError); + } +} + +function stringError(message: Message, t: TFunction, error: string) { + return alert(message, t(LanguageKeys.Events.Errors.String, { mention: message.author.toString(), message: error })); +} + +function argumentError(message: Message, t: TFunction, error: ArgumentError) { + const argument = error.argument.name; + const identifier = translate(error.identifier); + const parameter = error.parameter.replaceAll('`', '῾'); + return alert(message, t(identifier, { ...error, ...(error.context as NonNullObject), argument, parameter: cutText(parameter, 50) })); +} + +function userError(message: Message, t: TFunction, error: UserError) { + // `context: { silent: true }` should make UserError silent: + // Use cases for this are for example permissions error when running the `eval` command. + if (Reflect.get(Object(error.context), 'silent')) return; + + const identifier = translate(error.identifier); + const content = t(identifier, error.context as any) as string; + return alert(message, content); +} + +function alert(message: Message, content: string) { + return sendTemporaryMessage(message, { content, allowedMentions: { users: [message.author.id], roles: [] } }); +} + +async function sendErrorChannel(message: Message, command: Command, parameters: string, error: Error) { + const webhook = container.client.webhookError; + if (webhook === null) return; + + const lines = [getLinkLine(message.url), getCommandLine(command), getArgumentsLine(parameters), getErrorLine(error)]; + + // If it's a DiscordAPIError or a HTTPError, add the HTTP path and code lines after the second one. + if (error instanceof DiscordAPIError || error instanceof HTTPError) { + lines.splice(2, 0, getPathLine(error), getCodeLine(error)); + } + + const embed = new EmbedBuilder().setDescription(lines.join('\n')).setColor(Colors.Red).setTimestamp(); + try { + await webhook.send({ embeds: [embed] }); + } catch (err) { + container.client.emit(Events.Error, err); + } +} + +/** + * Formats a message url line. + * @param url The url to format. + */ +function getLinkLine(url: string): string { + return `[**Jump to Message!**](${url})`; +} + +/** + * Formats a command line. + * @param command The command to format. + */ +function getCommandLine(command: Command): string { + return `**Command**: ${command.location.full.slice(rootFolder.length)}`; +} + +/** + * Formats an error path line. + * @param error The error to format. + */ +function getPathLine(error: DiscordAPIError | HTTPError): string { + return `**Path**: ${error.method.toUpperCase()} ${error.url}`; +} + +/** + * Formats an error code line. + * @param error The error to format. + */ +function getCodeLine(error: DiscordAPIError | HTTPError): string { + return `**Code**: ${'code' in error ? error.code : error.status}`; +} + +/** + * Formats an arguments line. + * @param parameters The arguments the user used when running the command. + */ +function getArgumentsLine(parameters: string): string { + if (parameters.length === 0) return '**Parameters**: Not Supplied'; + return `**Parameters**: [\`${parameters.trim().replaceAll('`', '῾') || ZeroWidthSpace}\`]`; +} + +/** + * Formats an error codeblock. + * @param error The error to format. + */ +function getErrorLine(error: Error): string { + return `**Error**: ${codeBlock('js', error.stack || error.toString())}`; +} + +function getWarnError(message: Message) { + return `ERROR: /${message.guild ? `${message.guild.id}/${message.channel.id}` : `DM/${message.author.id}`}/${message.id}`; +} diff --git a/src/listeners/commands/messageCommandError.ts b/src/listeners/commands/messageCommandError.ts index 456c89c734a..c1f8676ab42 100644 --- a/src/listeners/commands/messageCommandError.ts +++ b/src/listeners/commands/messageCommandError.ts @@ -1,186 +1,8 @@ -import { LanguageKeys } from '#lib/i18n/languageKeys'; -import { translate } from '#lib/i18n/translate'; -import type { SkyraArgs } from '#lib/structures'; -import { OWNERS } from '#root/config'; -import { Colors, ZeroWidthSpace, rootFolder } from '#utils/constants'; -import { sendTemporaryMessage } from '#utils/functions'; -import { EmbedBuilder } from '@discordjs/builders'; -import { Args, ArgumentError, Command, Events, Listener, UserError, type MessageCommandErrorPayload } from '@sapphire/framework'; -import type { TFunction } from '@sapphire/plugin-i18next'; -import { codeBlock, cutText, type NonNullObject } from '@sapphire/utilities'; -import { captureException } from '@sentry/node'; -import { envIsDefined } from '@skyra/env-utilities'; -import { DiscordAPIError, HTTPError, RESTJSONErrorCodes, Routes, type Message } from 'discord.js'; - -const ignoredCodes = [RESTJSONErrorCodes.UnknownChannel, RESTJSONErrorCodes.UnknownMessage]; +import { Events, Listener, type MessageCommandErrorPayload } from '@sapphire/framework'; +import { handleCommandError } from './_shared.js'; export class UserListener extends Listener { - private readonly sentry = envIsDefined('SENTRY_URL'); - - public async run(error: Error, { message, command, parameters, args: unknownArgs }: MessageCommandErrorPayload) { - const args = unknownArgs as SkyraArgs; - - // If the error was a string or an UserError, send it to the user: - if (typeof error === 'string') return this.stringError(message, args.t, error); - if (error instanceof ArgumentError) return this.argumentError(message, args.t, error); - if (error instanceof UserError) return this.userError(message, args.t, error); - - const { client, logger } = this.container; - // If the error was an AbortError or an Internal Server Error, tell the user to re-try: - if (error.name === 'AbortError' || error.message === 'Internal Server Error') { - logger.warn(`${this.getWarnError(message)} (${message.author.id}) | ${error.constructor.name}`); - return sendTemporaryMessage(message, args.t(LanguageKeys.System.DiscordAbortError)); - } - - // Extract useful information about the DiscordAPIError - if (error instanceof DiscordAPIError) { - if (this.isSilencedError(args, error)) return; - client.emit(Events.Error, error); - } else { - logger.warn(`${this.getWarnError(message)} (${message.author.id}) | ${error.constructor.name}`); - } - - // Send a detailed message: - await this.sendErrorChannel(message, command, parameters, error); - - // Emit where the error was emitted - logger.fatal(`[COMMAND] ${command.location.full}\n${error.stack || error.message}`); - try { - await sendTemporaryMessage(message, this.generateUnexpectedErrorMessage(args, error)); - } catch (err) { - client.emit(Events.Error, err); - } - - return undefined; - } - - private isSilencedError(args: Args, error: DiscordAPIError) { - return ( - // If it's an unknown channel or an unknown message, ignore: - ignoredCodes.includes(error.code as number) || - // If it's a DM message reply after a block, ignore: - this.isDirectMessageReplyAfterBlock(args, error) - ); - } - - private isDirectMessageReplyAfterBlock(args: Args, error: DiscordAPIError) { - // When sending a message to a user who has blocked the bot, Discord replies with 50007 "Cannot send messages to this user": - if (error.code !== RESTJSONErrorCodes.CannotSendMessagesToThisUser) return false; - - // If it's not a Direct Message, return false: - if (args.message.guild !== null) return false; - - // If the query was made to the message's channel, then it was a DM response: - return error.url === Routes.channelMessages(args.message.channel.id); - } - - private generateUnexpectedErrorMessage(args: Args, error: Error) { - if (OWNERS.includes(args.message.author.id)) return codeBlock('js', error.stack!); - if (!this.sentry) return args.t(LanguageKeys.Events.Errors.UnexpectedError); - - try { - const report = captureException(error, { tags: { command: args.command.name } }); - return args.t(LanguageKeys.Events.Errors.UnexpectedErrorWithContext, { report }); - } catch (error) { - this.container.client.emit(Events.Error, error); - return args.t(LanguageKeys.Events.Errors.UnexpectedError); - } - } - - private stringError(message: Message, t: TFunction, error: string) { - return this.alert(message, t(LanguageKeys.Events.Errors.String, { mention: message.author.toString(), message: error })); - } - - private argumentError(message: Message, t: TFunction, error: ArgumentError) { - const argument = error.argument.name; - const identifier = translate(error.identifier); - const parameter = error.parameter.replaceAll('`', '῾'); - return this.alert(message, t(identifier, { ...error, ...(error.context as NonNullObject), argument, parameter: cutText(parameter, 50) })); - } - - private userError(message: Message, t: TFunction, error: UserError) { - // `context: { silent: true }` should make UserError silent: - // Use cases for this are for example permissions error when running the `eval` command. - if (Reflect.get(Object(error.context), 'silent')) return; - - const identifier = translate(error.identifier); - const content = t(identifier, error.context as any) as string; - return this.alert(message, content); - } - - private alert(message: Message, content: string) { - return sendTemporaryMessage(message, { content, allowedMentions: { users: [message.author.id], roles: [] } }); - } - - private async sendErrorChannel(message: Message, command: Command, parameters: string, error: Error) { - const webhook = this.container.client.webhookError; - if (webhook === null) return; - - const lines = [this.getLinkLine(message.url), this.getCommandLine(command), this.getArgumentsLine(parameters), this.getErrorLine(error)]; - - // If it's a DiscordAPIError or a HTTPError, add the HTTP path and code lines after the second one. - if (error instanceof DiscordAPIError || error instanceof HTTPError) { - lines.splice(2, 0, this.getPathLine(error), this.getCodeLine(error)); - } - - const embed = new EmbedBuilder().setDescription(lines.join('\n')).setColor(Colors.Red).setTimestamp(); - try { - await webhook.send({ embeds: [embed] }); - } catch (err) { - this.container.client.emit(Events.Error, err); - } - } - - /** - * Formats a message url line. - * @param url The url to format. - */ - private getLinkLine(url: string): string { - return `[**Jump to Message!**](${url})`; - } - - /** - * Formats a command line. - * @param command The command to format. - */ - private getCommandLine(command: Command): string { - return `**Command**: ${command.location.full.slice(rootFolder.length)}`; - } - - /** - * Formats an error path line. - * @param error The error to format. - */ - private getPathLine(error: DiscordAPIError | HTTPError): string { - return `**Path**: ${error.method.toUpperCase()} ${error.url}`; - } - - /** - * Formats an error code line. - * @param error The error to format. - */ - private getCodeLine(error: DiscordAPIError | HTTPError): string { - return `**Code**: ${'code' in error ? error.code : error.status}`; - } - - /** - * Formats an arguments line. - * @param parameters The arguments the user used when running the command. - */ - private getArgumentsLine(parameters: string): string { - if (parameters.length === 0) return '**Parameters**: Not Supplied'; - return `**Parameters**: [\`${parameters.trim().replaceAll('`', '῾') || ZeroWidthSpace}\`]`; - } - - /** - * Formats an error codeblock. - * @param error The error to format. - */ - private getErrorLine(error: Error): string { - return `**Error**: ${codeBlock('js', error.stack || error.toString())}`; - } - - private getWarnError(message: Message) { - return `ERROR: /${message.guild ? `${message.guild.id}/${message.channel.id}` : `DM/${message.author.id}`}/${message.id}`; + public run(error: Error, payload: MessageCommandErrorPayload) { + return handleCommandError(error, payload); } } diff --git a/src/listeners/commands/messageSubcommandError.ts b/src/listeners/commands/messageSubcommandError.ts new file mode 100644 index 00000000000..77bef5ff88a --- /dev/null +++ b/src/listeners/commands/messageSubcommandError.ts @@ -0,0 +1,9 @@ +import { Listener } from '@sapphire/framework'; +import type { MessageSubcommandErrorPayload, SubcommandPluginEvents } from '@sapphire/plugin-subcommands'; +import { handleCommandError } from './_shared.js'; + +export class UserListener extends Listener { + public run(error: unknown, payload: MessageSubcommandErrorPayload) { + return handleCommandError(error, payload); + } +} diff --git a/src/listeners/guilds/members/rawMemberRemoveNotify.ts b/src/listeners/guilds/members/rawMemberRemoveNotify.ts index 37d8a0a5a7d..e7306d4acc3 100644 --- a/src/listeners/guilds/members/rawMemberRemoveNotify.ts +++ b/src/listeners/guilds/members/rawMemberRemoveNotify.ts @@ -23,10 +23,10 @@ export class UserListener extends Listener { const footer = isModerationAction.kicked ? t(LanguageKeys.Events.Guilds.Members.GuildMemberKicked) : isModerationAction.banned - ? t(LanguageKeys.Events.Guilds.Members.GuildMemberBanned) - : isModerationAction.softbanned - ? t(LanguageKeys.Events.Guilds.Members.GuildMemberSoftBanned) - : t(LanguageKeys.Events.Guilds.Members.GuildMemberRemove); + ? t(LanguageKeys.Events.Guilds.Members.GuildMemberBanned) + : isModerationAction.softbanned + ? t(LanguageKeys.Events.Guilds.Members.GuildMemberSoftBanned) + : t(LanguageKeys.Events.Guilds.Members.GuildMemberRemove); const time = this.processJoinedTimestamp(member); this.container.client.emit(Events.GuildMessageLog, guild, logChannelId, key, () =>