From 09eb8e92f2aced04e2ddff50f70c66512c877549 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 22 May 2025 22:35:48 +0000 Subject: [PATCH] feat: update to djs 14.19.3, node 22, zod 4 --- .nvmrc | 2 +- backend/package.json | 8 +- backend/src/RegExpRunner.ts | 2 +- backend/src/SimpleCache.ts | 2 +- backend/src/api/docs.ts | 71 ++-- backend/src/api/guilds/importExport.ts | 2 +- backend/src/configValidator.ts | 2 +- backend/src/env.ts | 2 +- backend/src/exportSchemas.ts | 88 ++++- backend/src/index.ts | 6 +- ...12501-MigrateUsernamesToNewHistoryTable.ts | 2 +- backend/src/pluginUtils.ts | 144 ++++++-- backend/src/plugins/AutoDelete/types.ts | 2 +- .../plugins/AutoDelete/util/deleteNextItem.ts | 4 +- backend/src/plugins/AutoReactions/types.ts | 2 +- .../src/plugins/Automod/actions/addRoles.ts | 2 +- .../plugins/Automod/actions/addToCounter.ts | 2 +- backend/src/plugins/Automod/actions/alert.ts | 2 +- .../plugins/Automod/actions/archiveThread.ts | 2 +- backend/src/plugins/Automod/actions/ban.ts | 2 +- .../plugins/Automod/actions/changeNickname.ts | 2 +- .../plugins/Automod/actions/changePerms.ts | 2 +- backend/src/plugins/Automod/actions/clean.ts | 2 +- .../plugins/Automod/actions/exampleAction.ts | 2 +- backend/src/plugins/Automod/actions/kick.ts | 2 +- backend/src/plugins/Automod/actions/log.ts | 2 +- backend/src/plugins/Automod/actions/mute.ts | 2 +- .../plugins/Automod/actions/pauseInvites.ts | 2 +- .../plugins/Automod/actions/removeRoles.ts | 2 +- backend/src/plugins/Automod/actions/reply.ts | 2 +- .../src/plugins/Automod/actions/setCounter.ts | 2 +- .../plugins/Automod/actions/setSlowmode.ts | 2 +- .../plugins/Automod/actions/startThread.ts | 2 +- backend/src/plugins/Automod/actions/warn.ts | 2 +- backend/src/plugins/Automod/constants.ts | 2 +- .../functions/createMessageSpamTrigger.ts | 2 +- backend/src/plugins/Automod/helpers.ts | 2 +- .../plugins/Automod/triggers/antiraidLevel.ts | 2 +- .../plugins/Automod/triggers/anyMessage.ts | 2 +- backend/src/plugins/Automod/triggers/ban.ts | 2 +- .../Automod/triggers/counterTrigger.ts | 2 +- .../Automod/triggers/exampleTrigger.ts | 2 +- backend/src/plugins/Automod/triggers/kick.ts | 2 +- .../Automod/triggers/matchAttachmentType.ts | 2 +- .../plugins/Automod/triggers/matchInvites.ts | 2 +- .../plugins/Automod/triggers/matchLinks.ts | 2 +- .../plugins/Automod/triggers/matchMimeType.ts | 2 +- .../plugins/Automod/triggers/matchRegex.ts | 2 +- .../plugins/Automod/triggers/matchWords.ts | 2 +- .../plugins/Automod/triggers/memberJoin.ts | 2 +- .../Automod/triggers/memberJoinSpam.ts | 2 +- .../plugins/Automod/triggers/memberLeave.ts | 2 +- backend/src/plugins/Automod/triggers/mute.ts | 2 +- backend/src/plugins/Automod/triggers/note.ts | 2 +- .../src/plugins/Automod/triggers/roleAdded.ts | 2 +- .../plugins/Automod/triggers/roleRemoved.ts | 2 +- .../plugins/Automod/triggers/threadArchive.ts | 2 +- .../plugins/Automod/triggers/threadCreate.ts | 2 +- .../Automod/triggers/threadCreateSpam.ts | 2 +- .../plugins/Automod/triggers/threadDelete.ts | 2 +- .../Automod/triggers/threadUnarchive.ts | 2 +- backend/src/plugins/Automod/triggers/unban.ts | 2 +- .../src/plugins/Automod/triggers/unmute.ts | 2 +- backend/src/plugins/Automod/triggers/warn.ts | 2 +- backend/src/plugins/Automod/types.ts | 2 +- backend/src/plugins/BotControl/types.ts | 2 +- .../plugins/Cases/functions/getCaseEmbed.ts | 4 +- backend/src/plugins/Cases/types.ts | 2 +- backend/src/plugins/Censor/types.ts | 2 +- .../ChannelArchiver/ChannelArchiverPlugin.ts | 2 +- backend/src/plugins/Common/CommonPlugin.ts | 101 ++--- backend/src/plugins/Common/types.ts | 2 +- .../src/plugins/CompanionChannels/types.ts | 2 +- .../src/plugins/ContextMenus/actions/clean.ts | 28 +- backend/src/plugins/ContextMenus/types.ts | 2 +- .../src/plugins/Counters/CountersPlugin.ts | 9 +- .../getPrettyNameForCounterTrigger.ts | 2 +- backend/src/plugins/Counters/types.ts | 26 +- .../CustomEvents/actions/addRoleAction.ts | 2 +- .../CustomEvents/actions/createCaseAction.ts | 2 +- .../actions/makeRoleMentionableAction.ts | 2 +- .../actions/makeRoleUnmentionableAction.ts | 2 +- .../CustomEvents/actions/messageAction.ts | 2 +- .../actions/moveToVoiceChannelAction.ts | 2 +- .../actions/setChannelPermissionOverrides.ts | 2 +- .../CustomEvents/functions/runEvent.ts | 2 +- backend/src/plugins/CustomEvents/types.ts | 2 +- .../src/plugins/GuildAccessMonitor/types.ts | 2 +- .../GuildConfigReloaderPlugin.ts | 2 +- .../src/plugins/GuildConfigReloader/types.ts | 2 +- .../GuildInfoSaver/GuildInfoSaverPlugin.ts | 2 +- backend/src/plugins/GuildInfoSaver/types.ts | 2 +- .../GuildMemberCachePlugin.ts | 2 +- backend/src/plugins/GuildMemberCache/types.ts | 2 +- .../InternalPoster/InternalPosterPlugin.ts | 2 +- backend/src/plugins/InternalPoster/types.ts | 2 +- .../LocateUser/commands/ListFollowCmd.ts | 4 +- .../plugins/LocateUser/commands/WhereCmd.ts | 2 +- backend/src/plugins/LocateUser/types.ts | 2 +- backend/src/plugins/Logs/LogsPlugin.ts | 2 +- .../plugins/Logs/events/LogsGuildBanEvts.ts | 1 - .../plugins/Logs/logFunctions/logMemberBan.ts | 4 +- .../Logs/logFunctions/logMemberUnban.ts | 4 +- backend/src/plugins/Logs/types.ts | 5 +- backend/src/plugins/MessageSaver/types.ts | 2 +- .../commands/addcase/AddCaseMsgCmd.ts | 6 +- .../ModActions/commands/ban/BanMsgCmd.ts | 6 +- .../ModActions/commands/case/actualCaseCmd.ts | 4 +- .../commands/cases/CasesModMsgCmd.ts | 4 +- .../commands/cases/CasesUserMsgCmd.ts | 5 +- .../commands/cases/actualCasesCmd.ts | 4 +- .../commands/deletecase/DeleteCaseMsgCmd.ts | 4 +- .../deletecase/actualDeleteCaseCmd.ts | 7 +- .../commands/forceban/ForceBanMsgCmd.ts | 9 +- .../commands/forcemute/ForceMuteMsgCmd.ts | 7 +- .../commands/forceunmute/ForceUnmuteMsgCmd.ts | 8 +- .../ModActions/commands/kick/KickMsgCmd.ts | 7 +- .../commands/massban/MassBanMsgCmd.ts | 9 +- .../commands/massban/actualMassBanCmd.ts | 27 +- .../commands/massmute/MassMuteMsgCmd.ts | 9 +- .../commands/massmute/actualMassMuteCmd.ts | 7 +- .../commands/massunban/MassUnbanMsgCmd.ts | 9 +- .../commands/massunban/actualMassUnbanCmd.ts | 4 +- .../ModActions/commands/mute/MuteMsgCmd.ts | 9 +- .../ModActions/commands/unban/UnbanMsgCmd.ts | 6 +- .../commands/unmute/UnmuteMsgCmd.ts | 9 +- .../ModActions/commands/warn/WarnMsgCmd.ts | 7 +- backend/src/plugins/ModActions/types.ts | 2 +- backend/src/plugins/Mutes/types.ts | 2 +- backend/src/plugins/NameHistory/types.ts | 2 +- backend/src/plugins/Persist/types.ts | 2 +- backend/src/plugins/Phisherman/types.ts | 2 +- backend/src/plugins/PingableRoles/types.ts | 2 +- backend/src/plugins/Post/types.ts | 2 +- .../src/plugins/Post/util/actualPostCmd.ts | 6 +- .../commands/InitReactionRolesCmd.ts | 4 +- backend/src/plugins/ReactionRoles/types.ts | 2 +- backend/src/plugins/Reminders/types.ts | 2 +- backend/src/plugins/RoleButtons/types.ts | 2 +- backend/src/plugins/RoleManager/types.ts | 2 +- .../src/plugins/Roles/commands/AddRoleCmd.ts | 5 +- .../plugins/Roles/commands/MassAddRoleCmd.ts | 6 +- .../Roles/commands/MassRemoveRoleCmd.ts | 6 +- .../plugins/Roles/commands/RemoveRoleCmd.ts | 5 +- backend/src/plugins/Roles/types.ts | 2 +- .../SelfGrantableRoles/commands/RoleAddCmd.ts | 9 +- .../commands/RoleRemoveCmd.ts | 7 +- .../src/plugins/SelfGrantableRoles/types.ts | 2 +- backend/src/plugins/Slowmode/types.ts | 2 +- .../Slowmode/util/actualDisableSlowmodeCmd.ts | 2 +- backend/src/plugins/Spam/types.ts | 2 +- backend/src/plugins/Starboard/types.ts | 2 +- .../src/plugins/Tags/commands/TagEvalCmd.ts | 6 +- backend/src/plugins/Tags/types.ts | 2 +- backend/src/plugins/TimeAndDate/types.ts | 2 +- backend/src/plugins/UsernameSaver/types.ts | 2 +- backend/src/plugins/Utility/UtilityPlugin.ts | 7 +- .../src/plugins/Utility/commands/CleanCmd.ts | 346 ++++++------------ .../plugins/Utility/commands/ContextCmd.ts | 4 +- .../src/plugins/Utility/commands/InfoCmd.ts | 4 +- .../Utility/commands/MessageInfoCmd.ts | 4 +- .../plugins/Utility/commands/NicknameCmd.ts | 5 +- .../Utility/commands/NicknameResetCmd.ts | 5 +- .../src/plugins/Utility/commands/SourceCmd.ts | 5 +- .../Utility/commands/VcdisconnectCmd.ts | 5 +- .../src/plugins/Utility/commands/VcmoveCmd.ts | 10 +- .../Utility/functions/cleanMessages.ts | 49 +++ .../functions/fetchChannelMessagesToClean.ts | 116 ++++++ .../Utility/functions/getInviteInfoEmbed.ts | 3 +- backend/src/plugins/Utility/search.ts | 17 +- backend/src/plugins/Utility/types.ts | 2 +- backend/src/plugins/WelcomeMessage/types.ts | 2 +- backend/src/templateFormatter.ts | 2 +- backend/src/types.ts | 4 +- backend/src/utils.test.ts | 2 +- backend/src/utils.ts | 24 +- backend/src/utils/createPaginatedMessage.ts | 18 +- backend/src/utils/formatZodIssue.ts | 2 +- backend/src/utils/permissionNames.ts | 4 + backend/src/utils/templateSafeObjects.ts | 7 +- backend/src/utils/waitForInteraction.ts | 19 +- backend/src/utils/zColor.ts | 2 +- backend/src/utils/zValidTimezone.ts | 2 +- backend/src/utils/zodDeepPartial.ts | 165 +++++++++ backend/src/yamlParseTest.ts | 23 -- docker-compose.development.yml | 1 - docker/development/devenv/Dockerfile | 4 +- package-lock.json | 333 +++++++++-------- package.json | 2 +- 189 files changed, 1244 insertions(+), 900 deletions(-) create mode 100644 backend/src/plugins/Utility/functions/cleanMessages.ts create mode 100644 backend/src/plugins/Utility/functions/fetchChannelMessagesToClean.ts create mode 100644 backend/src/utils/zodDeepPartial.ts delete mode 100644 backend/src/yamlParseTest.ts diff --git a/.nvmrc b/.nvmrc index 3c032078..2bd5a0a9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +22 diff --git a/backend/package.json b/backend/package.json index 88a09ada..13d39d27 100644 --- a/backend/package.json +++ b/backend/package.json @@ -29,7 +29,7 @@ "migrate-rollback-prod": "npm run migrate-rollback", "migrate-rollback-dev": "npm run build && npm run migrate-rollback", "validate-active-configs": "node --enable-source-maps dist/validateActiveConfigs.js > ../config-errors.txt", - "export-config-json-schema": "node --enable-source-maps dist/exportSchemas.js > ../config-schema.json", + "export-config-json-schema": "node --enable-source-maps dist/exportSchemas.js", "test": "npm run build && npm run run-tests", "run-tests": "ava", "test-watch": "tsc-watch --build --onSuccess \"npx ava\"" @@ -41,7 +41,7 @@ "cors": "^2.8.5", "cross-env": "^7.0.3", "deep-diff": "^1.0.2", - "discord.js": "^14.14.1", + "discord.js": "^14.19.3", "dotenv": "^4.0.0", "emoji-regex": "^8.0.0", "escape-string-regexp": "^1.0.5", @@ -49,7 +49,7 @@ "fp-ts": "^2.0.1", "humanize-duration": "^3.15.0", "js-yaml": "^4.1.0", - "knub": "^32.0.0-next.21", + "knub": "^32.0.0-next.23", "knub-command-manager": "^9.1.0", "last-commit-log": "^2.1.0", "lodash-es": "^4.17.21", @@ -76,7 +76,7 @@ "uuid": "^9.0.0", "yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build", "zlib-sync": "^0.1.7", - "zod": "^3.7.2" + "zod": "^3.25.17" }, "devDependencies": { "@types/cors": "^2.8.5", diff --git a/backend/src/RegExpRunner.ts b/backend/src/RegExpRunner.ts index 6d835a84..a65eae3a 100644 --- a/backend/src/RegExpRunner.ts +++ b/backend/src/RegExpRunner.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from "events"; +import { EventEmitter } from "node:events"; import { CooldownManager } from "knub"; import { RegExpWorker, TimeoutError } from "regexp-worker"; import { MINUTES, SECONDS } from "./utils.js"; diff --git a/backend/src/SimpleCache.ts b/backend/src/SimpleCache.ts index 57a58bf7..4161ec79 100644 --- a/backend/src/SimpleCache.ts +++ b/backend/src/SimpleCache.ts @@ -46,7 +46,7 @@ export class SimpleCache { }); if (this.maxItems && this.store.size > this.maxItems) { - const keyToDelete = this.store.keys().next().value; + const keyToDelete = this.store.keys().next().value!; this.store.delete(keyToDelete); } } diff --git a/backend/src/api/docs.ts b/backend/src/api/docs.ts index 62513b02..9f2971e4 100644 --- a/backend/src/api/docs.ts +++ b/backend/src/api/docs.ts @@ -1,97 +1,90 @@ import express from "express"; -import z from "zod"; +import z from "zod/v4"; import { availableGuildPlugins } from "../plugins/availablePlugins.js"; import { ZeppelinGuildPluginInfo } from "../types.js"; import { indentLines } from "../utils.js"; import { notFound } from "./responses.js"; -function isZodObject(schema: z.ZodTypeAny): schema is z.ZodObject { - return schema._def.typeName === "ZodObject"; +function isZodObject(schema: z.ZodType): schema is z.ZodObject { + return schema.def.type === "object"; } -function isZodRecord(schema: z.ZodTypeAny): schema is z.ZodRecord { - return schema._def.typeName === "ZodRecord"; +function isZodRecord(schema: z.ZodType): schema is z.ZodRecord { + return schema.def.type === "record"; } -function isZodEffects(schema: z.ZodTypeAny): schema is z.ZodEffects { - return schema._def.typeName === "ZodEffects"; +function isZodOptional(schema: z.ZodType): schema is z.ZodOptional { + return schema.def.type === "optional"; } -function isZodOptional(schema: z.ZodTypeAny): schema is z.ZodOptional { - return schema._def.typeName === "ZodOptional"; +function isZodArray(schema: z.ZodType): schema is z.ZodArray { + return schema.def.type === "array"; } -function isZodArray(schema: z.ZodTypeAny): schema is z.ZodArray { - return schema._def.typeName === "ZodArray"; +function isZodUnion(schema: z.ZodType): schema is z.ZodUnion { + return schema.def.type === "union"; } -function isZodUnion(schema: z.ZodTypeAny): schema is z.ZodUnion { - return schema._def.typeName === "ZodUnion"; +function isZodNullable(schema: z.ZodType): schema is z.ZodNullable { + return schema.def.type === "nullable"; } -function isZodNullable(schema: z.ZodTypeAny): schema is z.ZodNullable { - return schema._def.typeName === "ZodNullable"; +function isZodDefault(schema: z.ZodType): schema is z.ZodDefault { + return schema.def.type === "default"; } -function isZodDefault(schema: z.ZodTypeAny): schema is z.ZodDefault { - return schema._def.typeName === "ZodDefault"; +function isZodLiteral(schema: z.ZodType): schema is z.ZodLiteral { + return schema.def.type === "literal"; } -function isZodLiteral(schema: z.ZodTypeAny): schema is z.ZodLiteral { - return schema._def.typeName === "ZodLiteral"; +function isZodIntersection(schema: z.ZodType): schema is z.ZodIntersection { + return schema.def.type === "intersection"; } -function isZodIntersection(schema: z.ZodTypeAny): schema is z.ZodIntersection { - return schema._def.typeName === "ZodIntersection"; -} - -function formatZodConfigSchema(schema: z.ZodTypeAny) { +function formatZodConfigSchema(schema: z.ZodType) { if (isZodObject(schema)) { return ( `{\n` + Object.entries(schema._def.shape()) - .map(([k, value]) => indentLines(`${k}: ${formatZodConfigSchema(value as z.ZodTypeAny)}`, 2)) + .map(([k, value]) => indentLines(`${k}: ${formatZodConfigSchema(value as z.ZodType)}`, 2)) .join("\n") + "\n}" ); } if (isZodRecord(schema)) { - return "{\n" + indentLines(`[string]: ${formatZodConfigSchema(schema._def.valueType)}`, 2) + "\n}"; - } - if (isZodEffects(schema)) { - return formatZodConfigSchema(schema._def.schema); + return "{\n" + indentLines(`[string]: ${formatZodConfigSchema(schema.valueType as z.ZodType)}`, 2) + "\n}"; } if (isZodOptional(schema)) { - return `Optional<${formatZodConfigSchema(schema._def.innerType)}>`; + return `Optional<${formatZodConfigSchema(schema.def.innerType)}>`; } if (isZodArray(schema)) { - return `Array<${formatZodConfigSchema(schema._def.type)}>`; + return `Array<${formatZodConfigSchema(schema.def.element)}>`; } if (isZodUnion(schema)) { return schema._def.options.map((t) => formatZodConfigSchema(t)).join(" | "); } if (isZodNullable(schema)) { - return `Nullable<${formatZodConfigSchema(schema._def.innerType)}>`; + return `Nullable<${formatZodConfigSchema(schema.def.innerType)}>`; } if (isZodDefault(schema)) { - return formatZodConfigSchema(schema._def.innerType); + return formatZodConfigSchema(schema.def.innerType); } if (isZodLiteral(schema)) { - return schema._def.value; + return schema.def.values; } if (isZodIntersection(schema)) { - return [formatZodConfigSchema(schema._def.left), formatZodConfigSchema(schema._def.right)].join(" & "); + return [formatZodConfigSchema(schema.def.left as z.ZodType), formatZodConfigSchema(schema.def.right as z.ZodType)].join(" & "); } - if (schema._def.typeName === "ZodString") { + if (schema.def.type === "string") { return "string"; } - if (schema._def.typeName === "ZodNumber") { + if (schema.def.type === "number") { return "number"; } - if (schema._def.typeName === "ZodBoolean") { + if (schema.def.type === "boolean") { return "boolean"; } - if (schema._def.typeName === "ZodNever") { + if (schema.def.type === "never") { return "never"; } return "unknown"; diff --git a/backend/src/api/guilds/importExport.ts b/backend/src/api/guilds/importExport.ts index 73d8af2f..15f5d277 100644 --- a/backend/src/api/guilds/importExport.ts +++ b/backend/src/api/guilds/importExport.ts @@ -1,7 +1,7 @@ import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js"; import express, { Request, Response } from "express"; import moment from "moment-timezone"; -import { z } from "zod"; +import { z } from "zod/v4"; import { GuildCases } from "../../data/GuildCases.js"; import { Case } from "../../data/entities/Case.js"; import { MINUTES } from "../../utils.js"; diff --git a/backend/src/configValidator.ts b/backend/src/configValidator.ts index 557ac1a5..ece85b94 100644 --- a/backend/src/configValidator.ts +++ b/backend/src/configValidator.ts @@ -1,5 +1,5 @@ import { BaseConfig, ConfigValidationError, GuildPluginBlueprint, PluginConfigManager } from "knub"; -import { ZodError } from "zod"; +import { ZodError } from "zod/v4"; import { availableGuildPlugins } from "./plugins/availablePlugins.js"; import { zZeppelinGuildConfig } from "./types.js"; import { formatZodIssue } from "./utils/formatZodIssue.js"; diff --git a/backend/src/env.ts b/backend/src/env.ts index e4b9214d..f2a387fd 100644 --- a/backend/src/env.ts +++ b/backend/src/env.ts @@ -1,7 +1,7 @@ import dotenv from "dotenv"; import fs from "fs"; import path from "path"; -import { z } from "zod"; +import { z } from "zod/v4"; import { rootDir } from "./paths.js"; const envType = z.object({ diff --git a/backend/src/exportSchemas.ts b/backend/src/exportSchemas.ts index 5a9c2f44..e00dade4 100644 --- a/backend/src/exportSchemas.ts +++ b/backend/src/exportSchemas.ts @@ -1,21 +1,89 @@ -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from "zod/v4"; import { availableGuildPlugins } from "./plugins/availablePlugins.js"; import { zZeppelinGuildConfig } from "./types.js"; +import { deepPartial } from "./utils/zodDeepPartial.js"; +import fs from "node:fs"; + +const basePluginOverrideCriteriaSchema = z.strictObject({ + channel: z + .union([z.string(), z.array(z.string())]) + .nullable() + .optional(), + category: z + .union([z.string(), z.array(z.string())]) + .nullable() + .optional(), + level: z + .union([z.string(), z.array(z.string())]) + .nullable() + .optional(), + user: z + .union([z.string(), z.array(z.string())]) + .nullable() + .optional(), + role: z + .union([z.string(), z.array(z.string())]) + .nullable() + .optional(), + thread: z + .union([z.string(), z.array(z.string())]) + .nullable() + .optional(), + is_thread: z.boolean().nullable().optional(), + thread_type: z.literal(["public", "private"]).nullable().optional(), + extra: z.any().optional(), +}); + +const pluginOverrideCriteriaSchema = basePluginOverrideCriteriaSchema.extend({ + get zzz_dummy_property_do_not_use() { + return pluginOverrideCriteriaSchema.optional(); + }, + get all() { + return z.array(pluginOverrideCriteriaSchema).optional(); + }, + get any() { + return z.array(pluginOverrideCriteriaSchema).optional(); + }, + get not() { + return pluginOverrideCriteriaSchema.optional(); + }, +}); + +const outputPath = process.argv[2]; +if (!outputPath) { + console.error("Output path required"); + process.exit(1); +} + +const partialConfigs = new Map(); +function getPartialConfig(configSchema: z.ZodType) { + if (!partialConfigs.has(configSchema)) { + partialConfigs.set(configSchema, deepPartial(configSchema)); + } + return partialConfigs.get(configSchema)!; +} + +function overrides(configSchema: z.ZodType): z.ZodType { + const partialConfig = getPartialConfig(configSchema); + return pluginOverrideCriteriaSchema.extend({ + config: partialConfig, + }); +} const pluginSchemaMap = availableGuildPlugins.reduce((map, pluginInfo) => { - map[pluginInfo.plugin.name] = pluginInfo.docs.configSchema; + map[pluginInfo.plugin.name] = z.object({ + config: pluginInfo.docs.configSchema.optional(), + overrides: overrides(pluginInfo.docs.configSchema).optional(), + }); return map; }, {}); -const fullSchema = zZeppelinGuildConfig.omit({ plugins: true }).merge( - z.strictObject({ - plugins: z.strictObject(pluginSchemaMap).partial(), - }), -); +const fullSchema = zZeppelinGuildConfig.omit({ plugins: true }).extend({ + plugins: z.strictObject(pluginSchemaMap).partial().optional(), +}); -const jsonSchema = zodToJsonSchema(fullSchema); +const jsonSchema = z.toJSONSchema(fullSchema, { io: "input", cycles: "ref" }); -console.log(JSON.stringify(jsonSchema, null, 2)); +fs.writeFileSync(outputPath, JSON.stringify(jsonSchema, null, 2), { encoding: "utf8" }); process.exit(0); diff --git a/backend/src/index.ts b/backend/src/index.ts index 24cd5136..87ec5411 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -12,7 +12,6 @@ import { TextChannel, ThreadChannel, } from "discord.js"; -import { EventEmitter } from "events"; import { Knub, PluginError, PluginLoadError, PluginNotLoadedError } from "knub"; import moment from "moment-timezone"; import { performance } from "perf_hooks"; @@ -252,9 +251,8 @@ connect().then(async () => { GatewayIntentBits.GuildVoiceStates, ], }); - // FIXME: TS doesn't see Client as a child of EventEmitter for some reason - // If you're here because of an error from TS 5.5.2, see https://github.com/discordjs/discord.js/issues/10358 - (client as unknown as EventEmitter).setMaxListeners(200); + + client.setMaxListeners(200); const safe429DecayInterval = 5 * SECONDS; const safe429MaxCount = 5; diff --git a/backend/src/migrations/1556909512501-MigrateUsernamesToNewHistoryTable.ts b/backend/src/migrations/1556909512501-MigrateUsernamesToNewHistoryTable.ts index 33dc43c3..2d1be496 100644 --- a/backend/src/migrations/1556909512501-MigrateUsernamesToNewHistoryTable.ts +++ b/backend/src/migrations/1556909512501-MigrateUsernamesToNewHistoryTable.ts @@ -9,7 +9,7 @@ export class MigrateUsernamesToNewHistoryTable1556909512501 implements Migration const migratedUsernames = new Set(); - await new Promise(async (resolve) => { + await new Promise(async (resolve) => { const stream = await queryRunner.stream("SELECT CONCAT(user_id, '-', username) AS `key` FROM username_history"); stream.on("data", (row: any) => { migratedUsernames.add(row.key); diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 615ec7a0..5a6dc910 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -3,16 +3,27 @@ */ import { + BaseChannel, + BitField, + BitFieldResolvable, ChatInputCommandInteraction, + CommandInteraction, GuildMember, + InteractionEditReplyOptions, InteractionReplyOptions, + InteractionResponse, Message, MessageCreateOptions, + MessageEditOptions, + MessageFlags, + MessageFlagsString, + ModalSubmitInteraction, PermissionsBitField, + SendableChannels, TextBasedChannel, User, } from "discord.js"; -import { AnyPluginData, BasePluginData, CommandContext, ExtendedMatchParams, GuildPluginData, helpers } from "knub"; +import { AnyPluginData, BasePluginData, CommandContext, ExtendedMatchParams, GuildPluginData, helpers, PluginConfigManager } from "knub"; import { isStaff } from "./staff.js"; import { TZeppelinKnub } from "./types.js"; import { Tail } from "./utils/typeUtils.js"; @@ -49,57 +60,124 @@ export async function hasPermission( return helpers.hasPermission(config, permission); } +export type GenericCommandSource = Message | CommandInteraction | ModalSubmitInteraction; + export function isContextInteraction( - context: TextBasedChannel | Message | User | ChatInputCommandInteraction, -): context is ChatInputCommandInteraction { - return "commandId" in context && !!context.commandId; + context: GenericCommandSource, +): context is CommandInteraction | ModalSubmitInteraction { + return (context instanceof CommandInteraction || context instanceof ModalSubmitInteraction); } export function isContextMessage( - context: TextBasedChannel | Message | User | ChatInputCommandInteraction, + context: GenericCommandSource, ): context is Message { - return "content" in context || "embeds" in context; + return (context instanceof Message); } export async function getContextChannel( - context: TextBasedChannel | Message | User | ChatInputCommandInteraction, -): Promise { + context: GenericCommandSource, +): Promise { if (isContextInteraction(context)) { - // context is ChatInputCommandInteraction - return context.channel!; - } else if ("username" in context) { - // context is User - return await (context as User).createDM(); - } else if ("send" in context) { - // context is TextBaseChannel - return context as TextBasedChannel; - } else { - // context is Message return context.channel; } + if (context instanceof Message) { + return context.channel; + } + throw new Error("Unknown context type"); } +export function getContextChannelId( + context: GenericCommandSource, +): string | null { + return context.channelId; +} + +export async function fetchContextChannel(context: GenericCommandSource) { + if (!context.guild) { + throw new Error("Missing context guild"); + } + const channelId = getContextChannelId(context); + if (!channelId) { + throw new Error("Missing context channel ID"); + } + return (await context.guild.channels.fetch(channelId))!; +} + +function flagsWithEphemeral< + TFlags extends string, + TType extends number | bigint +>(flags: BitFieldResolvable, ephemeral: boolean): BitFieldResolvable< + TFlags | Extract, + TType | MessageFlags.Ephemeral +> { + if (!ephemeral) { + return flags; + } + return new BitField(flags).add(MessageFlags.Ephemeral) as any; +} + +export type ContextResponseOptions = MessageCreateOptions & InteractionReplyOptions & InteractionEditReplyOptions; +export type ContextResponse = Message | InteractionResponse; + export async function sendContextResponse( - context: TextBasedChannel | Message | User | ChatInputCommandInteraction, - response: string | Omit | InteractionReplyOptions, + context: GenericCommandSource, + content: string | ContextResponseOptions, + ephemeral = false, ): Promise { if (isContextInteraction(context)) { - const options = { ...(typeof response === "string" ? { content: response } : response), fetchReply: true }; + const options = { ...(typeof content === "string" ? { content: content } : content), fetchReply: true }; - return ( - context.replied - ? context.followUp(options) - : context.deferred - ? context.editReply(options) - : context.reply(options) - ) as Promise; + if (context.replied) { + return context.followUp({ + ...options, + flags: flagsWithEphemeral(options.flags, ephemeral), + }); + } + if (context.deferred) { + return context.editReply(options); + } + + const replyResult = await context.reply({ + ...options, + flags: flagsWithEphemeral(options.flags, ephemeral), + withResponse: true, + }); + return replyResult.resource!.message!; } - if (typeof response !== "string" && "ephemeral" in response) { - delete response.ephemeral; + const contextChannel = await fetchContextChannel(context); + if (!contextChannel?.isSendable()) { + throw new Error("Context channel does not exist or is not sendable"); } + + return contextChannel.send(content); +} - return (await getContextChannel(context)).send(response as string | Omit); +export type ContextResponseEditOptions = MessageEditOptions & InteractionEditReplyOptions; + +export function editContextResponse( + response: ContextResponse, + content: string | ContextResponseEditOptions, +): Promise { + return response.edit(content); +} + +export async function deleteContextResponse(response: ContextResponse): Promise { + await response.delete(); +} + +export async function getConfigForContext>(config: PluginConfigManager, context: GenericCommandSource): Promise { + if (context instanceof ChatInputCommandInteraction) { + // TODO: Support for modal interactions (here and Knub) + return config.getForInteraction(context); + } + const channel = await getContextChannel(context); + const member = isContextMessage(context) && context.inGuild() ? await resolveMessageMember(context) : null; + + return config.getMatchingConfig({ + channel, + member, + }); } export function getBaseUrl(pluginData: AnyPluginData) { @@ -147,4 +225,6 @@ export function makePublicFn, T extends }; } -// ??? +export function resolveMessageMember(message: Message) { + return Promise.resolve(message.member || message.guild.members.fetch(message.author.id)); +} diff --git a/backend/src/plugins/AutoDelete/types.ts b/backend/src/plugins/AutoDelete/types.ts index 749eec49..b03ea600 100644 --- a/backend/src/plugins/AutoDelete/types.ts +++ b/backend/src/plugins/AutoDelete/types.ts @@ -1,5 +1,5 @@ import { BasePluginType } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildLogs } from "../../data/GuildLogs.js"; import { GuildSavedMessages } from "../../data/GuildSavedMessages.js"; import { SavedMessage } from "../../data/entities/SavedMessage.js"; diff --git a/backend/src/plugins/AutoDelete/util/deleteNextItem.ts b/backend/src/plugins/AutoDelete/util/deleteNextItem.ts index af40ac93..883dd9fd 100644 --- a/backend/src/plugins/AutoDelete/util/deleteNextItem.ts +++ b/backend/src/plugins/AutoDelete/util/deleteNextItem.ts @@ -17,8 +17,8 @@ export async function deleteNextItem(pluginData: GuildPluginData { +): Promise { const theCase = await pluginData.state.cases.with("notes").find(resolveCaseId(caseOrCaseId)); if (!theCase) { throw new Error("Unknown case"); diff --git a/backend/src/plugins/Cases/types.ts b/backend/src/plugins/Cases/types.ts index cb8ec139..4974901b 100644 --- a/backend/src/plugins/Cases/types.ts +++ b/backend/src/plugins/Cases/types.ts @@ -1,6 +1,6 @@ import { BasePluginType } from "knub"; import { U } from "ts-toolbelt"; -import z from "zod"; +import z from "zod/v4"; import { CaseNameToType, CaseTypes } from "../../data/CaseTypes.js"; import { GuildArchives } from "../../data/GuildArchives.js"; import { GuildCases } from "../../data/GuildCases.js"; diff --git a/backend/src/plugins/Censor/types.ts b/backend/src/plugins/Censor/types.ts index 62ed3816..812e2e23 100644 --- a/backend/src/plugins/Censor/types.ts +++ b/backend/src/plugins/Censor/types.ts @@ -1,5 +1,5 @@ import { BasePluginType } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { RegExpRunner } from "../../RegExpRunner.js"; import { GuildLogs } from "../../data/GuildLogs.js"; import { GuildSavedMessages } from "../../data/GuildSavedMessages.js"; diff --git a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts index d3885f24..dc4b5080 100644 --- a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts +++ b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts @@ -1,5 +1,5 @@ import { guildPlugin } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { CommonPlugin } from "../Common/CommonPlugin.js"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin.js"; import { ArchiveChannelCmd } from "./commands/ArchiveChannelCmd.js"; diff --git a/backend/src/plugins/Common/CommonPlugin.ts b/backend/src/plugins/Common/CommonPlugin.ts index 042a72e0..b0fcd31f 100644 --- a/backend/src/plugins/Common/CommonPlugin.ts +++ b/backend/src/plugins/Common/CommonPlugin.ts @@ -1,16 +1,18 @@ import { Attachment, ChatInputCommandInteraction, + InteractionResponse, Message, MessageCreateOptions, MessageMentionOptions, ModalSubmitInteraction, + SendableChannels, TextBasedChannel, User, } from "discord.js"; import { PluginOptions, guildPlugin } from "knub"; import { logger } from "../../logger.js"; -import { isContextInteraction, sendContextResponse } from "../../pluginUtils.js"; +import { GenericCommandSource, isContextInteraction, sendContextResponse } from "../../pluginUtils.js"; import { errorMessage, successMessage } from "../../utils.js"; import { getErrorEmoji, getSuccessEmoji } from "./functions/getEmoji.js"; import { CommonPluginType, zCommonConfig } from "./types.js"; @@ -34,101 +36,39 @@ export const CommonPlugin = guildPlugin()({ getErrorEmoji, sendSuccessMessage: async ( - context: TextBasedChannel | Message | User | ChatInputCommandInteraction, + context: GenericCommandSource | SendableChannels, body: string, allowedMentions?: MessageMentionOptions, - responseInteraction?: ModalSubmitInteraction, + responseInteraction?: never, ephemeral = true, - ): Promise => { + ) => { const emoji = getSuccessEmoji(pluginData); const formattedBody = successMessage(body, emoji); - const content: MessageCreateOptions = allowedMentions + const content = allowedMentions ? { content: formattedBody, allowedMentions } : { content: formattedBody }; - - if (responseInteraction) { - await responseInteraction - .editReply({ content: formattedBody, embeds: [], components: [] }) - .catch((err) => logger.error(`Interaction reply failed: ${err}`)); - - return; + if ("isSendable" in context) { + return context.send(content); } - - if (!isContextInteraction(context)) { - // noinspection TypeScriptValidateJSTypes - return sendContextResponse(context, { ...content }) // Force line break - .catch((err) => { - const channelInfo = - "guild" in context && context.guild ? `${context.id} (${context.guild.id})` : context.id; - - logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`); - - return undefined; - }); - } - - const replyMethod = context.replied || context.deferred ? "editReply" : "reply"; - - return context[replyMethod]({ - content: formattedBody, - embeds: [], - components: [], - fetchReply: true, - ephemeral, - }).catch((err) => { - logger.error(`Context reply failed: ${err}`); - - return undefined; - }) as Promise; + return sendContextResponse(context, content, ephemeral); }, sendErrorMessage: async ( - context: TextBasedChannel | Message | User | ChatInputCommandInteraction, + context: GenericCommandSource | SendableChannels, body: string, allowedMentions?: MessageMentionOptions, - responseInteraction?: ModalSubmitInteraction, + responseInteraction?: never, ephemeral = true, - ): Promise => { + ) => { const emoji = getErrorEmoji(pluginData); const formattedBody = errorMessage(body, emoji); - const content: MessageCreateOptions = allowedMentions + const content = allowedMentions ? { content: formattedBody, allowedMentions } : { content: formattedBody }; - - if (responseInteraction) { - await responseInteraction - .editReply({ content: formattedBody, embeds: [], components: [] }) - .catch((err) => logger.error(`Interaction reply failed: ${err}`)); - - return; + if ("isSendable" in context) { + return context.send(content); } - - if (!isContextInteraction(context)) { - // noinspection TypeScriptValidateJSTypes - return sendContextResponse(context, { ...content }) // Force line break - .catch((err) => { - const channelInfo = - "guild" in context && context.guild ? `${context.id} (${context.guild.id})` : context.id; - - logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`); - - return undefined; - }); - } - - const replyMethod = context.replied || context.deferred ? "editReply" : "reply"; - - return context[replyMethod]({ - content: formattedBody, - embeds: [], - components: [], - fetchReply: true, - ephemeral, - }).catch((err) => { - logger.error(`Context reply failed: ${err}`); - - return undefined; - }) as Promise; + return sendContextResponse(context, content, ephemeral); }, storeAttachmentsAsMessage: async (attachments: Attachment[], backupChannel?: TextBasedChannel | null) => { @@ -142,8 +82,13 @@ export const CommonPlugin = guildPlugin()({ "Cannot store attachments: no attachment storing channel configured, and no backup channel passed", ); } + if (!channel.isSendable()) { + throw new Error( + "Passed attachment storage channel is not sendable", + ); + } - return channel!.send({ + return channel.send({ content: `Storing ${attachments.length} attachment${attachments.length === 1 ? "" : "s"}`, files: attachments.map((a) => a.url), }); diff --git a/backend/src/plugins/Common/types.ts b/backend/src/plugins/Common/types.ts index 87a342f5..cf3ab991 100644 --- a/backend/src/plugins/Common/types.ts +++ b/backend/src/plugins/Common/types.ts @@ -1,5 +1,5 @@ import { BasePluginType } from "knub"; -import z from "zod"; +import z from "zod/v4"; export const zCommonConfig = z.strictObject({ success_emoji: z.string(), diff --git a/backend/src/plugins/CompanionChannels/types.ts b/backend/src/plugins/CompanionChannels/types.ts index e009f560..31db9498 100644 --- a/backend/src/plugins/CompanionChannels/types.ts +++ b/backend/src/plugins/CompanionChannels/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, CooldownManager, guildPluginEventListener } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildLogs } from "../../data/GuildLogs.js"; import { zBoundedCharacters, zSnowflake } from "../../utils.js"; diff --git a/backend/src/plugins/ContextMenus/actions/clean.ts b/backend/src/plugins/ContextMenus/actions/clean.ts index da29018a..43f2010c 100644 --- a/backend/src/plugins/ContextMenus/actions/clean.ts +++ b/backend/src/plugins/ContextMenus/actions/clean.ts @@ -18,7 +18,7 @@ export async function cleanAction( amount: number, target: string, targetMessage: Message, - targetChannel: string, + targetChannelId: string, interaction: ModalSubmitInteraction, ) { const executingMember = await pluginData.guild.members.fetch(interaction.user.id); @@ -28,12 +28,20 @@ export async function cleanAction( }); const utility = pluginData.getPlugin(UtilityPlugin); - if (!userCfg.can_use || !(await utility.hasPermission(executingMember, targetChannel, "can_clean"))) { + if (!userCfg.can_use || !(await utility.hasPermission(executingMember, targetChannelId, "can_clean"))) { await interaction .editReply({ content: "Cannot clean: insufficient permissions", embeds: [], components: [] }) .catch((err) => logger.error(`Clean interaction reply failed: ${err}`)); return; } + + const targetChannel = await pluginData.guild.channels.fetch(targetChannelId); + if (!targetChannel?.isTextBased()) { + await interaction + .editReply({ content: "Cannot clean: target channel is not a text channel", embeds: [], components: [] }) + .catch((err) => logger.error(`Clean interaction reply failed: ${err}`)); + return; + } await interaction .editReply({ @@ -43,7 +51,21 @@ export async function cleanAction( }) .catch((err) => logger.error(`Clean interaction reply failed: ${err}`)); - await utility.clean({ count: amount, channel: targetChannel, "response-interaction": interaction }, targetMessage); + const fetchMessagesResult = await utility.fetchChannelMessagesToClean(targetChannel, { + count: amount, + beforeId: targetMessage.id, + }); + if ("error" in fetchMessagesResult) { + interaction.editReply(fetchMessagesResult.error); + return; + } + + if (fetchMessagesResult.messages.length > 0) { + await utility.cleanMessages(targetChannel, fetchMessagesResult.messages, interaction.user); + interaction.editReply(`Cleaned ${fetchMessagesResult.messages.length} ${fetchMessagesResult.messages.length === 1 ? "message" : "messages"}`); + } else { + interaction.editReply("No messages to clean"); + } } export async function launchCleanActionModal( diff --git a/backend/src/plugins/ContextMenus/types.ts b/backend/src/plugins/ContextMenus/types.ts index 69276b52..07c74d9c 100644 --- a/backend/src/plugins/ContextMenus/types.ts +++ b/backend/src/plugins/ContextMenus/types.ts @@ -1,6 +1,6 @@ import { APIEmbed, Awaitable } from "discord.js"; import { BasePluginType } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildCases } from "../../data/GuildCases.js"; export const zContextMenusConfig = z.strictObject({ diff --git a/backend/src/plugins/Counters/CountersPlugin.ts b/backend/src/plugins/Counters/CountersPlugin.ts index 8140314a..7e507cd5 100644 --- a/backend/src/plugins/Counters/CountersPlugin.ts +++ b/backend/src/plugins/Counters/CountersPlugin.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events"; import { PluginOptions, guildPlugin } from "knub"; import { GuildCounters } from "../../data/GuildCounters.js"; -import { CounterTrigger, parseCounterConditionString } from "../../data/entities/CounterTrigger.js"; +import { buildCounterConditionString, CounterTrigger, getReverseCounterComparisonOp, parseCounterConditionString } from "../../data/entities/CounterTrigger.js"; import { makePublicFn } from "../../pluginUtils.js"; import { MINUTES, convertDelayStringToMS } from "../../utils.js"; import { CommonPlugin } from "../Common/CommonPlugin.js"; @@ -89,7 +89,7 @@ export const CountersPlugin = guildPlugin()({ const { state, guild } = pluginData; state.counters = new GuildCounters(guild.id); - state.events = new EventEmitter(); + state.events = new EventEmitter() as any; state.counterTriggersByCounterId = new Map(); const activeTriggerIds: number[] = []; @@ -107,7 +107,8 @@ export const CountersPlugin = guildPlugin()({ // Initialize triggers for (const [triggerName, trigger] of Object.entries(counter.triggers)) { const parsedCondition = parseCounterConditionString(trigger.condition)!; - const parsedReverseCondition = parseCounterConditionString(trigger.reverse_condition)!; + const rawReverseCondition = trigger.reverse_condition || buildCounterConditionString(getReverseCounterComparisonOp(parsedCondition[0]), parsedCondition[1]); + const parsedReverseCondition = parseCounterConditionString(rawReverseCondition)!; const counterTrigger = await state.counters.initCounterTrigger( dbCounter.id, triggerName, @@ -167,6 +168,6 @@ export const CountersPlugin = guildPlugin()({ } } - state.events.removeAllListeners(); + (state.events as any).removeAllListeners(); }, }); diff --git a/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts b/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts index 00a2be7d..097634e9 100644 --- a/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts +++ b/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts @@ -13,5 +13,5 @@ export function getPrettyNameForCounterTrigger( } const trigger = counter.triggers[triggerName]; - return trigger ? trigger.pretty_name || trigger.name : "Unknown Counter Trigger"; + return trigger ? trigger.pretty_name || triggerName : "Unknown Counter Trigger"; } diff --git a/backend/src/plugins/Counters/types.ts b/backend/src/plugins/Counters/types.ts index 8924791f..29ef6519 100644 --- a/backend/src/plugins/Counters/types.ts +++ b/backend/src/plugins/Counters/types.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "events"; import { BasePluginType, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildCounters, MAX_COUNTER_VALUE, MIN_COUNTER_VALUE } from "../../data/GuildCounters.js"; import { CounterTrigger, @@ -18,10 +18,6 @@ const MAX_TRIGGERS_PER_COUNTER = 5; export const zTrigger = z .strictObject({ // Dummy type because name gets replaced by the property key in transform() - name: z - .never() - .optional() - .transform(() => ""), pretty_name: zBoundedCharacters(0, 100).nullable().default(null), condition: zBoundedCharacters(1, 64).refine((str) => parseCounterConditionString(str) !== null, { message: "Invalid counter trigger condition", @@ -31,28 +27,9 @@ export const zTrigger = z message: "Invalid counter trigger reverse condition", }) .optional(), - }) - .transform((val, ctx) => { - const ruleName = String(ctx.path[ctx.path.length - 1]).trim(); - - let reverseCondition = val.reverse_condition; - if (!reverseCondition) { - const parsedCondition = parseCounterConditionString(val.condition)!; - reverseCondition = buildCounterConditionString( - getReverseCounterComparisonOp(parsedCondition[0]), - parsedCondition[1], - ); - } - - return { - ...val, - name: ruleName, - reverse_condition: reverseCondition, - }; }); const zTriggerFromString = zBoundedCharacters(0, 100).transform((val, ctx) => { - const ruleName = String(ctx.path[ctx.path.length - 1]).trim(); const parsedCondition = parseCounterConditionString(val); if (!parsedCondition) { ctx.addIssue({ @@ -62,7 +39,6 @@ const zTriggerFromString = zBoundedCharacters(0, 100).transform((val, ctx) => { return z.NEVER; } return { - name: ruleName, pretty_name: null, condition: buildCounterConditionString(parsedCondition[0], parsedCondition[1]), reverse_condition: buildCounterConditionString( diff --git a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts index 8a3fa477..8d7ca355 100644 --- a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts +++ b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts @@ -1,5 +1,5 @@ import { GuildPluginData } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { canActOn } from "../../../pluginUtils.js"; import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter.js"; import { resolveMember, zSnowflake } from "../../../utils.js"; diff --git a/backend/src/plugins/CustomEvents/actions/createCaseAction.ts b/backend/src/plugins/CustomEvents/actions/createCaseAction.ts index 29c4bc4d..0ab98c56 100644 --- a/backend/src/plugins/CustomEvents/actions/createCaseAction.ts +++ b/backend/src/plugins/CustomEvents/actions/createCaseAction.ts @@ -1,5 +1,5 @@ import { GuildPluginData } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { CaseTypes } from "../../../data/CaseTypes.js"; import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter.js"; import { zBoundedCharacters, zSnowflake } from "../../../utils.js"; diff --git a/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts b/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts index 39984bf2..637b1a10 100644 --- a/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts +++ b/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts @@ -1,6 +1,6 @@ import { Snowflake } from "discord.js"; import { GuildPluginData } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { TemplateSafeValueContainer } from "../../../templateFormatter.js"; import { convertDelayStringToMS, noop, zDelayString, zSnowflake } from "../../../utils.js"; import { ActionError } from "../ActionError.js"; diff --git a/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts b/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts index d1fefd11..01b4d648 100644 --- a/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts +++ b/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts @@ -1,6 +1,6 @@ import { Snowflake } from "discord.js"; import { GuildPluginData } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { TemplateSafeValueContainer } from "../../../templateFormatter.js"; import { zSnowflake } from "../../../utils.js"; import { ActionError } from "../ActionError.js"; diff --git a/backend/src/plugins/CustomEvents/actions/messageAction.ts b/backend/src/plugins/CustomEvents/actions/messageAction.ts index 0e17c19f..645483ef 100644 --- a/backend/src/plugins/CustomEvents/actions/messageAction.ts +++ b/backend/src/plugins/CustomEvents/actions/messageAction.ts @@ -1,6 +1,6 @@ import { Snowflake, TextChannel } from "discord.js"; import { GuildPluginData } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter.js"; import { zBoundedCharacters, zSnowflake } from "../../../utils.js"; import { ActionError } from "../ActionError.js"; diff --git a/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts b/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts index c7d25bca..2b08162f 100644 --- a/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts +++ b/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts @@ -1,6 +1,6 @@ import { Snowflake, VoiceChannel } from "discord.js"; import { GuildPluginData } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { canActOn } from "../../../pluginUtils.js"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter.js"; import { resolveMember, zSnowflake } from "../../../utils.js"; diff --git a/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts b/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts index 4c6af376..39cb5e3e 100644 --- a/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts +++ b/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts @@ -1,6 +1,6 @@ import { PermissionsBitField, PermissionsString, Snowflake } from "discord.js"; import { GuildPluginData } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { TemplateSafeValueContainer } from "../../../templateFormatter.js"; import { zSnowflake } from "../../../utils.js"; import { ActionError } from "../ActionError.js"; diff --git a/backend/src/plugins/CustomEvents/functions/runEvent.ts b/backend/src/plugins/CustomEvents/functions/runEvent.ts index c0247950..b91fb61c 100644 --- a/backend/src/plugins/CustomEvents/functions/runEvent.ts +++ b/backend/src/plugins/CustomEvents/functions/runEvent.ts @@ -38,7 +38,7 @@ export async function runEvent( } catch (e) { if (e instanceof ActionError) { if (event.trigger.type === "command") { - void pluginData.state.common.sendErrorMessage((eventData.msg as Message).channel, e.message); + void pluginData.state.common.sendErrorMessage(eventData.msg, e.message); } else { // TODO: Where to log action errors from other kinds of triggers? } diff --git a/backend/src/plugins/CustomEvents/types.ts b/backend/src/plugins/CustomEvents/types.ts index b9f31f64..f401258c 100644 --- a/backend/src/plugins/CustomEvents/types.ts +++ b/backend/src/plugins/CustomEvents/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { zBoundedCharacters, zBoundedRecord } from "../../utils.js"; import { CommonPlugin } from "../Common/CommonPlugin.js"; import { zAddRoleAction } from "./actions/addRoleAction.js"; diff --git a/backend/src/plugins/GuildAccessMonitor/types.ts b/backend/src/plugins/GuildAccessMonitor/types.ts index 5e027b15..1eb60817 100644 --- a/backend/src/plugins/GuildAccessMonitor/types.ts +++ b/backend/src/plugins/GuildAccessMonitor/types.ts @@ -1,3 +1,3 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const zGuildAccessMonitorConfig = z.strictObject({}); diff --git a/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts b/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts index f1f69d59..33dbbf55 100644 --- a/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts +++ b/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts @@ -1,5 +1,5 @@ import { globalPlugin } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { Configs } from "../../data/Configs.js"; import { reloadChangedGuilds } from "./functions/reloadChangedGuilds.js"; import { GuildConfigReloaderPluginType } from "./types.js"; diff --git a/backend/src/plugins/GuildConfigReloader/types.ts b/backend/src/plugins/GuildConfigReloader/types.ts index f9c0ad79..af9b62cf 100644 --- a/backend/src/plugins/GuildConfigReloader/types.ts +++ b/backend/src/plugins/GuildConfigReloader/types.ts @@ -1,5 +1,5 @@ import { BasePluginType } from "knub"; -import { z } from "zod"; +import { z } from "zod/v4"; import { Configs } from "../../data/Configs.js"; import Timeout = NodeJS.Timeout; diff --git a/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts b/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts index e6d2b54d..5271027c 100644 --- a/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts +++ b/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts @@ -1,6 +1,6 @@ import { Guild } from "discord.js"; import { guildPlugin, guildPluginEventListener } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { AllowedGuilds } from "../../data/AllowedGuilds.js"; import { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments.js"; import { MINUTES } from "../../utils.js"; diff --git a/backend/src/plugins/GuildInfoSaver/types.ts b/backend/src/plugins/GuildInfoSaver/types.ts index 7b40ee2a..32118760 100644 --- a/backend/src/plugins/GuildInfoSaver/types.ts +++ b/backend/src/plugins/GuildInfoSaver/types.ts @@ -1,5 +1,5 @@ import { BasePluginType } from "knub"; -import { z } from "zod"; +import { z } from "zod/v4"; export const zGuildInfoSaverConfig = z.strictObject({}); diff --git a/backend/src/plugins/GuildMemberCache/GuildMemberCachePlugin.ts b/backend/src/plugins/GuildMemberCache/GuildMemberCachePlugin.ts index cf521083..bdab13b7 100644 --- a/backend/src/plugins/GuildMemberCache/GuildMemberCachePlugin.ts +++ b/backend/src/plugins/GuildMemberCache/GuildMemberCachePlugin.ts @@ -1,5 +1,5 @@ import { guildPlugin } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildMemberCache } from "../../data/GuildMemberCache.js"; import { makePublicFn } from "../../pluginUtils.js"; import { SECONDS } from "../../utils.js"; diff --git a/backend/src/plugins/GuildMemberCache/types.ts b/backend/src/plugins/GuildMemberCache/types.ts index 3ad89f53..77f15920 100644 --- a/backend/src/plugins/GuildMemberCache/types.ts +++ b/backend/src/plugins/GuildMemberCache/types.ts @@ -1,5 +1,5 @@ import { BasePluginType } from "knub"; -import { z } from "zod"; +import { z } from "zod/v4"; import { GuildMemberCache } from "../../data/GuildMemberCache.js"; export const zGuildMemberCacheConfig = z.strictObject({}); diff --git a/backend/src/plugins/InternalPoster/InternalPosterPlugin.ts b/backend/src/plugins/InternalPoster/InternalPosterPlugin.ts index 985608c6..79ca3c04 100644 --- a/backend/src/plugins/InternalPoster/InternalPosterPlugin.ts +++ b/backend/src/plugins/InternalPoster/InternalPosterPlugin.ts @@ -1,5 +1,5 @@ import { PluginOptions, guildPlugin } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { Queue } from "../../Queue.js"; import { Webhooks } from "../../data/Webhooks.js"; import { makePublicFn } from "../../pluginUtils.js"; diff --git a/backend/src/plugins/InternalPoster/types.ts b/backend/src/plugins/InternalPoster/types.ts index c0c99c55..90571ea7 100644 --- a/backend/src/plugins/InternalPoster/types.ts +++ b/backend/src/plugins/InternalPoster/types.ts @@ -1,6 +1,6 @@ import { WebhookClient } from "discord.js"; import { BasePluginType } from "knub"; -import { z } from "zod"; +import { z } from "zod/v4"; import { Queue } from "../../Queue.js"; import { Webhooks } from "../../data/Webhooks.js"; diff --git a/backend/src/plugins/LocateUser/commands/ListFollowCmd.ts b/backend/src/plugins/LocateUser/commands/ListFollowCmd.ts index cd3d6d45..fa2e4edc 100644 --- a/backend/src/plugins/LocateUser/commands/ListFollowCmd.ts +++ b/backend/src/plugins/LocateUser/commands/ListFollowCmd.ts @@ -10,7 +10,7 @@ export const ListFollowCmd = locateUserCmd({ permission: "can_alert", async run({ message: msg, pluginData }) { - const alerts = await pluginData.state.alerts.getAlertsByRequestorId(msg.member.id); + const alerts = await pluginData.state.alerts.getAlertsByRequestorId(msg.author.id); if (alerts.length === 0) { void pluginData.state.common.sendErrorMessage(msg, "You have no active alerts!"); return; @@ -41,7 +41,7 @@ export const DeleteFollowCmd = locateUserCmd({ }, async run({ message: msg, args, pluginData }) { - const alerts = await pluginData.state.alerts.getAlertsByRequestorId(msg.member.id); + const alerts = await pluginData.state.alerts.getAlertsByRequestorId(msg.author.id); alerts.sort(sorter("expires_at")); if (args.num > alerts.length || args.num <= 0) { diff --git a/backend/src/plugins/LocateUser/commands/WhereCmd.ts b/backend/src/plugins/LocateUser/commands/WhereCmd.ts index 245a128a..500f9bfe 100644 --- a/backend/src/plugins/LocateUser/commands/WhereCmd.ts +++ b/backend/src/plugins/LocateUser/commands/WhereCmd.ts @@ -13,6 +13,6 @@ export const WhereCmd = locateUserCmd({ }, async run({ message: msg, args, pluginData }) { - sendWhere(pluginData, args.member, msg.channel, `<@${msg.member.id}> | `); + sendWhere(pluginData, args.member, msg.channel, `<@${msg.author.id}> | `); }, }); diff --git a/backend/src/plugins/LocateUser/types.ts b/backend/src/plugins/LocateUser/types.ts index bbcb6d88..14302403 100644 --- a/backend/src/plugins/LocateUser/types.ts +++ b/backend/src/plugins/LocateUser/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildVCAlerts } from "../../data/GuildVCAlerts.js"; import { CommonPlugin } from "../Common/CommonPlugin.js"; diff --git a/backend/src/plugins/Logs/LogsPlugin.ts b/backend/src/plugins/Logs/LogsPlugin.ts index d45b86dc..be2593a5 100644 --- a/backend/src/plugins/Logs/LogsPlugin.ts +++ b/backend/src/plugins/Logs/LogsPlugin.ts @@ -1,5 +1,5 @@ import { CooldownManager, PluginOptions, guildPlugin } from "knub"; -import DefaultLogMessages from "../../data/DefaultLogMessages.json" assert { type: "json" }; +import DefaultLogMessages from "../../data/DefaultLogMessages.json" with { type: "json" }; import { GuildArchives } from "../../data/GuildArchives.js"; import { GuildCases } from "../../data/GuildCases.js"; import { GuildLogs } from "../../data/GuildLogs.js"; diff --git a/backend/src/plugins/Logs/events/LogsGuildBanEvts.ts b/backend/src/plugins/Logs/events/LogsGuildBanEvts.ts index 0b6c2558..75c50de8 100644 --- a/backend/src/plugins/Logs/events/LogsGuildBanEvts.ts +++ b/backend/src/plugins/Logs/events/LogsGuildBanEvts.ts @@ -49,7 +49,6 @@ export const LogsGuildBanRemoveEvt = logsEvt({ user.id, ); const mod = relevantAuditLogEntry?.executor ?? null; - logMemberUnban(pluginData, { mod, userId: user.id, diff --git a/backend/src/plugins/Logs/logFunctions/logMemberBan.ts b/backend/src/plugins/Logs/logFunctions/logMemberBan.ts index d87fd9df..001e7615 100644 --- a/backend/src/plugins/Logs/logFunctions/logMemberBan.ts +++ b/backend/src/plugins/Logs/logFunctions/logMemberBan.ts @@ -1,4 +1,4 @@ -import { User } from "discord.js"; +import { PartialUser, User } from "discord.js"; import { GuildPluginData } from "knub"; import { LogType } from "../../../data/LogType.js"; import { createTypedTemplateSafeValueContainer } from "../../../templateFormatter.js"; @@ -8,7 +8,7 @@ import { LogsPluginType } from "../types.js"; import { log } from "../util/log.js"; export interface LogMemberBanData { - mod: User | UnknownUser | null; + mod: User | UnknownUser | PartialUser | null; user: User | UnknownUser; caseNumber: number; reason: string; diff --git a/backend/src/plugins/Logs/logFunctions/logMemberUnban.ts b/backend/src/plugins/Logs/logFunctions/logMemberUnban.ts index 7d763192..be6c8aa3 100644 --- a/backend/src/plugins/Logs/logFunctions/logMemberUnban.ts +++ b/backend/src/plugins/Logs/logFunctions/logMemberUnban.ts @@ -1,4 +1,4 @@ -import { Snowflake, User } from "discord.js"; +import { PartialUser, Snowflake, User } from "discord.js"; import { GuildPluginData } from "knub"; import { LogType } from "../../../data/LogType.js"; import { createTypedTemplateSafeValueContainer } from "../../../templateFormatter.js"; @@ -8,7 +8,7 @@ import { LogsPluginType } from "../types.js"; import { log } from "../util/log.js"; export interface LogMemberUnbanData { - mod: User | UnknownUser | null; + mod: User | UnknownUser | PartialUser | null; userId: Snowflake; caseNumber: number; reason: string; diff --git a/backend/src/plugins/Logs/types.ts b/backend/src/plugins/Logs/types.ts index 11eadbd1..a757524a 100644 --- a/backend/src/plugins/Logs/types.ts +++ b/backend/src/plugins/Logs/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, CooldownManager, guildPluginEventListener } from "knub"; -import { z } from "zod"; +import { z } from "zod/v4"; import { RegExpRunner } from "../../RegExpRunner.js"; import { GuildArchives } from "../../data/GuildArchives.js"; import { GuildCases } from "../../data/GuildCases.js"; @@ -29,6 +29,7 @@ const MAX_BATCH_TIME = 5000; // A bit of a workaround so we can pass LogType keys to z.enum() const logTypes = Object.keys(LogType) as [keyof typeof LogType, ...Array]; const zLogFormats = z.record(z.enum(logTypes), zMessageContent); +type TLogFormats = z.infer; const zLogChannel = z.strictObject({ include: z.array(zBoundedCharacters(1, 255)).default([]), @@ -42,7 +43,7 @@ const zLogChannel = z.strictObject({ excluded_threads: z.array(zSnowflake).nullable().default(null), exclude_bots: z.boolean().default(false), excluded_roles: z.array(zSnowflake).nullable().default(null), - format: zLogFormats.default({}), + format: zLogFormats.default({} as TLogFormats), timestamp_format: z.string().nullable().default(null), include_embed_timestamp: z.boolean().nullable().default(null), }); diff --git a/backend/src/plugins/MessageSaver/types.ts b/backend/src/plugins/MessageSaver/types.ts index 1102a9ac..f7e0495d 100644 --- a/backend/src/plugins/MessageSaver/types.ts +++ b/backend/src/plugins/MessageSaver/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildSavedMessages } from "../../data/GuildSavedMessages.js"; import { CommonPlugin } from "../Common/CommonPlugin.js"; diff --git a/backend/src/plugins/ModActions/commands/addcase/AddCaseMsgCmd.ts b/backend/src/plugins/ModActions/commands/addcase/AddCaseMsgCmd.ts index 4c69967e..e4be443e 100644 --- a/backend/src/plugins/ModActions/commands/addcase/AddCaseMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/addcase/AddCaseMsgCmd.ts @@ -31,8 +31,10 @@ export const AddCaseMsgCmd = modActionsMsgCmd({ return; } + const member = msg.member || await msg.guild.members.fetch(msg.author.id); + // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; + let mod = member; if (args.mod) { if (!(await hasPermission(pluginData, "can_act_as_other", { message: msg }))) { pluginData.state.common.sendErrorMessage(msg, "You don't have permission to use -mod"); @@ -52,7 +54,7 @@ export const AddCaseMsgCmd = modActionsMsgCmd({ actualAddCaseCmd( pluginData, msg, - msg.member, + member, mod, [...msg.attachments.values()], user, diff --git a/backend/src/plugins/ModActions/commands/ban/BanMsgCmd.ts b/backend/src/plugins/ModActions/commands/ban/BanMsgCmd.ts index c02fe298..d5272ef5 100644 --- a/backend/src/plugins/ModActions/commands/ban/BanMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/ban/BanMsgCmd.ts @@ -41,8 +41,10 @@ export const BanMsgCmd = modActionsMsgCmd({ return; } + const member = msg.member || await msg.guild.members.fetch(msg.author.id); + // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; + let mod = member; if (args.mod) { if (!(await hasPermission(pluginData, "can_act_as_other", { message: msg }))) { pluginData.state.common.sendErrorMessage(msg, "You don't have permission to use -mod"); @@ -67,7 +69,7 @@ export const BanMsgCmd = modActionsMsgCmd({ args["time"] ? args["time"] : null, args.reason || "", [...msg.attachments.values()], - msg.member, + member, mod, contactMethods, ); diff --git a/backend/src/plugins/ModActions/commands/case/actualCaseCmd.ts b/backend/src/plugins/ModActions/commands/case/actualCaseCmd.ts index ac4311ec..15af3e49 100644 --- a/backend/src/plugins/ModActions/commands/case/actualCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/case/actualCaseCmd.ts @@ -19,7 +19,7 @@ export async function actualCaseCmd( } const casesPlugin = pluginData.getPlugin(CasesPlugin); - const embed = await casesPlugin.getCaseEmbed(theCase.id, authorId); + const content = await casesPlugin.getCaseEmbed(theCase.id, authorId); - void sendContextResponse(context, { ...embed, ephemeral: show !== true }); + void sendContextResponse(context, content, show !== true); } diff --git a/backend/src/plugins/ModActions/commands/cases/CasesModMsgCmd.ts b/backend/src/plugins/ModActions/commands/cases/CasesModMsgCmd.ts index b57da947..92fe53f3 100644 --- a/backend/src/plugins/ModActions/commands/cases/CasesModMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/cases/CasesModMsgCmd.ts @@ -1,4 +1,5 @@ import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; +import { resolveMessageMember } from "../../../../pluginUtils.js"; import { modActionsMsgCmd } from "../../types.js"; import { actualCasesCmd } from "./actualCasesCmd.js"; @@ -29,12 +30,13 @@ export const CasesModMsgCmd = modActionsMsgCmd({ ], async run({ pluginData, message: msg, args }) { + const member = await resolveMessageMember(msg); return actualCasesCmd( pluginData, msg, args.mod, null, - msg.member, + member, args.notes, args.warns, args.mutes, diff --git a/backend/src/plugins/ModActions/commands/cases/CasesUserMsgCmd.ts b/backend/src/plugins/ModActions/commands/cases/CasesUserMsgCmd.ts index 29adebd6..808f8b26 100644 --- a/backend/src/plugins/ModActions/commands/cases/CasesUserMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/cases/CasesUserMsgCmd.ts @@ -1,4 +1,5 @@ import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; +import { resolveMessageMember } from "../../../../pluginUtils.js"; import { resolveMember, resolveUser, UnknownUser } from "../../../../utils.js"; import { modActionsMsgCmd } from "../../types.js"; import { actualCasesCmd } from "./actualCasesCmd.js"; @@ -40,13 +41,15 @@ export const CasesUserMsgCmd = modActionsMsgCmd({ pluginData.state.common.sendErrorMessage(msg, `User not found`); return; } + + const member = await resolveMessageMember(msg); return actualCasesCmd( pluginData, msg, args.mod, user, - msg.member, + member, args.notes, args.warns, args.mutes, diff --git a/backend/src/plugins/ModActions/commands/cases/actualCasesCmd.ts b/backend/src/plugins/ModActions/commands/cases/actualCasesCmd.ts index 9047d674..1f668c70 100644 --- a/backend/src/plugins/ModActions/commands/cases/actualCasesCmd.ts +++ b/backend/src/plugins/ModActions/commands/cases/actualCasesCmd.ts @@ -41,8 +41,8 @@ async function sendExpandedCases( const casesPlugin = pluginData.getPlugin(CasesPlugin); for (const theCase of cases) { - const embed = await casesPlugin.getCaseEmbed(theCase.id); - await sendContextResponse(context, { ...embed, ephemeral: !show }); + const content = await casesPlugin.getCaseEmbed(theCase.id); + await sendContextResponse(context, content, !show); } } diff --git a/backend/src/plugins/ModActions/commands/deletecase/DeleteCaseMsgCmd.ts b/backend/src/plugins/ModActions/commands/deletecase/DeleteCaseMsgCmd.ts index c1449e93..5d109553 100644 --- a/backend/src/plugins/ModActions/commands/deletecase/DeleteCaseMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/deletecase/DeleteCaseMsgCmd.ts @@ -1,4 +1,5 @@ import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; +import { resolveMessageMember } from "../../../../pluginUtils.js"; import { trimLines } from "../../../../utils.js"; import { modActionsMsgCmd } from "../../types.js"; import { actualDeleteCaseCmd } from "./actualDeleteCaseCmd.js"; @@ -18,6 +19,7 @@ export const DeleteCaseMsgCmd = modActionsMsgCmd({ }, async run({ pluginData, message, args }) { - actualDeleteCaseCmd(pluginData, message, message.member, args.caseNumber, args.force); + const member = await resolveMessageMember(message); + actualDeleteCaseCmd(pluginData, message, member, args.caseNumber, args.force); }, }); diff --git a/backend/src/plugins/ModActions/commands/deletecase/actualDeleteCaseCmd.ts b/backend/src/plugins/ModActions/commands/deletecase/actualDeleteCaseCmd.ts index 0e57fbf1..f3a6322c 100644 --- a/backend/src/plugins/ModActions/commands/deletecase/actualDeleteCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/deletecase/actualDeleteCaseCmd.ts @@ -36,6 +36,11 @@ export async function actualDeleteCaseCmd( for (const theCase of validCases) { if (!force) { + const channel = await getContextChannel(context); + if (!channel) { + return; + } + const cases = pluginData.getPlugin(CasesPlugin); const embedContent = await cases.getCaseEmbed(theCase); sendContextResponse(context, { @@ -45,7 +50,7 @@ export async function actualDeleteCaseCmd( const reply = await helpers.waitForReply( pluginData.client, - await getContextChannel(context), + channel, author.id, 15 * SECONDS, ); diff --git a/backend/src/plugins/ModActions/commands/forceban/ForceBanMsgCmd.ts b/backend/src/plugins/ModActions/commands/forceban/ForceBanMsgCmd.ts index 2f2cce21..d6323098 100644 --- a/backend/src/plugins/ModActions/commands/forceban/ForceBanMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/forceban/ForceBanMsgCmd.ts @@ -1,5 +1,5 @@ import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; -import { canActOn, hasPermission } from "../../../../pluginUtils.js"; +import { canActOn, hasPermission, resolveMessageMember } from "../../../../pluginUtils.js"; import { resolveMember, resolveUser } from "../../../../utils.js"; import { isBanned } from "../../functions/isBanned.js"; import { modActionsMsgCmd } from "../../types.js"; @@ -31,8 +31,9 @@ export const ForceBanMsgCmd = modActionsMsgCmd({ } // If the user exists as a guild member, make sure we can act on them first - const member = await resolveMember(pluginData.client, pluginData.guild, user.id); - if (member && !canActOn(pluginData, msg.member, member)) { + const authorMember = await resolveMessageMember(msg); + const targetMember = await resolveMember(pluginData.client, pluginData.guild, user.id); + if (targetMember && !canActOn(pluginData, authorMember, targetMember)) { pluginData.state.common.sendErrorMessage(msg, "Cannot forceban this user: insufficient permissions"); return; } @@ -45,7 +46,7 @@ export const ForceBanMsgCmd = modActionsMsgCmd({ } // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; + let mod = authorMember; if (args.mod) { if (!(await hasPermission(pluginData, "can_act_as_other", { message: msg }))) { pluginData.state.common.sendErrorMessage(msg, "You don't have permission to use -mod"); diff --git a/backend/src/plugins/ModActions/commands/forcemute/ForceMuteMsgCmd.ts b/backend/src/plugins/ModActions/commands/forcemute/ForceMuteMsgCmd.ts index 1268fbc4..4be9c104 100644 --- a/backend/src/plugins/ModActions/commands/forcemute/ForceMuteMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/forcemute/ForceMuteMsgCmd.ts @@ -1,5 +1,5 @@ import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; -import { canActOn, hasPermission } from "../../../../pluginUtils.js"; +import { canActOn, hasPermission, resolveMessageMember } from "../../../../pluginUtils.js"; import { resolveMember, resolveUser } from "../../../../utils.js"; import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs.js"; import { modActionsMsgCmd } from "../../types.js"; @@ -39,16 +39,17 @@ export const ForceMuteMsgCmd = modActionsMsgCmd({ return; } + const authorMember = await resolveMessageMember(msg) const memberToMute = await resolveMember(pluginData.client, pluginData.guild, user.id); // Make sure we're allowed to mute this user - if (memberToMute && !canActOn(pluginData, msg.member, memberToMute)) { + if (memberToMute && !canActOn(pluginData, authorMember, memberToMute)) { pluginData.state.common.sendErrorMessage(msg, "Cannot mute: insufficient permissions"); return; } // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; + let mod = authorMember; let ppId: string | undefined; if (args.mod) { diff --git a/backend/src/plugins/ModActions/commands/forceunmute/ForceUnmuteMsgCmd.ts b/backend/src/plugins/ModActions/commands/forceunmute/ForceUnmuteMsgCmd.ts index 6194a932..b5a7f3c0 100644 --- a/backend/src/plugins/ModActions/commands/forceunmute/ForceUnmuteMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/forceunmute/ForceUnmuteMsgCmd.ts @@ -1,5 +1,5 @@ import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; -import { canActOn, hasPermission } from "../../../../pluginUtils.js"; +import { canActOn, hasPermission, resolveMessageMember } from "../../../../pluginUtils.js"; import { resolveMember, resolveUser } from "../../../../utils.js"; import { modActionsMsgCmd } from "../../types.js"; import { actualUnmuteCmd } from "../unmute/actualUnmuteCmd.js"; @@ -42,17 +42,17 @@ export const ForceUnmuteMsgCmd = modActionsMsgCmd({ return; } - // Find the server member to unmute + const authorMember = await resolveMessageMember(msg); const memberToUnmute = await resolveMember(pluginData.client, pluginData.guild, user.id); // Make sure we're allowed to unmute this member - if (memberToUnmute && !canActOn(pluginData, msg.member, memberToUnmute)) { + if (memberToUnmute && !canActOn(pluginData, authorMember, memberToUnmute)) { pluginData.state.common.sendErrorMessage(msg, "Cannot unmute: insufficient permissions"); return; } // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; + let mod = authorMember; let ppId: string | undefined; if (args.mod) { diff --git a/backend/src/plugins/ModActions/commands/kick/KickMsgCmd.ts b/backend/src/plugins/ModActions/commands/kick/KickMsgCmd.ts index ebc165ff..27ad3796 100644 --- a/backend/src/plugins/ModActions/commands/kick/KickMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/kick/KickMsgCmd.ts @@ -4,6 +4,7 @@ import { resolveUser } from "../../../../utils.js"; import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs.js"; import { modActionsMsgCmd } from "../../types.js"; import { actualKickCmd } from "./actualKickCmd.js"; +import { resolveMessageMember } from "../../../../pluginUtils.js"; const opts = { mod: ct.member({ option: true }), @@ -33,8 +34,10 @@ export const KickMsgCmd = modActionsMsgCmd({ return; } + const authorMember = await resolveMessageMember(msg); + // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; + let mod = authorMember; if (args.mod) { if (!(await hasPermission(await pluginData.config.getForMessage(msg), "can_act_as_other"))) { pluginData.state.common.sendErrorMessage(msg, "You don't have permission to use -mod"); @@ -55,7 +58,7 @@ export const KickMsgCmd = modActionsMsgCmd({ actualKickCmd( pluginData, msg, - msg.member, + authorMember, user, args.reason, [...msg.attachments.values()], diff --git a/backend/src/plugins/ModActions/commands/massban/MassBanMsgCmd.ts b/backend/src/plugins/ModActions/commands/massban/MassBanMsgCmd.ts index ac5fdb4f..ae76a372 100644 --- a/backend/src/plugins/ModActions/commands/massban/MassBanMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/massban/MassBanMsgCmd.ts @@ -1,6 +1,6 @@ import { waitForReply } from "knub/helpers"; import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; -import { getContextChannel, sendContextResponse } from "../../../../pluginUtils.js"; +import { getContextChannel, resolveMessageMember, sendContextResponse } from "../../../../pluginUtils.js"; import { modActionsMsgCmd } from "../../types.js"; import { actualMassBanCmd } from "./actualMassBanCmd.js"; @@ -17,15 +17,16 @@ export const MassBanMsgCmd = modActionsMsgCmd({ async run({ pluginData, message: msg, args }) { // Ask for ban reason (cleaner this way instead of trying to cram it into the args) - sendContextResponse(msg, "Ban reason? `cancel` to cancel"); - const banReasonReply = await waitForReply(pluginData.client, await getContextChannel(msg), msg.author.id); + msg.reply("Ban reason? `cancel` to cancel"); + const banReasonReply = await waitForReply(pluginData.client, msg.channel, msg.author.id); if (!banReasonReply || !banReasonReply.content || banReasonReply.content.toLowerCase().trim() === "cancel") { pluginData.state.common.sendErrorMessage(msg, "Cancelled"); return; } - actualMassBanCmd(pluginData, msg, args.userIds, msg.member, banReasonReply.content, [ + const authorMember = await resolveMessageMember(msg); + actualMassBanCmd(pluginData, msg, args.userIds, authorMember, banReasonReply.content, [ ...banReasonReply.attachments.values(), ]); }, diff --git a/backend/src/plugins/ModActions/commands/massban/actualMassBanCmd.ts b/backend/src/plugins/ModActions/commands/massban/actualMassBanCmd.ts index 2ec33308..58f9cb4a 100644 --- a/backend/src/plugins/ModActions/commands/massban/actualMassBanCmd.ts +++ b/backend/src/plugins/ModActions/commands/massban/actualMassBanCmd.ts @@ -3,7 +3,7 @@ import { GuildPluginData } from "knub"; import { CaseTypes } from "../../../../data/CaseTypes.js"; import { LogType } from "../../../../data/LogType.js"; import { humanizeDurationShort } from "../../../../humanizeDuration.js"; -import { canActOn, getContextChannel, isContextInteraction, sendContextResponse } from "../../../../pluginUtils.js"; +import { canActOn, deleteContextResponse, editContextResponse, getConfigForContext, getContextChannel, isContextInteraction, sendContextResponse } from "../../../../pluginUtils.js"; import { DAYS, MINUTES, SECONDS, noop } from "../../../../utils.js"; import { CasesPlugin } from "../../../Cases/CasesPlugin.js"; import { LogsPlugin } from "../../../Logs/LogsPlugin.js"; @@ -52,46 +52,31 @@ export async function actualMassBanCmd( pluginData.state.massbanQueue.length === 0 ? "Banning..." : `Massban queued. Waiting for previous massban to finish (max wait ${maxWaitTimeFormatted}).`; - const loadingMsg = await sendContextResponse(context, { content: initialLoadingText, ephemeral: true }); + const loadingMsg = await sendContextResponse(context, initialLoadingText, true); const waitTimeStart = performance.now(); const waitingInterval = setInterval(() => { const waitTime = humanizeDurationShort(performance.now() - waitTimeStart, { round: true }); const waitMessageContent = `Massban queued. Still waiting for previous massban to finish (waited ${waitTime}).`; - if (isContextInteraction(context)) { - context.editReply(waitMessageContent).catch(() => clearInterval(waitingInterval)); - } else { - loadingMsg.edit(waitMessageContent).catch(() => clearInterval(waitingInterval)); - } + editContextResponse(loadingMsg, waitMessageContent).catch(() => clearInterval(waitingInterval)); }, 1 * MINUTES); pluginData.state.massbanQueue.add(async () => { clearInterval(waitingInterval); if (pluginData.state.unloaded) { - if (isContextInteraction(context)) { - void context.deleteReply().catch(noop); - } else { - void loadingMsg.delete().catch(noop); - } - + await deleteContextResponse(loadingMsg); return; } - if (isContextInteraction(context)) { - void context.editReply("Banning...").catch(noop); - } else { - void loadingMsg.edit("Banning...").catch(noop); - } + editContextResponse(loadingMsg, "Banning...").catch(noop); // Ban each user and count failed bans (if any) const startTime = performance.now(); const failedBans: string[] = []; const casesPlugin = pluginData.getPlugin(CasesPlugin); - const messageConfig = isContextInteraction(context) - ? await pluginData.config.getForInteraction(context) - : await pluginData.config.getForChannel(await getContextChannel(context)); + const messageConfig = await getConfigForContext(pluginData.config, context); const deleteDays = messageConfig.ban_delete_message_days; for (const [i, userId] of userIds.entries()) { diff --git a/backend/src/plugins/ModActions/commands/massmute/MassMuteMsgCmd.ts b/backend/src/plugins/ModActions/commands/massmute/MassMuteMsgCmd.ts index 1c9c37ae..ea3881ba 100644 --- a/backend/src/plugins/ModActions/commands/massmute/MassMuteMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/massmute/MassMuteMsgCmd.ts @@ -1,6 +1,6 @@ import { waitForReply } from "knub/helpers"; import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; -import { getContextChannel, sendContextResponse } from "../../../../pluginUtils.js"; +import { getContextChannel, resolveMessageMember, sendContextResponse } from "../../../../pluginUtils.js"; import { modActionsMsgCmd } from "../../types.js"; import { actualMassMuteCmd } from "./actualMassMuteCmd.js"; @@ -17,8 +17,8 @@ export const MassMuteMsgCmd = modActionsMsgCmd({ async run({ pluginData, message: msg, args }) { // Ask for mute reason - sendContextResponse(msg, "Mute reason? `cancel` to cancel"); - const muteReasonReceived = await waitForReply(pluginData.client, await getContextChannel(msg), msg.author.id); + msg.reply("Mute reason? `cancel` to cancel"); + const muteReasonReceived = await waitForReply(pluginData.client, msg.channel, msg.author.id); if ( !muteReasonReceived || !muteReasonReceived.content || @@ -28,7 +28,8 @@ export const MassMuteMsgCmd = modActionsMsgCmd({ return; } - actualMassMuteCmd(pluginData, msg, args.userIds, msg.member, muteReasonReceived.content, [ + const member = await resolveMessageMember(msg); + actualMassMuteCmd(pluginData, msg, args.userIds, member, muteReasonReceived.content, [ ...muteReasonReceived.attachments.values(), ]); }, diff --git a/backend/src/plugins/ModActions/commands/massmute/actualMassMuteCmd.ts b/backend/src/plugins/ModActions/commands/massmute/actualMassMuteCmd.ts index f0c8053c..64f7722d 100644 --- a/backend/src/plugins/ModActions/commands/massmute/actualMassMuteCmd.ts +++ b/backend/src/plugins/ModActions/commands/massmute/actualMassMuteCmd.ts @@ -2,7 +2,7 @@ import { Attachment, ChatInputCommandInteraction, GuildMember, Message, Snowflak import { GuildPluginData } from "knub"; import { LogType } from "../../../../data/LogType.js"; import { logger } from "../../../../logger.js"; -import { canActOn, isContextInteraction, sendContextResponse } from "../../../../pluginUtils.js"; +import { canActOn, deleteContextResponse, isContextInteraction, sendContextResponse } from "../../../../pluginUtils.js"; import { LogsPlugin } from "../../../Logs/LogsPlugin.js"; import { MutesPlugin } from "../../../Mutes/MutesPlugin.js"; import { handleAttachmentLinkDetectionAndGetRestriction } from "../../functions/attachmentLinkReaction.js"; @@ -11,6 +11,7 @@ import { formatReasonWithMessageLinkForAttachments, } from "../../functions/formatReasonForAttachments.js"; import { ModActionsPluginType } from "../../types.js"; +import { noop } from "../../../../utils.js"; export async function actualMassMuteCmd( pluginData: GuildPluginData, @@ -50,7 +51,7 @@ export async function actualMassMuteCmd( }); // Show loading indicator - const loadingMsg = await sendContextResponse(context, { content: "Muting...", ephemeral: true }); + const loadingMsg = await sendContextResponse(context, "Muting...", true); // Mute everyone and count fails const modId = author.id; @@ -71,7 +72,7 @@ export async function actualMassMuteCmd( if (!isContextInteraction(context)) { // Clear loading indicator - loadingMsg.delete(); + deleteContextResponse(loadingMsg).catch(noop); } const successfulMuteCount = userIds.length - failedMutes.length; diff --git a/backend/src/plugins/ModActions/commands/massunban/MassUnbanMsgCmd.ts b/backend/src/plugins/ModActions/commands/massunban/MassUnbanMsgCmd.ts index 6bfa1f18..8dbc4d99 100644 --- a/backend/src/plugins/ModActions/commands/massunban/MassUnbanMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/massunban/MassUnbanMsgCmd.ts @@ -1,6 +1,6 @@ import { waitForReply } from "knub/helpers"; import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; -import { getContextChannel, sendContextResponse } from "../../../../pluginUtils.js"; +import { getContextChannel, resolveMessageMember, sendContextResponse } from "../../../../pluginUtils.js"; import { modActionsMsgCmd } from "../../types.js"; import { actualMassUnbanCmd } from "./actualMassUnbanCmd.js"; @@ -17,14 +17,15 @@ export const MassUnbanMsgCmd = modActionsMsgCmd({ async run({ pluginData, message: msg, args }) { // Ask for unban reason (cleaner this way instead of trying to cram it into the args) - sendContextResponse(msg, "Unban reason? `cancel` to cancel"); - const unbanReasonReply = await waitForReply(pluginData.client, await getContextChannel(msg), msg.author.id); + msg.reply("Unban reason? `cancel` to cancel"); + const unbanReasonReply = await waitForReply(pluginData.client, msg.channel, msg.author.id); if (!unbanReasonReply || !unbanReasonReply.content || unbanReasonReply.content.toLowerCase().trim() === "cancel") { pluginData.state.common.sendErrorMessage(msg, "Cancelled"); return; } - actualMassUnbanCmd(pluginData, msg, args.userIds, msg.member, unbanReasonReply.content, [ + const member = await resolveMessageMember(msg); + actualMassUnbanCmd(pluginData, msg, args.userIds, member, unbanReasonReply.content, [ ...unbanReasonReply.attachments.values(), ]); }, diff --git a/backend/src/plugins/ModActions/commands/massunban/actualMassUnbanCmd.ts b/backend/src/plugins/ModActions/commands/massunban/actualMassUnbanCmd.ts index ce973a05..9a132bf5 100644 --- a/backend/src/plugins/ModActions/commands/massunban/actualMassUnbanCmd.ts +++ b/backend/src/plugins/ModActions/commands/massunban/actualMassUnbanCmd.ts @@ -2,7 +2,7 @@ import { Attachment, ChatInputCommandInteraction, GuildMember, Message, Snowflak import { GuildPluginData } from "knub"; import { CaseTypes } from "../../../../data/CaseTypes.js"; import { LogType } from "../../../../data/LogType.js"; -import { isContextInteraction, sendContextResponse } from "../../../../pluginUtils.js"; +import { deleteContextResponse, isContextInteraction, sendContextResponse } from "../../../../pluginUtils.js"; import { MINUTES, noop } from "../../../../utils.js"; import { CasesPlugin } from "../../../Cases/CasesPlugin.js"; import { LogsPlugin } from "../../../Logs/LogsPlugin.js"; @@ -69,7 +69,7 @@ export async function actualMassUnbanCmd( if (!isContextInteraction(context)) { // Clear loading indicator - loadingMsg.delete().catch(noop); + await deleteContextResponse(loadingMsg).catch(noop); } const successfulUnbanCount = userIds.length - failedUnbans.length; diff --git a/backend/src/plugins/ModActions/commands/mute/MuteMsgCmd.ts b/backend/src/plugins/ModActions/commands/mute/MuteMsgCmd.ts index d31efb0c..07b23c62 100644 --- a/backend/src/plugins/ModActions/commands/mute/MuteMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/mute/MuteMsgCmd.ts @@ -1,5 +1,5 @@ import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; -import { canActOn, hasPermission } from "../../../../pluginUtils.js"; +import { canActOn, hasPermission, resolveMessageMember } from "../../../../pluginUtils.js"; import { resolveMember, resolveUser } from "../../../../utils.js"; import { waitForButtonConfirm } from "../../../../utils/waitForInteraction.js"; import { isBanned } from "../../functions/isBanned.js"; @@ -41,6 +41,7 @@ export const MuteMsgCmd = modActionsMsgCmd({ return; } + const authorMember = await resolveMessageMember(msg); const memberToMute = await resolveMember(pluginData.client, pluginData.guild, user.id); if (!memberToMute) { @@ -57,7 +58,7 @@ export const MuteMsgCmd = modActionsMsgCmd({ const reply = await waitForButtonConfirm( msg, { content: "User not found on the server, forcemute instead?" }, - { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, + { confirmText: "Yes", cancelText: "No", restrictToId: authorMember.id }, ); if (!reply) { @@ -68,13 +69,13 @@ export const MuteMsgCmd = modActionsMsgCmd({ } // Make sure we're allowed to mute this member - if (memberToMute && !canActOn(pluginData, msg.member, memberToMute)) { + if (memberToMute && !canActOn(pluginData, authorMember, memberToMute)) { pluginData.state.common.sendErrorMessage(msg, "Cannot mute: insufficient permissions"); return; } // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; + let mod = authorMember; let ppId: string | undefined; if (args.mod) { diff --git a/backend/src/plugins/ModActions/commands/unban/UnbanMsgCmd.ts b/backend/src/plugins/ModActions/commands/unban/UnbanMsgCmd.ts index 8cf5d499..053427c5 100644 --- a/backend/src/plugins/ModActions/commands/unban/UnbanMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/unban/UnbanMsgCmd.ts @@ -1,5 +1,5 @@ import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; -import { hasPermission } from "../../../../pluginUtils.js"; +import { hasPermission, resolveMessageMember } from "../../../../pluginUtils.js"; import { resolveUser } from "../../../../utils.js"; import { modActionsMsgCmd } from "../../types.js"; import { actualUnbanCmd } from "./actualUnbanCmd.js"; @@ -29,8 +29,10 @@ export const UnbanMsgCmd = modActionsMsgCmd({ return; } + const authorMember = await resolveMessageMember(msg); + // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; + let mod = authorMember; if (args.mod) { if (!(await hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id }))) { pluginData.state.common.sendErrorMessage(msg, "You don't have permission to use -mod"); diff --git a/backend/src/plugins/ModActions/commands/unmute/UnmuteMsgCmd.ts b/backend/src/plugins/ModActions/commands/unmute/UnmuteMsgCmd.ts index 638ef2f1..6c2887dd 100644 --- a/backend/src/plugins/ModActions/commands/unmute/UnmuteMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/unmute/UnmuteMsgCmd.ts @@ -1,5 +1,5 @@ import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; -import { canActOn, hasPermission } from "../../../../pluginUtils.js"; +import { canActOn, hasPermission, resolveMessageMember } from "../../../../pluginUtils.js"; import { resolveMember, resolveUser } from "../../../../utils.js"; import { waitForButtonConfirm } from "../../../../utils/waitForInteraction.js"; import { MutesPlugin } from "../../../Mutes/MutesPlugin.js"; @@ -39,6 +39,7 @@ export const UnmuteMsgCmd = modActionsMsgCmd({ return; } + const authorMember = await resolveMessageMember(msg); const memberToUnmute = await resolveMember(pluginData.client, pluginData.guild, user.id); const mutesPlugin = pluginData.getPlugin(MutesPlugin); const hasMuteRole = memberToUnmute && mutesPlugin.hasMutedRole(memberToUnmute); @@ -67,7 +68,7 @@ export const UnmuteMsgCmd = modActionsMsgCmd({ const reply = await waitForButtonConfirm( msg, { content: "User not on server, forceunmute instead?" }, - { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, + { confirmText: "Yes", cancelText: "No", restrictToId: authorMember.id }, ); if (!reply) { @@ -78,13 +79,13 @@ export const UnmuteMsgCmd = modActionsMsgCmd({ } // Make sure we're allowed to unmute this member - if (memberToUnmute && !canActOn(pluginData, msg.member, memberToUnmute)) { + if (memberToUnmute && !canActOn(pluginData, authorMember, memberToUnmute)) { pluginData.state.common.sendErrorMessage(msg, "Cannot unmute: insufficient permissions"); return; } // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; + let mod = authorMember; let ppId: string | undefined; if (args.mod) { diff --git a/backend/src/plugins/ModActions/commands/warn/WarnMsgCmd.ts b/backend/src/plugins/ModActions/commands/warn/WarnMsgCmd.ts index 7e07b2f9..91ea058a 100644 --- a/backend/src/plugins/ModActions/commands/warn/WarnMsgCmd.ts +++ b/backend/src/plugins/ModActions/commands/warn/WarnMsgCmd.ts @@ -1,5 +1,5 @@ import { commandTypeHelpers as ct } from "../../../../commandTypes.js"; -import { canActOn, hasPermission } from "../../../../pluginUtils.js"; +import { canActOn, hasPermission, resolveMessageMember } from "../../../../pluginUtils.js"; import { errorMessage, resolveMember, resolveUser } from "../../../../utils.js"; import { isBanned } from "../../functions/isBanned.js"; import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs.js"; @@ -27,6 +27,7 @@ export const WarnMsgCmd = modActionsMsgCmd({ return; } + const authorMember = await resolveMessageMember(msg); const memberToWarn = await resolveMember(pluginData.client, pluginData.guild, user.id); if (!memberToWarn) { @@ -41,13 +42,13 @@ export const WarnMsgCmd = modActionsMsgCmd({ } // Make sure we're allowed to warn this member - if (!canActOn(pluginData, msg.member, memberToWarn)) { + if (!canActOn(pluginData, authorMember, memberToWarn)) { await pluginData.state.common.sendErrorMessage(msg, "Cannot warn: insufficient permissions"); return; } // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; + let mod = authorMember; if (args.mod) { if (!(await hasPermission(pluginData, "can_act_as_other", { message: msg }))) { msg.channel.send(errorMessage("You don't have permission to use -mod")); diff --git a/backend/src/plugins/ModActions/types.ts b/backend/src/plugins/ModActions/types.ts index c7dcc6e9..f7e52bfc 100644 --- a/backend/src/plugins/ModActions/types.ts +++ b/backend/src/plugins/ModActions/types.ts @@ -8,7 +8,7 @@ import { guildPluginSlashGroup, pluginUtils, } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { Queue } from "../../Queue.js"; import { GuildCases } from "../../data/GuildCases.js"; import { GuildLogs } from "../../data/GuildLogs.js"; diff --git a/backend/src/plugins/Mutes/types.ts b/backend/src/plugins/Mutes/types.ts index 4a4c01de..94d9a499 100644 --- a/backend/src/plugins/Mutes/types.ts +++ b/backend/src/plugins/Mutes/types.ts @@ -1,7 +1,7 @@ import { GuildMember } from "discord.js"; import { EventEmitter } from "events"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildArchives } from "../../data/GuildArchives.js"; import { GuildCases } from "../../data/GuildCases.js"; import { GuildLogs } from "../../data/GuildLogs.js"; diff --git a/backend/src/plugins/NameHistory/types.ts b/backend/src/plugins/NameHistory/types.ts index d36763b7..259f4b7b 100644 --- a/backend/src/plugins/NameHistory/types.ts +++ b/backend/src/plugins/NameHistory/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { Queue } from "../../Queue.js"; import { GuildNicknameHistory } from "../../data/GuildNicknameHistory.js"; import { UsernameHistory } from "../../data/UsernameHistory.js"; diff --git a/backend/src/plugins/Persist/types.ts b/backend/src/plugins/Persist/types.ts index 6ce5d1df..1c6fc714 100644 --- a/backend/src/plugins/Persist/types.ts +++ b/backend/src/plugins/Persist/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildLogs } from "../../data/GuildLogs.js"; import { GuildPersistedData } from "../../data/GuildPersistedData.js"; import { zSnowflake } from "../../utils.js"; diff --git a/backend/src/plugins/Phisherman/types.ts b/backend/src/plugins/Phisherman/types.ts index d21eb38e..77c9bfbc 100644 --- a/backend/src/plugins/Phisherman/types.ts +++ b/backend/src/plugins/Phisherman/types.ts @@ -1,5 +1,5 @@ import { BasePluginType } from "knub"; -import z from "zod"; +import z from "zod/v4"; export const zPhishermanConfig = z.strictObject({ api_key: z.string().max(255).nullable(), diff --git a/backend/src/plugins/PingableRoles/types.ts b/backend/src/plugins/PingableRoles/types.ts index b79ac27f..4e9c623d 100644 --- a/backend/src/plugins/PingableRoles/types.ts +++ b/backend/src/plugins/PingableRoles/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildPingableRoles } from "../../data/GuildPingableRoles.js"; import { PingableRole } from "../../data/entities/PingableRole.js"; import { CommonPlugin } from "../Common/CommonPlugin.js"; diff --git a/backend/src/plugins/Post/types.ts b/backend/src/plugins/Post/types.ts index 96b11e71..73fcbeca 100644 --- a/backend/src/plugins/Post/types.ts +++ b/backend/src/plugins/Post/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildLogs } from "../../data/GuildLogs.js"; import { GuildSavedMessages } from "../../data/GuildSavedMessages.js"; import { GuildScheduledPosts } from "../../data/GuildScheduledPosts.js"; diff --git a/backend/src/plugins/Post/util/actualPostCmd.ts b/backend/src/plugins/Post/util/actualPostCmd.ts index a5831510..04c6def0 100644 --- a/backend/src/plugins/Post/util/actualPostCmd.ts +++ b/backend/src/plugins/Post/util/actualPostCmd.ts @@ -27,13 +27,13 @@ export async function actualPostCmd( "repeat-times"?: number; } = {}, ) { - if (!targetChannel.isTextBased()) { - msg.channel.send(errorMessage("Specified channel is not a text-based channel")); + if (!targetChannel.isSendable()) { + msg.reply(errorMessage("Specified channel is not a sendable channel")); return; } if (content == null && msg.attachments.size === 0) { - msg.channel.send(errorMessage("Message content or attachment required")); + msg.reply(errorMessage("Message content or attachment required")); return; } diff --git a/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts b/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts index e3144123..f864153e 100644 --- a/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts +++ b/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts @@ -4,6 +4,7 @@ import { canUseEmoji, isDiscordAPIError, isValidEmoji, noop, trimPluginDescripti import { canReadChannel } from "../../../utils/canReadChannel.js"; import { TReactionRolePair, reactionRolesCmd } from "../types.js"; import { applyReactionRoleReactionsToMessage } from "../util/applyReactionRoleReactionsToMessage.js"; +import { resolveMessageMember } from "../../../pluginUtils.js"; const CLEAR_ROLES_EMOJI = "❌"; @@ -32,7 +33,8 @@ export const InitReactionRolesCmd = reactionRolesCmd({ }, async run({ message: msg, args, pluginData }) { - if (!canReadChannel(args.message.channel, msg.member)) { + const member = await resolveMessageMember(msg); + if (!canReadChannel(args.message.channel, member)) { void pluginData.state.common.sendErrorMessage( msg, "You can't add reaction roles to channels you can't see yourself", diff --git a/backend/src/plugins/ReactionRoles/types.ts b/backend/src/plugins/ReactionRoles/types.ts index b5506c30..89418363 100644 --- a/backend/src/plugins/ReactionRoles/types.ts +++ b/backend/src/plugins/ReactionRoles/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { Queue } from "../../Queue.js"; import { GuildReactionRoles } from "../../data/GuildReactionRoles.js"; import { GuildSavedMessages } from "../../data/GuildSavedMessages.js"; diff --git a/backend/src/plugins/Reminders/types.ts b/backend/src/plugins/Reminders/types.ts index d3b3c05f..8583033a 100644 --- a/backend/src/plugins/Reminders/types.ts +++ b/backend/src/plugins/Reminders/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildReminders } from "../../data/GuildReminders.js"; import { CommonPlugin } from "../Common/CommonPlugin.js"; diff --git a/backend/src/plugins/RoleButtons/types.ts b/backend/src/plugins/RoleButtons/types.ts index 4f2a5495..64e0c7e9 100644 --- a/backend/src/plugins/RoleButtons/types.ts +++ b/backend/src/plugins/RoleButtons/types.ts @@ -1,6 +1,6 @@ import { ButtonStyle } from "discord.js"; import { BasePluginType, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildRoleButtons } from "../../data/GuildRoleButtons.js"; import { zBoundedCharacters, zBoundedRecord, zMessageContent, zSnowflake } from "../../utils.js"; import { CommonPlugin } from "../Common/CommonPlugin.js"; diff --git a/backend/src/plugins/RoleManager/types.ts b/backend/src/plugins/RoleManager/types.ts index 1954d994..2ed0bdd7 100644 --- a/backend/src/plugins/RoleManager/types.ts +++ b/backend/src/plugins/RoleManager/types.ts @@ -1,5 +1,5 @@ import { BasePluginType } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildRoleQueue } from "../../data/GuildRoleQueue.js"; export const zRoleManagerConfig = z.strictObject({}); diff --git a/backend/src/plugins/Roles/commands/AddRoleCmd.ts b/backend/src/plugins/Roles/commands/AddRoleCmd.ts index d984082f..76fcdbd1 100644 --- a/backend/src/plugins/Roles/commands/AddRoleCmd.ts +++ b/backend/src/plugins/Roles/commands/AddRoleCmd.ts @@ -1,6 +1,6 @@ import { GuildChannel } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js"; -import { canActOn } from "../../../pluginUtils.js"; +import { canActOn, resolveMessageMember } from "../../../pluginUtils.js"; import { resolveRoleId, verboseUserMention } from "../../../utils.js"; import { LogsPlugin } from "../../Logs/LogsPlugin.js"; import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin.js"; @@ -17,7 +17,8 @@ export const AddRoleCmd = rolesCmd({ }, async run({ message: msg, args, pluginData }) { - if (!canActOn(pluginData, msg.member, args.member, true)) { + const member = await resolveMessageMember(msg); + if (!canActOn(pluginData, member, args.member, true)) { void pluginData.state.common.sendErrorMessage(msg, "Cannot add roles to this user: insufficient permissions"); return; } diff --git a/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts b/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts index 6d008f32..7ad2432d 100644 --- a/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts +++ b/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts @@ -1,7 +1,7 @@ import { GuildMember } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js"; import { logger } from "../../../logger.js"; -import { canActOn } from "../../../pluginUtils.js"; +import { canActOn, resolveMessageMember } from "../../../pluginUtils.js"; import { resolveMember, resolveRoleId, successMessage } from "../../../utils.js"; import { LogsPlugin } from "../../Logs/LogsPlugin.js"; import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin.js"; @@ -19,6 +19,8 @@ export const MassAddRoleCmd = rolesCmd({ async run({ message: msg, args, pluginData }) { msg.channel.send(`Resolving members...`); + const authorMember = await resolveMessageMember(msg); + const members: GuildMember[] = []; const unknownMembers: string[] = []; for (const memberId of args.members) { @@ -28,7 +30,7 @@ export const MassAddRoleCmd = rolesCmd({ } for (const member of members) { - if (!canActOn(pluginData, msg.member, member, true)) { + if (!canActOn(pluginData, authorMember, member, true)) { void pluginData.state.common.sendErrorMessage( msg, "Cannot add roles to 1 or more specified members: insufficient permissions", diff --git a/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts b/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts index 169cf413..8b17eecc 100644 --- a/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts +++ b/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts @@ -1,6 +1,6 @@ import { GuildMember } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js"; -import { canActOn } from "../../../pluginUtils.js"; +import { canActOn, resolveMessageMember } from "../../../pluginUtils.js"; import { resolveMember, resolveRoleId, successMessage } from "../../../utils.js"; import { LogsPlugin } from "../../Logs/LogsPlugin.js"; import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin.js"; @@ -18,6 +18,8 @@ export const MassRemoveRoleCmd = rolesCmd({ async run({ message: msg, args, pluginData }) { msg.channel.send(`Resolving members...`); + const authorMember = await resolveMessageMember(msg); + const members: GuildMember[] = []; const unknownMembers: string[] = []; for (const memberId of args.members) { @@ -27,7 +29,7 @@ export const MassRemoveRoleCmd = rolesCmd({ } for (const member of members) { - if (!canActOn(pluginData, msg.member, member, true)) { + if (!canActOn(pluginData, authorMember, member, true)) { void pluginData.state.common.sendErrorMessage( msg, "Cannot add roles to 1 or more specified members: insufficient permissions", diff --git a/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts b/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts index 1a4a4202..a1367a9e 100644 --- a/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts +++ b/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts @@ -1,6 +1,6 @@ import { GuildChannel } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js"; -import { canActOn } from "../../../pluginUtils.js"; +import { canActOn, resolveMessageMember } from "../../../pluginUtils.js"; import { resolveRoleId, verboseUserMention } from "../../../utils.js"; import { LogsPlugin } from "../../Logs/LogsPlugin.js"; import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin.js"; @@ -17,7 +17,8 @@ export const RemoveRoleCmd = rolesCmd({ }, async run({ message: msg, args, pluginData }) { - if (!canActOn(pluginData, msg.member, args.member, true)) { + const authorMember = await resolveMessageMember(msg); + if (!canActOn(pluginData, authorMember, args.member, true)) { void pluginData.state.common.sendErrorMessage( msg, "Cannot remove roles from this user: insufficient permissions", diff --git a/backend/src/plugins/Roles/types.ts b/backend/src/plugins/Roles/types.ts index 557a1d86..35ac1556 100644 --- a/backend/src/plugins/Roles/types.ts +++ b/backend/src/plugins/Roles/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildLogs } from "../../data/GuildLogs.js"; import { CommonPlugin } from "../Common/CommonPlugin.js"; diff --git a/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts b/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts index eacd9b2b..0a3d268a 100644 --- a/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts +++ b/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts @@ -6,6 +6,7 @@ import { findMatchingRoles } from "../util/findMatchingRoles.js"; import { getApplyingEntries } from "../util/getApplyingEntries.js"; import { normalizeRoleNames } from "../util/normalizeRoleNames.js"; import { splitRoleNames } from "../util/splitRoleNames.js"; +import { resolveMessageMember } from "../../../pluginUtils.js"; export const RoleAddCmd = selfGrantableRolesCmd({ trigger: ["role", "role add"], @@ -49,8 +50,10 @@ export const RoleAddCmd = selfGrantableRolesCmd({ return; } + const authorMember = await resolveMessageMember(msg); + // Grant the roles - const newRoleIds = new Set([...rolesToAdd.keys(), ...msg.member.roles.cache.keys()]); + const newRoleIds = new Set([...rolesToAdd.keys(), ...authorMember.roles.cache.keys()]); // Remove extra roles (max_roles) for each entry const skipped: Set = new Set(); @@ -69,7 +72,7 @@ export const RoleAddCmd = selfGrantableRolesCmd({ newRoleIds.delete(roleId); rolesToAdd.delete(roleId); - if (msg.member.roles.cache.has(roleId as Snowflake)) { + if (authorMember.roles.cache.has(roleId as Snowflake)) { removed.add(pluginData.guild.roles.cache.get(roleId as Snowflake)!); } else { skipped.add(pluginData.guild.roles.cache.get(roleId as Snowflake)!); @@ -80,7 +83,7 @@ export const RoleAddCmd = selfGrantableRolesCmd({ } try { - await msg.member.edit({ + await authorMember.edit({ roles: Array.from(newRoleIds) as Snowflake[], }); } catch { diff --git a/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts b/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts index b6e8d8ff..611b75bb 100644 --- a/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts +++ b/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts @@ -6,6 +6,7 @@ import { findMatchingRoles } from "../util/findMatchingRoles.js"; import { getApplyingEntries } from "../util/getApplyingEntries.js"; import { normalizeRoleNames } from "../util/normalizeRoleNames.js"; import { splitRoleNames } from "../util/splitRoleNames.js"; +import { resolveMessageMember } from "../../../pluginUtils.js"; export const RoleRemoveCmd = selfGrantableRolesCmd({ trigger: "role remove", @@ -32,12 +33,14 @@ export const RoleRemoveCmd = selfGrantableRolesCmd({ ); const roleIdsToRemove = rolesToRemove.map((r) => r.id); + const authorMember = await resolveMessageMember(msg); + // Remove the roles if (rolesToRemove.length) { - const newRoleIds = msg.member.roles.cache.filter((role) => !roleIdsToRemove.includes(role.id)); + const newRoleIds = authorMember.roles.cache.filter((role) => !roleIdsToRemove.includes(role.id)); try { - await msg.member.edit({ + await authorMember.edit({ roles: newRoleIds, }); diff --git a/backend/src/plugins/SelfGrantableRoles/types.ts b/backend/src/plugins/SelfGrantableRoles/types.ts index 891672e9..462d3fa9 100644 --- a/backend/src/plugins/SelfGrantableRoles/types.ts +++ b/backend/src/plugins/SelfGrantableRoles/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, CooldownManager, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { zBoundedCharacters, zBoundedRecord } from "../../utils.js"; import { CommonPlugin } from "../Common/CommonPlugin.js"; diff --git a/backend/src/plugins/Slowmode/types.ts b/backend/src/plugins/Slowmode/types.ts index a8ea070b..babacc79 100644 --- a/backend/src/plugins/Slowmode/types.ts +++ b/backend/src/plugins/Slowmode/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildLogs } from "../../data/GuildLogs.js"; import { GuildSavedMessages } from "../../data/GuildSavedMessages.js"; import { GuildSlowmodes } from "../../data/GuildSlowmodes.js"; diff --git a/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts b/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts index 4c26e812..b7d71c1a 100644 --- a/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts +++ b/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts @@ -24,7 +24,7 @@ export async function actualDisableSlowmodeCmd(msg: Message, args, pluginData) { return; } - const initMsg = await msg.channel.send("Disabling slowmode..."); + const initMsg = await msg.reply("Disabling slowmode..."); // Disable bot-maintained slowmode let failedUsers: string[] = []; diff --git a/backend/src/plugins/Spam/types.ts b/backend/src/plugins/Spam/types.ts index ea7b2406..37a13320 100644 --- a/backend/src/plugins/Spam/types.ts +++ b/backend/src/plugins/Spam/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildArchives } from "../../data/GuildArchives.js"; import { GuildLogs } from "../../data/GuildLogs.js"; import { GuildMutes } from "../../data/GuildMutes.js"; diff --git a/backend/src/plugins/Starboard/types.ts b/backend/src/plugins/Starboard/types.ts index 336cd297..1e8e2850 100644 --- a/backend/src/plugins/Starboard/types.ts +++ b/backend/src/plugins/Starboard/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildSavedMessages } from "../../data/GuildSavedMessages.js"; import { GuildStarboardMessages } from "../../data/GuildStarboardMessages.js"; import { GuildStarboardReactions } from "../../data/GuildStarboardReactions.js"; diff --git a/backend/src/plugins/Tags/commands/TagEvalCmd.ts b/backend/src/plugins/Tags/commands/TagEvalCmd.ts index 204f5e8c..f55eea32 100644 --- a/backend/src/plugins/Tags/commands/TagEvalCmd.ts +++ b/backend/src/plugins/Tags/commands/TagEvalCmd.ts @@ -5,6 +5,7 @@ import { TemplateParseError } from "../../../templateFormatter.js"; import { memberToTemplateSafeMember, userToTemplateSafeUser } from "../../../utils/templateSafeObjects.js"; import { tagsCmd } from "../types.js"; import { renderTagBody } from "../util/renderTagBody.js"; +import { resolveMessageMember } from "../../../pluginUtils.js"; export const TagEvalCmd = tagsCmd({ trigger: "tag eval", @@ -15,14 +16,15 @@ export const TagEvalCmd = tagsCmd({ }, async run({ message: msg, args, pluginData }) { + const authorMember = await resolveMessageMember(msg); try { const rendered = (await renderTagBody( pluginData, args.body, [], { - member: memberToTemplateSafeMember(msg.member), - user: userToTemplateSafeUser(msg.member.user), + member: memberToTemplateSafeMember(authorMember), + user: userToTemplateSafeUser(msg.author), }, { member: msg.member }, )) as MessageCreateOptions; diff --git a/backend/src/plugins/Tags/types.ts b/backend/src/plugins/Tags/types.ts index 6a730e75..51798334 100644 --- a/backend/src/plugins/Tags/types.ts +++ b/backend/src/plugins/Tags/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildArchives } from "../../data/GuildArchives.js"; import { GuildLogs } from "../../data/GuildLogs.js"; import { GuildSavedMessages } from "../../data/GuildSavedMessages.js"; diff --git a/backend/src/plugins/TimeAndDate/types.ts b/backend/src/plugins/TimeAndDate/types.ts index bcdc4b09..59885cf9 100644 --- a/backend/src/plugins/TimeAndDate/types.ts +++ b/backend/src/plugins/TimeAndDate/types.ts @@ -1,6 +1,6 @@ import { BasePluginType, guildPluginMessageCommand, pluginUtils } from "knub"; import { U } from "ts-toolbelt"; -import z from "zod"; +import z from "zod/v4"; import { GuildMemberTimezones } from "../../data/GuildMemberTimezones.js"; import { keys } from "../../utils.js"; import { zValidTimezone } from "../../utils/zValidTimezone.js"; diff --git a/backend/src/plugins/UsernameSaver/types.ts b/backend/src/plugins/UsernameSaver/types.ts index 4828db6e..cf7417df 100644 --- a/backend/src/plugins/UsernameSaver/types.ts +++ b/backend/src/plugins/UsernameSaver/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { Queue } from "../../Queue.js"; import { UsernameHistory } from "../../data/UsernameHistory.js"; diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index 3fa32e72..17fd8252 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -15,7 +15,7 @@ import { AboutCmd } from "./commands/AboutCmd.js"; import { AvatarCmd } from "./commands/AvatarCmd.js"; import { BanSearchCmd } from "./commands/BanSearchCmd.js"; import { ChannelInfoCmd } from "./commands/ChannelInfoCmd.js"; -import { CleanCmd, cleanCmd } from "./commands/CleanCmd.js"; +import { CleanCmd } from "./commands/CleanCmd.js"; import { ContextCmd } from "./commands/ContextCmd.js"; import { EmojiInfoCmd } from "./commands/EmojiInfoCmd.js"; import { HelpCmd } from "./commands/HelpCmd.js"; @@ -43,6 +43,8 @@ import { hasPermission } from "./functions/hasPermission.js"; import { activeReloads } from "./guildReloads.js"; import { refreshMembersIfNeeded } from "./refreshMembers.js"; import { UtilityPluginType, zUtilityConfig } from "./types.js"; +import { cleanMessages } from "./functions/cleanMessages.js"; +import { fetchChannelMessagesToClean } from "./functions/fetchChannelMessagesToClean.js"; const defaultOptions: PluginOptions = { config: { @@ -158,7 +160,8 @@ export const UtilityPlugin = guildPlugin()({ public(pluginData) { return { - clean: makePublicFn(pluginData, cleanCmd), + fetchChannelMessagesToClean: makePublicFn(pluginData, fetchChannelMessagesToClean), + cleanMessages: makePublicFn(pluginData, cleanMessages), userInfo: (userId: Snowflake) => getUserInfoEmbed(pluginData, userId, false), hasPermission: makePublicFn(pluginData, hasPermission), }; diff --git a/backend/src/plugins/Utility/commands/CleanCmd.ts b/backend/src/plugins/Utility/commands/CleanCmd.ts index 9515394c..aeb6126a 100644 --- a/backend/src/plugins/Utility/commands/CleanCmd.ts +++ b/backend/src/plugins/Utility/commands/CleanCmd.ts @@ -1,62 +1,14 @@ -import { Message, ModalSubmitInteraction, Snowflake, TextChannel, User } from "discord.js"; -import { GuildPluginData } from "knub"; -import { allowTimeout } from "../../../RegExpRunner.js"; +import { Message, Snowflake } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js"; -import { LogType } from "../../../data/LogType.js"; -import { SavedMessage } from "../../../data/entities/SavedMessage.js"; -import { humanizeDurationShort } from "../../../humanizeDuration.js"; -import { getBaseUrl } from "../../../pluginUtils.js"; +import { ContextResponse, deleteContextResponse } from "../../../pluginUtils.js"; import { ModActionsPlugin } from "../../../plugins/ModActions/ModActionsPlugin.js"; -import { DAYS, SECONDS, chunkArray, getInviteCodesInString, noop } from "../../../utils.js"; -import { LogsPlugin } from "../../Logs/LogsPlugin.js"; -import { UtilityPluginType, utilityCmd } from "../types.js"; +import { SECONDS, noop } from "../../../utils.js"; +import { cleanMessages } from "../functions/cleanMessages.js"; +import { fetchChannelMessagesToClean } from "../functions/fetchChannelMessagesToClean.js"; +import { utilityCmd } from "../types.js"; -const MAX_CLEAN_COUNT = 300; -const MAX_CLEAN_TIME = 1 * DAYS; -const MAX_CLEAN_API_REQUESTS = 20; const CLEAN_COMMAND_DELETE_DELAY = 10 * SECONDS; -export async function cleanMessages( - pluginData: GuildPluginData, - channel: TextChannel, - savedMessages: SavedMessage[], - mod: User, -) { - pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, savedMessages[0].id); - pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, savedMessages[0].id); - - // Delete & archive in ID order - savedMessages = Array.from(savedMessages).sort((a, b) => (a.id > b.id ? 1 : -1)); - const idsToDelete = savedMessages.map((m) => m.id) as Snowflake[]; - - // Make sure the deletions aren't double logged - idsToDelete.forEach((id) => pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, id)); - pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, idsToDelete[0]); - - // Actually delete the messages (in chunks of 100) - - const chunks = chunkArray(idsToDelete, 100); - await Promise.all( - chunks.map((chunk) => - Promise.all([channel.bulkDelete(chunk), pluginData.state.savedMessages.markBulkAsDeleted(chunk)]), - ), - ); - - // Create an archive - const archiveId = await pluginData.state.archives.createFromSavedMessages(savedMessages, pluginData.guild); - const baseUrl = getBaseUrl(pluginData); - const archiveUrl = pluginData.state.archives.getUrl(baseUrl, archiveId); - - pluginData.getPlugin(LogsPlugin).logClean({ - mod, - channel, - count: savedMessages.length, - archiveUrl, - }); - - return { archiveUrl }; -} - const opts = { user: ct.userId({ option: true, shortcut: "u" }), channel: ct.channelId({ option: true, shortcut: "c" }), @@ -67,188 +19,6 @@ const opts = { "to-id": ct.anyId({ option: true, shortcut: "id" }), }; -export interface CleanArgs { - count: number; - update?: boolean; - user?: string; - channel?: string; - bots?: boolean; - "delete-pins"?: boolean; - "has-invites"?: boolean; - match?: RegExp; - "to-id"?: string; - "response-interaction"?: ModalSubmitInteraction; -} - -export async function cleanCmd(pluginData: GuildPluginData, args: CleanArgs | any, msg) { - if (args.count > MAX_CLEAN_COUNT || args.count <= 0) { - void pluginData.state.common.sendErrorMessage( - msg, - `Clean count must be between 1 and ${MAX_CLEAN_COUNT}`, - undefined, - args["response-interaction"], - ); - return; - } - - const targetChannel = args.channel ? pluginData.guild.channels.cache.get(args.channel as Snowflake) : msg.channel; - if (!targetChannel?.isTextBased()) { - void pluginData.state.common.sendErrorMessage( - msg, - `Invalid channel specified`, - undefined, - args["response-interaction"], - ); - return; - } - - if (targetChannel.id !== msg.channel.id) { - const configForTargetChannel = await pluginData.config.getMatchingConfig({ - userId: msg.author.id, - member: msg.member, - channelId: targetChannel.id, - categoryId: targetChannel.parentId, - }); - if (configForTargetChannel.can_clean !== true) { - void pluginData.state.common.sendErrorMessage( - msg, - `Missing permissions to use clean on that channel`, - undefined, - args["response-interaction"], - ); - return; - } - } - - let cleaningMessage: Message | undefined = undefined; - if (!args["response-interaction"]) { - cleaningMessage = await msg.channel.send("Cleaning..."); - } - - const messagesToClean: Message[] = []; - let beforeId = msg.id; - const timeCutoff = msg.createdTimestamp - MAX_CLEAN_TIME; - const upToMsgId = args["to-id"]; - let foundId = false; - - const deletePins = args["delete-pins"] != null ? args["delete-pins"] : false; - let pinIds: Set = new Set(); - if (!deletePins) { - pinIds = new Set((await msg.channel.messages.fetchPinned()).keys()); - } - - let note: string | null = null; - let requests = 0; - while (messagesToClean.length < args.count) { - const potentialMessages = await targetChannel.messages.fetch({ - before: beforeId, - limit: 100, - }); - if (potentialMessages.size === 0) break; - - requests++; - - const filtered: Message[] = []; - for (const message of potentialMessages.values()) { - const contentString = message.content || ""; - if (args.user && message.author.id !== args.user) continue; - if (args.bots && !message.author.bot) continue; - if (!deletePins && pinIds.has(message.id)) continue; - if (args["has-invites"] && getInviteCodesInString(contentString).length === 0) continue; - if (upToMsgId != null && message.id < upToMsgId) { - foundId = true; - break; - } - if (message.createdTimestamp < timeCutoff) continue; - if (args.match && !(await pluginData.state.regexRunner.exec(args.match, contentString).catch(allowTimeout))) { - continue; - } - - filtered.push(message); - } - const remaining = args.count - messagesToClean.length; - const withoutOverflow = filtered.slice(0, remaining); - messagesToClean.push(...withoutOverflow); - - beforeId = potentialMessages.lastKey()!; - - if (foundId) { - break; - } - - if (messagesToClean.length < args.count) { - if (potentialMessages.last()!.createdTimestamp < timeCutoff) { - note = `stopped looking after reaching ${humanizeDurationShort(MAX_CLEAN_TIME)} old messages`; - break; - } - - if (requests >= MAX_CLEAN_API_REQUESTS) { - note = `stopped looking after ${requests * 100} messages`; - break; - } - } - } - - let responseMsg: Message | undefined; - if (messagesToClean.length > 0) { - // Save to-be-deleted messages that were missing from the database - const existingStored = await pluginData.state.savedMessages.getMultiple(messagesToClean.map((m) => m.id)); - const alreadyStored = existingStored.map((stored) => stored.id); - const messagesToStore = messagesToClean.filter((potentialMsg) => !alreadyStored.includes(potentialMsg.id)); - await pluginData.state.savedMessages.createFromMessages(messagesToStore); - - const savedMessagesToClean = await pluginData.state.savedMessages.getMultiple(messagesToClean.map((m) => m.id)); - const cleanResult = await cleanMessages(pluginData, targetChannel, savedMessagesToClean, msg.author); - - let responseText = `Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`; - if (note) { - responseText += ` (${note})`; - } - if (targetChannel.id !== msg.channel.id) { - responseText += ` in <#${targetChannel.id}>: ${cleanResult.archiveUrl}`; - } - - if (args.update) { - const modActions = pluginData.getPlugin(ModActionsPlugin); - const channelId = targetChannel.id !== msg.channel.id ? targetChannel.id : msg.channel.id; - const updateMessage = `Cleaned ${messagesToClean.length} ${ - messagesToClean.length === 1 ? "message" : "messages" - } in <#${channelId}>: ${cleanResult.archiveUrl}`; - if (typeof args.update === "number") { - modActions.updateCase(msg, args.update, updateMessage); - } else { - modActions.updateCase(msg, null, updateMessage); - } - } - - responseMsg = await pluginData.state.common.sendSuccessMessage( - msg, - responseText, - undefined, - args["response-interaction"], - ); - } else { - const responseText = `Found no messages to clean${note ? ` (${note})` : ""}!`; - responseMsg = await pluginData.state.common.sendErrorMessage( - msg, - responseText, - undefined, - args["response-interaction"], - ); - } - - cleaningMessage?.delete(); - - if (targetChannel.id === msg.channel.id) { - // Delete the !clean command and the bot response if a different channel wasn't specified - // (so as not to spam the cleaned channel with the command itself) - msg.delete().catch(noop); - setTimeout(() => { - responseMsg?.delete().catch(noop); - }, CLEAN_COMMAND_DELETE_DELAY); - } -} - export const CleanCmd = utilityCmd({ trigger: ["clean", "clear"], description: "Remove a number of recent messages", @@ -271,6 +41,108 @@ export const CleanCmd = utilityCmd({ ], async run({ message: msg, args, pluginData }) { - cleanCmd(pluginData, args, msg); + const targetChannel = args.channel ? pluginData.guild.channels.cache.get(args.channel as Snowflake) : msg.channel; + if (!targetChannel?.isTextBased()) { + void pluginData.state.common.sendErrorMessage( + msg, + `Invalid channel specified`, + undefined, + args["response-interaction"], + ); + return; + } + + if (targetChannel.id !== msg.channel.id) { + const configForTargetChannel = await pluginData.config.getMatchingConfig({ + userId: msg.author.id, + member: msg.member, + channelId: targetChannel.id, + categoryId: targetChannel.parentId, + }); + if (configForTargetChannel.can_clean !== true) { + void pluginData.state.common.sendErrorMessage( + msg, + `Missing permissions to use clean on that channel`, + undefined, + args["response-interaction"], + ); + return; + } + } + + let cleaningMessage: Message | undefined = undefined; + if (!args["response-interaction"]) { + cleaningMessage = await msg.channel.send("Cleaning..."); + } + + const fetchMessagesResult = await fetchChannelMessagesToClean(pluginData, targetChannel, { + beforeId: msg.id, + count: args.count, + authorId: args.user, + includePins: args["delete-pins"], + onlyBotMessages: args.bots, + onlyWithInvites: args["has-invites"], + upToId: args["to-id"], + matchContent: args.match, + }); + if ("error" in fetchMessagesResult) { + void pluginData.state.common.sendErrorMessage(msg, fetchMessagesResult.error); + return; + } + + const { messages: messagesToClean, note } = fetchMessagesResult; + + let responseMsg: ContextResponse | null = null; + if (messagesToClean.length > 0) { + const cleanResult = await cleanMessages(pluginData, targetChannel, messagesToClean, msg.author); + + let responseText = `Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`; + if (note) { + responseText += ` (${note})`; + } + if (targetChannel.id !== msg.channel.id) { + responseText += ` in <#${targetChannel.id}>: ${cleanResult.archiveUrl}`; + } + + if (args.update) { + const modActions = pluginData.getPlugin(ModActionsPlugin); + const channelId = targetChannel.id !== msg.channel.id ? targetChannel.id : msg.channel.id; + const updateMessage = `Cleaned ${messagesToClean.length} ${ + messagesToClean.length === 1 ? "message" : "messages" + } in <#${channelId}>: ${cleanResult.archiveUrl}`; + if (typeof args.update === "number") { + modActions.updateCase(msg, args.update, updateMessage); + } else { + modActions.updateCase(msg, null, updateMessage); + } + } + + responseMsg = await pluginData.state.common.sendSuccessMessage( + msg, + responseText, + undefined, + args["response-interaction"], + ); + } else { + const responseText = `Found no messages to clean${note ? ` (${note})` : ""}!`; + responseMsg = await pluginData.state.common.sendErrorMessage( + msg, + responseText, + undefined, + args["response-interaction"], + ); + } + + cleaningMessage?.delete(); + + if (targetChannel.id === msg.channel.id) { + // Delete the !clean command and the bot response if a different channel wasn't specified + // (so as not to spam the cleaned channel with the command itself) + msg.delete().catch(noop); + setTimeout(() => { + deleteContextResponse(responseMsg).catch(noop); + responseMsg?.delete().catch(noop); + }, CLEAN_COMMAND_DELETE_DELAY); + } }, }); diff --git a/backend/src/plugins/Utility/commands/ContextCmd.ts b/backend/src/plugins/Utility/commands/ContextCmd.ts index 295116ec..2af1219f 100644 --- a/backend/src/plugins/Utility/commands/ContextCmd.ts +++ b/backend/src/plugins/Utility/commands/ContextCmd.ts @@ -3,6 +3,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes.js"; import { messageLink } from "../../../utils.js"; import { canReadChannel } from "../../../utils/canReadChannel.js"; import { utilityCmd } from "../types.js"; +import { resolveMessageMember } from "../../../pluginUtils.js"; export const ContextCmd = utilityCmd({ trigger: "context", @@ -29,7 +30,8 @@ export const ContextCmd = utilityCmd({ const channel = args.channel ?? args.message.channel; const messageId = args.messageId ?? args.message.messageId; - if (!canReadChannel(channel, msg.member)) { + const authorMember = await resolveMessageMember(msg); + if (!canReadChannel(channel, authorMember)) { void pluginData.state.common.sendErrorMessage(msg, "Message context not found"); return; } diff --git a/backend/src/plugins/Utility/commands/InfoCmd.ts b/backend/src/plugins/Utility/commands/InfoCmd.ts index f286de40..e9df7104 100644 --- a/backend/src/plugins/Utility/commands/InfoCmd.ts +++ b/backend/src/plugins/Utility/commands/InfoCmd.ts @@ -15,6 +15,7 @@ import { getServerInfoEmbed } from "../functions/getServerInfoEmbed.js"; import { getSnowflakeInfoEmbed } from "../functions/getSnowflakeInfoEmbed.js"; import { getUserInfoEmbed } from "../functions/getUserInfoEmbed.js"; import { utilityCmd } from "../types.js"; +import { resolveMessageMember } from "../../../pluginUtils.js"; export const InfoCmd = utilityCmd({ trigger: "info", @@ -77,7 +78,8 @@ export const InfoCmd = utilityCmd({ if (userCfg.can_messageinfo) { const messageTarget = await resolveMessageTarget(pluginData, value); if (messageTarget) { - if (canReadChannel(messageTarget.channel, message.member)) { + const authorMember = await resolveMessageMember(message); + if (canReadChannel(messageTarget.channel, authorMember)) { const embed = await getMessageInfoEmbed(pluginData, messageTarget.channel.id, messageTarget.messageId); if (embed) { message.channel.send({ embeds: [embed] }); diff --git a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts index 8abff0f1..f519d8ab 100644 --- a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts +++ b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts @@ -1,4 +1,5 @@ import { commandTypeHelpers as ct } from "../../../commandTypes.js"; +import { resolveMessageMember } from "../../../pluginUtils.js"; import { canReadChannel } from "../../../utils/canReadChannel.js"; import { getMessageInfoEmbed } from "../functions/getMessageInfoEmbed.js"; import { utilityCmd } from "../types.js"; @@ -14,7 +15,8 @@ export const MessageInfoCmd = utilityCmd({ }, async run({ message, args, pluginData }) { - if (!canReadChannel(args.message.channel, message.member)) { + const messageMember = await resolveMessageMember(message); + if (!canReadChannel(args.message.channel, messageMember)) { void pluginData.state.common.sendErrorMessage(message, "Unknown message"); return; } diff --git a/backend/src/plugins/Utility/commands/NicknameCmd.ts b/backend/src/plugins/Utility/commands/NicknameCmd.ts index 8c00f784..360eb2c2 100644 --- a/backend/src/plugins/Utility/commands/NicknameCmd.ts +++ b/backend/src/plugins/Utility/commands/NicknameCmd.ts @@ -1,6 +1,6 @@ import { escapeBold } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js"; -import { canActOn } from "../../../pluginUtils.js"; +import { canActOn, resolveMessageMember } from "../../../pluginUtils.js"; import { errorMessage } from "../../../utils.js"; import { utilityCmd } from "../types.js"; @@ -25,7 +25,8 @@ export const NicknameCmd = utilityCmd({ return; } - if (msg.member.id !== args.member.id && !canActOn(pluginData, msg.member, args.member)) { + const authorMember = await resolveMessageMember(msg); + if (msg.author.id !== args.member.id && !canActOn(pluginData, authorMember, args.member)) { msg.channel.send(errorMessage("Cannot change nickname: insufficient permissions")); return; } diff --git a/backend/src/plugins/Utility/commands/NicknameResetCmd.ts b/backend/src/plugins/Utility/commands/NicknameResetCmd.ts index df4d89c3..b6eb34b0 100644 --- a/backend/src/plugins/Utility/commands/NicknameResetCmd.ts +++ b/backend/src/plugins/Utility/commands/NicknameResetCmd.ts @@ -1,5 +1,5 @@ import { commandTypeHelpers as ct } from "../../../commandTypes.js"; -import { canActOn } from "../../../pluginUtils.js"; +import { canActOn, resolveMessageMember } from "../../../pluginUtils.js"; import { errorMessage } from "../../../utils.js"; import { utilityCmd } from "../types.js"; @@ -14,7 +14,8 @@ export const NicknameResetCmd = utilityCmd({ }, async run({ message: msg, args, pluginData }) { - if (msg.member.id !== args.member.id && !canActOn(pluginData, msg.member, args.member)) { + const authorMember = await resolveMessageMember(msg); + if (msg.author.id !== args.member.id && !canActOn(pluginData, authorMember, args.member)) { msg.channel.send(errorMessage("Cannot reset nickname: insufficient permissions")); return; } diff --git a/backend/src/plugins/Utility/commands/SourceCmd.ts b/backend/src/plugins/Utility/commands/SourceCmd.ts index dde7d537..fcb8724f 100644 --- a/backend/src/plugins/Utility/commands/SourceCmd.ts +++ b/backend/src/plugins/Utility/commands/SourceCmd.ts @@ -1,6 +1,6 @@ import moment from "moment-timezone"; import { commandTypeHelpers as ct } from "../../../commandTypes.js"; -import { getBaseUrl } from "../../../pluginUtils.js"; +import { getBaseUrl, resolveMessageMember } from "../../../pluginUtils.js"; import { canReadChannel } from "../../../utils/canReadChannel.js"; import { utilityCmd } from "../types.js"; @@ -15,7 +15,8 @@ export const SourceCmd = utilityCmd({ }, async run({ message: cmdMessage, args, pluginData }) { - if (!canReadChannel(args.message.channel, cmdMessage.member)) { + const cmdAuthorMember = await resolveMessageMember(cmdMessage); + if (!canReadChannel(args.message.channel, cmdAuthorMember)) { void pluginData.state.common.sendErrorMessage(cmdMessage, "Unknown message"); return; } diff --git a/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts b/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts index 8c849d98..2c9928a4 100644 --- a/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts +++ b/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts @@ -1,6 +1,6 @@ import { VoiceChannel } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js"; -import { canActOn } from "../../../pluginUtils.js"; +import { canActOn, resolveMessageMember } from "../../../pluginUtils.js"; import { renderUsername } from "../../../utils.js"; import { LogsPlugin } from "../../Logs/LogsPlugin.js"; import { utilityCmd } from "../types.js"; @@ -16,7 +16,8 @@ export const VcdisconnectCmd = utilityCmd({ }, async run({ message: msg, args, pluginData }) { - if (!canActOn(pluginData, msg.member, args.member)) { + const authorMember = await resolveMessageMember(msg); + if (!canActOn(pluginData, authorMember, args.member)) { void pluginData.state.common.sendErrorMessage(msg, "Cannot move: insufficient permissions"); return; } diff --git a/backend/src/plugins/Utility/commands/VcmoveCmd.ts b/backend/src/plugins/Utility/commands/VcmoveCmd.ts index d8bee3ac..f36ce64e 100644 --- a/backend/src/plugins/Utility/commands/VcmoveCmd.ts +++ b/backend/src/plugins/Utility/commands/VcmoveCmd.ts @@ -1,6 +1,6 @@ import { ChannelType, Snowflake, VoiceChannel } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes.js"; -import { canActOn } from "../../../pluginUtils.js"; +import { canActOn, resolveMessageMember } from "../../../pluginUtils.js"; import { channelMentionRegex, isSnowflake, renderUsername, simpleClosestStringMatch } from "../../../utils.js"; import { LogsPlugin } from "../../Logs/LogsPlugin.js"; import { utilityCmd } from "../types.js"; @@ -144,15 +144,17 @@ export const VcmoveAllCmd = utilityCmd({ return; } + const authorMember = await resolveMessageMember(msg); + // Cant leave null, otherwise we get an assignment error in the catch - let currMember = msg.member; + let currMember = authorMember; const moveAmt = args.oldChannel.members.size; let errAmt = 0; for (const memberWithId of args.oldChannel.members) { currMember = memberWithId[1]; // Check for permissions but allow self-moves - if (currMember.id !== msg.member.id && !canActOn(pluginData, msg.member, currMember)) { + if (currMember.id !== authorMember.id && !canActOn(pluginData, authorMember, currMember)) { void pluginData.state.common.sendErrorMessage( msg, `Failed to move ${renderUsername(currMember)} (${currMember.id}): You cannot act on this member`, @@ -166,7 +168,7 @@ export const VcmoveAllCmd = utilityCmd({ channel: channel.id, }); } catch { - if (msg.member.id === currMember.id) { + if (authorMember.id === currMember.id) { void pluginData.state.common.sendErrorMessage(msg, "Unknown error when trying to move members"); return; } diff --git a/backend/src/plugins/Utility/functions/cleanMessages.ts b/backend/src/plugins/Utility/functions/cleanMessages.ts new file mode 100644 index 00000000..0cc4d2b7 --- /dev/null +++ b/backend/src/plugins/Utility/functions/cleanMessages.ts @@ -0,0 +1,49 @@ +import { GuildPluginData } from "knub"; +import { UtilityPluginType } from "../types.js"; +import { GuildBasedChannel, Snowflake, TextBasedChannel, User } from "discord.js"; +import { SavedMessage } from "../../../data/entities/SavedMessage.js"; +import { LogType } from "../../../data/LogType.js"; +import { chunkArray } from "../../../utils.js"; +import { getBaseUrl } from "../../../pluginUtils.js"; +import { LogsPlugin } from "../../Logs/LogsPlugin.js"; + +export async function cleanMessages( + pluginData: GuildPluginData, + channel: GuildBasedChannel & TextBasedChannel, + savedMessages: SavedMessage[], + mod: User, +) { + pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, savedMessages[0].id); + pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, savedMessages[0].id); + + // Delete & archive in ID order + savedMessages = Array.from(savedMessages).sort((a, b) => (a.id > b.id ? 1 : -1)); + const idsToDelete = savedMessages.map((m) => m.id) as Snowflake[]; + + // Make sure the deletions aren't double logged + idsToDelete.forEach((id) => pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, id)); + pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, idsToDelete[0]); + + // Actually delete the messages (in chunks of 100) + + const chunks = chunkArray(idsToDelete, 100); + await Promise.all( + chunks.map((chunk) => + Promise.all([channel.bulkDelete(chunk), pluginData.state.savedMessages.markBulkAsDeleted(chunk)]), + ), + ); + + // Create an archive + const archiveId = await pluginData.state.archives.createFromSavedMessages(savedMessages, pluginData.guild); + const baseUrl = getBaseUrl(pluginData); + const archiveUrl = pluginData.state.archives.getUrl(baseUrl, archiveId); + + pluginData.getPlugin(LogsPlugin).logClean({ + mod, + channel, + count: savedMessages.length, + archiveUrl, + }); + + return { archiveUrl }; +} diff --git a/backend/src/plugins/Utility/functions/fetchChannelMessagesToClean.ts b/backend/src/plugins/Utility/functions/fetchChannelMessagesToClean.ts new file mode 100644 index 00000000..82048123 --- /dev/null +++ b/backend/src/plugins/Utility/functions/fetchChannelMessagesToClean.ts @@ -0,0 +1,116 @@ +import { GuildBasedChannel, Message, OmitPartialGroupDMChannel, Snowflake, TextBasedChannel } from "discord.js"; +import { DAYS, getInviteCodesInString } from "../../../utils.js"; +import { GuildPluginData } from "knub"; +import { UtilityPluginType } from "../types.js"; +import { snowflakeToTimestamp } from "../../../utils/snowflakeToTimestamp.js"; +import { humanizeDurationShort } from "../../../humanizeDuration.js"; +import { allowTimeout } from "../../../RegExpRunner.js"; +import { SavedMessage } from "../../../data/entities/SavedMessage.js"; + +const MAX_CLEAN_COUNT = 300; +const MAX_CLEAN_TIME = 1 * DAYS; +const MAX_CLEAN_API_REQUESTS = 20; + +export interface FetchChannelMessagesToCleanOpts { + count: number; + beforeId: string; + upToId?: string; + authorId?: string; + includePins?: boolean; + onlyBotMessages?: boolean; + onlyWithInvites?: boolean; + matchContent?: RegExp; +} + +export interface SuccessResult { + messages: SavedMessage[]; + note: string; +} + +export interface ErrorResult { + error: string; +} + +export type FetchChannelMessagesToCleanResult = SuccessResult | ErrorResult; + +export async function fetchChannelMessagesToClean(pluginData: GuildPluginData, targetChannel: GuildBasedChannel & TextBasedChannel, opts: FetchChannelMessagesToCleanOpts): Promise { + if (opts.count > MAX_CLEAN_COUNT || opts.count <= 0) { + return { error: `Clean count must be between 1 and ${MAX_CLEAN_COUNT}` }; + } + + const result: FetchChannelMessagesToCleanResult = { + messages: [], + note: "", + }; + + const timestampCutoff = snowflakeToTimestamp(opts.beforeId) - MAX_CLEAN_TIME; + let foundId = false; + + let pinIds: Set = new Set(); + if (!opts.includePins) { + pinIds = new Set((await targetChannel.messages.fetchPinned()).keys()); + } + + let rawMessagesToClean: Array>> = []; + let beforeId = opts.beforeId; + let requests = 0; + while (rawMessagesToClean.length < opts.count) { + const potentialMessages = await targetChannel.messages.fetch({ + before: beforeId, + limit: 100, + }); + if (potentialMessages.size === 0) break; + + requests++; + + const filtered: Array>> = []; + for (const message of potentialMessages.values()) { + const contentString = message.content || ""; + if (opts.authorId && message.author.id !== opts.authorId) continue; + if (opts.onlyBotMessages && !message.author.bot) continue; + if (pinIds.has(message.id)) continue; + if (opts.onlyWithInvites && getInviteCodesInString(contentString).length === 0) continue; + if (opts.upToId && message.id < opts.upToId) { + foundId = true; + break; + } + if (message.createdTimestamp < timestampCutoff) continue; + if (opts.matchContent && !(await pluginData.state.regexRunner.exec(opts.matchContent, contentString).catch(allowTimeout))) { + continue; + } + + filtered.push(message); + } + const remaining = opts.count - rawMessagesToClean.length; + const withoutOverflow = filtered.slice(0, remaining); + rawMessagesToClean.push(...withoutOverflow); + + beforeId = potentialMessages.lastKey()!; + + if (foundId) { + break; + } + + if (rawMessagesToClean.length < opts.count) { + if (potentialMessages.last()!.createdTimestamp < timestampCutoff) { + result.note = `stopped looking after reaching ${humanizeDurationShort(MAX_CLEAN_TIME)} old messages`; + break; + } + + if (requests >= MAX_CLEAN_API_REQUESTS) { + result.note = `stopped looking after ${requests * 100} messages`; + break; + } + } + } + + // Discord messages -> SavedMessages + const existingStored = await pluginData.state.savedMessages.getMultiple(rawMessagesToClean.map((m) => m.id)); + const alreadyStored = existingStored.map((stored) => stored.id); + const messagesToStore = rawMessagesToClean.filter((potentialMsg) => !alreadyStored.includes(potentialMsg.id)); + await pluginData.state.savedMessages.createFromMessages(messagesToStore); + + result.messages = await pluginData.state.savedMessages.getMultiple(rawMessagesToClean.map((m) => m.id)); + + return result; +} diff --git a/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts b/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts index a990dd50..c78a50c0 100644 --- a/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts @@ -100,9 +100,8 @@ export async function getInviteInfoEmbed( fields: [], }; - invite = invite as GroupDMInvite; embed.author = { - name: invite.channel!.name ? `Group DM invite: ${invite.channel!.name}` : `Group DM invite`, + name: invite.channel.name ? `Group DM invite: ${invite.channel.name}` : `Group DM invite`, url: `https://discord.gg/${invite.code}`, }; // FIXME pending invite re-think diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index a2a23700..73981b42 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -5,6 +5,7 @@ import { GuildMember, Message, MessageComponentInteraction, + OmitPartialGroupDMChannel, PermissionsBitField, Snowflake, User, @@ -73,22 +74,22 @@ export async function displaySearch( pluginData: GuildPluginData, args: MemberSearchParams, searchType: SearchType.MemberSearch, - msg: Message, + msg: OmitPartialGroupDMChannel, ); export async function displaySearch( pluginData: GuildPluginData, args: BanSearchParams, searchType: SearchType.BanSearch, - msg: Message, + msg: OmitPartialGroupDMChannel, ); export async function displaySearch( pluginData: GuildPluginData, args: MemberSearchParams | BanSearchParams, searchType: SearchType, - msg: Message, + msg: OmitPartialGroupDMChannel, ) { // If we're not exporting, load 1 page of search results at a time and allow the user to switch pages with reactions - let originalSearchMsg: Message; + let originalSearchMsg: OmitPartialGroupDMChannel; let searching = false; let currentPage = args.page || 1; let stopCollectionFn: () => void; @@ -107,7 +108,7 @@ export async function displaySearch( searchMsgPromise = originalSearchMsg.edit("Searching..."); } else { searchMsgPromise = msg.channel.send("Searching..."); - searchMsgPromise.then((m) => (originalSearchMsg = m)); + searchMsgPromise.then((m) => (originalSearchMsg = m as OmitPartialGroupDMChannel)); } let searchResult; @@ -240,19 +241,19 @@ export async function archiveSearch( pluginData: GuildPluginData, args: MemberSearchParams, searchType: SearchType.MemberSearch, - msg: Message, + msg: OmitPartialGroupDMChannel, ); export async function archiveSearch( pluginData: GuildPluginData, args: BanSearchParams, searchType: SearchType.BanSearch, - msg: Message, + msg: OmitPartialGroupDMChannel, ); export async function archiveSearch( pluginData: GuildPluginData, args: MemberSearchParams | BanSearchParams, searchType: SearchType, - msg: Message, + msg: OmitPartialGroupDMChannel, ) { let results; try { diff --git a/backend/src/plugins/Utility/types.ts b/backend/src/plugins/Utility/types.ts index 2ab37f3e..6a2deba4 100644 --- a/backend/src/plugins/Utility/types.ts +++ b/backend/src/plugins/Utility/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { RegExpRunner } from "../../RegExpRunner.js"; import { GuildArchives } from "../../data/GuildArchives.js"; import { GuildCases } from "../../data/GuildCases.js"; diff --git a/backend/src/plugins/WelcomeMessage/types.ts b/backend/src/plugins/WelcomeMessage/types.ts index 05bd3354..815cac6e 100644 --- a/backend/src/plugins/WelcomeMessage/types.ts +++ b/backend/src/plugins/WelcomeMessage/types.ts @@ -1,5 +1,5 @@ import { BasePluginType, guildPluginEventListener } from "knub"; -import z from "zod"; +import z from "zod/v4"; import { GuildLogs } from "../../data/GuildLogs.js"; export const zWelcomeMessageConfig = z.strictObject({ diff --git a/backend/src/templateFormatter.ts b/backend/src/templateFormatter.ts index db053d3e..431891f7 100644 --- a/backend/src/templateFormatter.ts +++ b/backend/src/templateFormatter.ts @@ -481,7 +481,7 @@ export async function renderTemplate( // If our template cache is full, delete the first item if (templateCache.size >= TEMPLATE_CACHE_SIZE) { - const firstKey = templateCache.keys().next().value; + const firstKey = templateCache.keys().next().value!; templateCache.delete(firstKey); } diff --git a/backend/src/types.ts b/backend/src/types.ts index dfc9037a..35142f4c 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -1,5 +1,5 @@ import { GlobalPluginBlueprint, GuildPluginBlueprint, Knub } from "knub"; -import z, { ZodTypeAny } from "zod"; +import z from "zod/v4"; import { zSnowflake } from "./utils.js"; export const zZeppelinGuildConfig = z.strictObject({ @@ -31,7 +31,7 @@ export type DocsPluginType = "stable" | "legacy" | "internal"; export interface ZeppelinPluginDocs { type: DocsPluginType; - configSchema: ZodTypeAny; + configSchema: z.ZodType; prettyName?: string; description?: TMarkdown; diff --git a/backend/src/utils.test.ts b/backend/src/utils.test.ts index 2c1627d6..3fd7b054 100644 --- a/backend/src/utils.test.ts +++ b/backend/src/utils.test.ts @@ -1,5 +1,5 @@ import test from "ava"; -import z from "zod"; +import z from "zod/v4"; import { convertDelayStringToMS, convertMSToDelayString, getUrlsInString, zAllowedMentions } from "./utils.js"; import { ErisAllowedMentionFormat } from "./utils/erisAllowedMentionsToDjsMentionOptions.js"; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 2849d5a4..24d823d8 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -15,13 +15,17 @@ import { GuildTextBasedChannel, Invite, InviteGuild, + InviteType, LimitedCollection, Message, MessageCreateOptions, MessageMentionOptions, PartialChannelData, + PartialGroupDMChannel, PartialMessage, + PartialUser, RoleResolvable, + SendableChannels, Sticker, TextBasedChannel, User, @@ -31,10 +35,10 @@ import fs from "fs"; import https from "https"; import isEqual from "lodash/isEqual.js"; import { performance } from "perf_hooks"; -import tlds from "tlds" assert { type: "json" }; +import tlds from "tlds" with { type: "json" }; import tmp from "tmp"; import { URL } from "url"; -import { z, ZodEffects, ZodError, ZodRecord, ZodString } from "zod"; +import { z, ZodError, ZodPipe, ZodRecord, ZodString, ZodTransform } from "zod/v4"; import { ISavedMessageAttachmentData, SavedMessage } from "./data/entities/SavedMessage.js"; import { delayStringMultipliers, humanizeDuration } from "./humanizeDuration.js"; import { getProfiler } from "./profiler.js"; @@ -42,6 +46,7 @@ import { SimpleCache } from "./SimpleCache.js"; import { sendDM } from "./utils/sendDM.js"; import { Brand } from "./utils/typeUtils.js"; import { waitForButtonConfirm } from "./utils/waitForInteraction.js"; +import { GenericCommandSource } from "./pluginUtils.js"; const fsp = fs.promises; @@ -83,7 +88,7 @@ export function isDiscordAPIError(err: Error | string): err is DiscordAPIError { // null | undefined -> undefined export function zNullishToUndefined( type: T, -): ZodEffects> | undefined> { +): ZodPipe> | undefined>> { return type.transform((v) => v ?? undefined); } @@ -142,8 +147,7 @@ export function nonNullish(v: V): v is NonNullable { export type GuildInvite = Invite & { guild: InviteGuild | Guild }; export type GroupDMInvite = Invite & { - channel: PartialChannelData; - type: typeof ChannelType.GroupDM; + channel: PartialGroupDMChannel; }; export function zBoundedCharacters(min: number, max: number) { @@ -374,7 +378,7 @@ export function zBoundedRecord>( record: TRecord, minKeys: number, maxKeys: number, -): ZodEffects { +): TRecord { return record.refine( (data) => { const len = Object.keys(data).length; @@ -832,7 +836,7 @@ export function chunkMessageLines(str: string, maxChunkLength = 1990): string[] } export async function createChunkedMessage( - channel: TextBasedChannel | User, + channel: SendableChannels | User, messageText: string, allowedMentions?: MessageMentionOptions, ) { @@ -1299,7 +1303,7 @@ export async function resolveStickerId(bot: Client, id: Snowflake): Promise { @@ -1472,11 +1476,11 @@ export function isFullMessage(msg: Message | PartialMessage): msg is Message { } export function isGuildInvite(invite: Invite): invite is GuildInvite { - return invite.guild != null; + return invite.type === InviteType.Guild; } export function isGroupDMInvite(invite: Invite): invite is GroupDMInvite { - return invite.guild == null && invite.channel?.type === ChannelType.GroupDM; + return invite.type === InviteType.GroupDM; } export function inviteHasCounts(invite: Invite): invite is Invite { diff --git a/backend/src/utils/createPaginatedMessage.ts b/backend/src/utils/createPaginatedMessage.ts index aebf165a..5a7581ab 100644 --- a/backend/src/utils/createPaginatedMessage.ts +++ b/backend/src/utils/createPaginatedMessage.ts @@ -1,20 +1,17 @@ import { - ChatInputCommandInteraction, Client, Message, - MessageCreateOptions, - MessageEditOptions, MessageReaction, PartialMessageReaction, PartialUser, - User, + User } from "discord.js"; -import { sendContextResponse } from "../pluginUtils.js"; +import { ContextResponseOptions, fetchContextChannel, GenericCommandSource } from "../pluginUtils.js"; import { MINUTES, noop } from "../utils.js"; import { Awaitable } from "./typeUtils.js"; import Timeout = NodeJS.Timeout; -export type LoadPageFn = (page: number) => Awaitable; +export type LoadPageFn = (page: number) => Awaitable; export interface PaginateMessageOpts { timeout: number; @@ -28,14 +25,19 @@ const defaultOpts: PaginateMessageOpts = { export async function createPaginatedMessage( client: Client, - context: Message | User | ChatInputCommandInteraction, + context: GenericCommandSource, totalPages: number, loadPageFn: LoadPageFn, opts: Partial = {}, ): Promise { const fullOpts = { ...defaultOpts, ...opts } as PaginateMessageOpts; + const channel = await fetchContextChannel(context); + if (!channel.isSendable()) { + throw new Error("Context channel is not sendable"); + } + const firstPageContent = await loadPageFn(1); - const message = await sendContextResponse(context, firstPageContent); + const message = await channel.send(firstPageContent); let page = 1; let pageLoadId = 0; // Used to avoid race conditions when rapidly switching pages diff --git a/backend/src/utils/formatZodIssue.ts b/backend/src/utils/formatZodIssue.ts index 93932f66..a27a863f 100644 --- a/backend/src/utils/formatZodIssue.ts +++ b/backend/src/utils/formatZodIssue.ts @@ -1,4 +1,4 @@ -import { ZodIssue } from "zod"; +import { ZodIssue } from "zod/v4"; export function formatZodIssue(issue: ZodIssue): string { const path = issue.path.join("/"); diff --git a/backend/src/utils/permissionNames.ts b/backend/src/utils/permissionNames.ts index e888ab3c..801f512f 100644 --- a/backend/src/utils/permissionNames.ts +++ b/backend/src/utils/permissionNames.ts @@ -48,4 +48,8 @@ export const PERMISSION_NAMES = { UseExternalSounds: "Use External Sounds", UseSoundboard: "Use Soundboard", ViewCreatorMonetizationAnalytics: "View Creator Monetization Analytics", + CreateGuildExpressions: "Create Guild Expressions", + CreateEvents: "Create Events", + SendPolls: "Send Polls", + UseExternalApps: "Use External Apps", } as const satisfies Record; diff --git a/backend/src/utils/templateSafeObjects.ts b/backend/src/utils/templateSafeObjects.ts index 3a65aa8d..9ca5a498 100644 --- a/backend/src/utils/templateSafeObjects.ts +++ b/backend/src/utils/templateSafeObjects.ts @@ -5,6 +5,7 @@ import { GuildMember, Message, PartialGuildMember, + PartialUser, Role, Snowflake, StageInstance, @@ -242,15 +243,15 @@ export function guildToTemplateSafeGuild(guild: Guild): TemplateSafeGuild { }); } -export function userToTemplateSafeUser(user: User | UnknownUser): TemplateSafeUser { - if (user instanceof UnknownUser) { +export function userToTemplateSafeUser(user: User | UnknownUser | PartialUser): TemplateSafeUser { + if (user instanceof UnknownUser || user.partial) { return new TemplateSafeUser({ id: user.id, username: "Unknown", discriminator: "0000", mention: `<@${user.id}>`, tag: "Unknown#0000", - renderedUsername: renderUsername(user), + renderedUsername: "Unknown", }); } diff --git a/backend/src/utils/waitForInteraction.ts b/backend/src/utils/waitForInteraction.ts index 6cf9ad07..cd035a76 100644 --- a/backend/src/utils/waitForInteraction.ts +++ b/backend/src/utils/waitForInteraction.ts @@ -2,20 +2,17 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, - ChatInputCommandInteraction, - Message, MessageActionRowComponentBuilder, MessageComponentInteraction, - MessageCreateOptions, - User, + MessageCreateOptions } from "discord.js"; import moment from "moment"; import { v4 as uuidv4 } from "uuid"; -import { isContextInteraction } from "../pluginUtils.js"; +import { GenericCommandSource, isContextInteraction, sendContextResponse } from "../pluginUtils.js"; import { noop } from "../utils.js"; export async function waitForButtonConfirm( - context: Message | User | ChatInputCommandInteraction, + context: GenericCommandSource, toPost: Omit, options?: WaitForOptions, ): Promise { @@ -33,15 +30,7 @@ export async function waitForButtonConfirm( .setLabel(options?.cancelText || "Cancel") .setCustomId(`cancelButton:${idMod}:${uuidv4()}`), ]); - const sendMethod = () => { - if (contextIsInteraction) { - return context.replied ? context.editReply.bind(context) : context.reply.bind(context); - } else { - return "send" in context ? context.send.bind(context) : context.channel.send.bind(context.channel); - } - }; - const extraParameters = contextIsInteraction ? { fetchReply: true, ephemeral: true } : {}; - const message = (await sendMethod()({ ...toPost, components: [row], ...extraParameters })) as Message; + const message = await sendContextResponse(context, { ...toPost, components: [row] }, true); const collector = message.createMessageComponentCollector({ time: 10000 }); diff --git a/backend/src/utils/zColor.ts b/backend/src/utils/zColor.ts index 4961b67d..ab256313 100644 --- a/backend/src/utils/zColor.ts +++ b/backend/src/utils/zColor.ts @@ -1,4 +1,4 @@ -import z from "zod"; +import z from "zod/v4"; import { parseColor } from "./parseColor.js"; import { rgbToInt } from "./rgbToInt.js"; diff --git a/backend/src/utils/zValidTimezone.ts b/backend/src/utils/zValidTimezone.ts index 7757de05..b0b06556 100644 --- a/backend/src/utils/zValidTimezone.ts +++ b/backend/src/utils/zValidTimezone.ts @@ -1,4 +1,4 @@ -import { ZodString } from "zod"; +import { ZodString } from "zod/v4"; import { isValidTimezone } from "./isValidTimezone.js"; export function zValidTimezone(z: Z) { diff --git a/backend/src/utils/zodDeepPartial.ts b/backend/src/utils/zodDeepPartial.ts new file mode 100644 index 00000000..504f4ce2 --- /dev/null +++ b/backend/src/utils/zodDeepPartial.ts @@ -0,0 +1,165 @@ +/* + Modified version of https://gist.github.com/jaens/7e15ae1984bb338c86eb5e452dee3010 + Original version's license: + + Copyright 2024, Jaen - https://github.com/jaens + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { z } from "zod/v4"; +import { $ZodRecordKey, $ZodType } from "zod/v4/core"; + +const RESOLVING = Symbol("mapOnSchema/resolving"); + +export function mapOnSchema( + schema: T, + fn: (schema: $ZodType) => TResult, +): TResult; + +/** + * Applies {@link fn} to each element of the schema recursively, replacing every schema with its return value. + * The rewriting is applied bottom-up (ie. {@link fn} will get called on "children" first). + */ +export function mapOnSchema(schema: $ZodType, fn: (schema: $ZodType) => $ZodType): $ZodType { + // Cache results to support recursive schemas + const results = new Map<$ZodType, $ZodType | typeof RESOLVING>(); + + function mapElement(s: $ZodType) { + const value = results.get(s); + if (value === RESOLVING) { + throw new Error("Recursive schema access detected"); + } else if (value !== undefined) { + return value; + } + + results.set(s, RESOLVING); + const result = mapOnSchema(s, fn); + results.set(s, result); + return result; + } + + function mapInner() { + if (schema instanceof z.ZodObject) { + const newShape: Record = {}; + for (const [key, value] of Object.entries(schema.shape)) { + newShape[key] = mapElement(value); + } + + return new z.ZodObject({ + ...schema.def, + shape: newShape, + }); + } else if (schema instanceof z.ZodArray) { + return new z.ZodArray({ + ...schema.def, + type: "array", + element: mapElement(schema.def.element), + }); + } else if (schema instanceof z.ZodMap) { + return new z.ZodMap({ + ...schema.def, + keyType: mapElement(schema.def.keyType), + valueType: mapElement(schema.def.valueType), + }); + } else if (schema instanceof z.ZodSet) { + return new z.ZodSet({ + ...schema.def, + valueType: mapElement(schema.def.valueType), + }); + } else if (schema instanceof z.ZodOptional) { + return new z.ZodOptional({ + ...schema.def, + innerType: mapElement(schema.def.innerType), + }); + } else if (schema instanceof z.ZodNullable) { + return new z.ZodNullable({ + ...schema.def, + innerType: mapElement(schema.def.innerType), + }); + } else if (schema instanceof z.ZodDefault) { + return new z.ZodDefault({ + ...schema.def, + innerType: mapElement(schema.def.innerType), + }); + } else if (schema instanceof z.ZodReadonly) { + return new z.ZodReadonly({ + ...schema.def, + innerType: mapElement(schema.def.innerType), + }); + } else if (schema instanceof z.ZodLazy) { + return new z.ZodLazy({ + ...schema.def, + // NB: This leaks `fn` into the schema, but there is no other way to support recursive schemas + getter: () => mapElement(schema._def.getter()), + }); + } else if (schema instanceof z.ZodPromise) { + return new z.ZodPromise({ + ...schema.def, + innerType: mapElement(schema.def.innerType), + }); + } else if (schema instanceof z.ZodCatch) { + return new z.ZodCatch({ + ...schema.def, + innerType: mapElement(schema._def.innerType), + }); + } else if (schema instanceof z.ZodTuple) { + return new z.ZodTuple({ + ...schema.def, + items: schema.def.items.map((item: $ZodType) => mapElement(item)), + rest: schema.def.rest && mapElement(schema.def.rest), + }); + } else if (schema instanceof z.ZodDiscriminatedUnion) { + return new z.ZodDiscriminatedUnion({ + ...schema.def, + options: schema.options.map((option) => mapOnSchema(option, fn)), + }); + } else if (schema instanceof z.ZodUnion) { + return new z.ZodUnion({ + ...schema.def, + options: schema.options.map((option) => mapOnSchema(option, fn)), + }); + } else if (schema instanceof z.ZodIntersection) { + return new z.ZodIntersection({ + ...schema.def, + right: mapElement(schema.def.right), + left: mapElement(schema.def.left), + }); + } else if (schema instanceof z.ZodRecord) { + return new z.ZodRecord({ + ...schema.def, + keyType: mapElement(schema.def.keyType) as $ZodRecordKey, + valueType: mapElement(schema.def.valueType), + }); + } else { + return schema; + } + } + + return fn(mapInner()); +} + +export function deepPartial(schema: T): T { + return mapOnSchema(schema, (s) => (s instanceof z.ZodObject ? s.partial() : s)) as T; +} + +/** Make all object schemas "strict" (ie. fail on unknown keys), except if they are marked as `.passthrough()` */ +export function deepStrict(schema: T): T { + return mapOnSchema(schema, (s) => + s instanceof z.ZodObject /* && s.def.unknownKeys !== "passthrough" */ ? s.strict() : s, + ) as T; +} + +export function deepStrictAll(schema: T): T { + return mapOnSchema(schema, (s) => (s instanceof z.ZodObject ? s.strict() : s)) as T; +} diff --git a/backend/src/yamlParseTest.ts b/backend/src/yamlParseTest.ts deleted file mode 100644 index 5db961ff..00000000 --- a/backend/src/yamlParseTest.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { load } from "js-yaml"; -import YAML from "yawn-yaml/cjs"; - -const src = ` -prefix: '!' - -plugins: - myplugin: - config: - - can_do_thing: true - - # Lol - can_do_other_thing: false -`; - -const json = load(src); -const yaml = new YAML(src); -json.plugins.myplugin.config.can_do_thing = false; -yaml.json = json; - -// tslint:disable-next-line:no-console -console.log(yaml.yaml); diff --git a/docker-compose.development.yml b/docker-compose.development.yml index bbeb020d..74c264f8 100644 --- a/docker-compose.development.yml +++ b/docker-compose.development.yml @@ -1,4 +1,3 @@ -version: '3' name: zeppelin-dev volumes: home: {} diff --git a/docker/development/devenv/Dockerfile b/docker/development/devenv/Dockerfile index 84d6fcf4..8304b54a 100644 --- a/docker/development/devenv/Dockerfile +++ b/docker/development/devenv/Dockerfile @@ -18,8 +18,8 @@ RUN mkdir /var/run/sshd RUN useradd -rm -d /home/ubuntu -s /bin/bash -g root -G sudo -u $DEVELOPMENT_UID ubuntu RUN echo "ubuntu:${DEVELOPMENT_SSH_PASSWORD}" | chpasswd -# Install Node.js 20 and packages needed to build native packages -RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - +# Install Node.js 22 and packages needed to build native packages +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - RUN apt-get install -y nodejs gcc g++ make python3 CMD ["/usr/sbin/sshd", "-D", "-e"] diff --git a/package-lock.json b/package-lock.json index a0b5df7e..cf65388a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "dashboard" ], "devDependencies": { - "@types/node": "^20.12.6", + "@types/node": "^22.15.18", "@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/parser": "^5.59.5", "eslint": "^8.40.0", @@ -35,7 +35,7 @@ "cors": "^2.8.5", "cross-env": "^7.0.3", "deep-diff": "^1.0.2", - "discord.js": "^14.14.1", + "discord.js": "^14.19.3", "dotenv": "^4.0.0", "emoji-regex": "^8.0.0", "escape-string-regexp": "^1.0.5", @@ -43,7 +43,7 @@ "fp-ts": "^2.0.1", "humanize-duration": "^3.15.0", "js-yaml": "^4.1.0", - "knub": "^32.0.0-next.21", + "knub": "^32.0.0-next.23", "knub-command-manager": "^9.1.0", "last-commit-log": "^2.1.0", "lodash-es": "^4.17.21", @@ -70,7 +70,7 @@ "uuid": "^9.0.0", "yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build", "zlib-sync": "^0.1.7", - "zod": "^3.7.2" + "zod": "^3.25.17" }, "devDependencies": { "@types/cors": "^2.8.5", @@ -3048,26 +3048,31 @@ } }, "node_modules/@discordjs/builders": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.7.0.tgz", - "integrity": "sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.2.tgz", + "integrity": "sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/formatters": "^0.3.3", - "@discordjs/util": "^1.0.2", - "@sapphire/shapeshift": "^3.9.3", - "discord-api-types": "0.37.61", + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.1", "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.3", - "tslib": "^2.6.2" + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" }, "engines": { "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/builders/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/@discordjs/collection": { "version": "1.5.3", @@ -3078,87 +3083,113 @@ } }, "node_modules/@discordjs/formatters": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.3.tgz", - "integrity": "sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "license": "Apache-2.0", "dependencies": { - "discord-api-types": "0.37.61" + "discord-api-types": "^0.38.1" }, "engines": { "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/rest": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.2.0.tgz", - "integrity": "sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.5.0.tgz", + "integrity": "sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/collection": "^2.0.0", - "@discordjs/util": "^1.0.2", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.5.1", - "@vladfrangu/async_event_emitter": "^2.2.2", - "discord-api-types": "0.37.61", - "magic-bytes.js": "^1.5.0", - "tslib": "^2.6.2", - "undici": "5.27.2" + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.1" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz", - "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/rest/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/@discordjs/util": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.2.tgz", - "integrity": "sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", "engines": { - "node": ">=16.11.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/ws": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.0.2.tgz", - "integrity": "sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.2.tgz", + "integrity": "sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/collection": "^2.0.0", - "@discordjs/rest": "^2.1.0", - "@discordjs/util": "^1.0.2", - "@sapphire/async-queue": "^1.5.0", - "@types/ws": "^8.5.9", - "@vladfrangu/async_event_emitter": "^2.2.2", - "discord-api-types": "0.37.61", + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.0", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", "tslib": "^2.6.2", - "ws": "^8.14.2" + "ws": "^8.17.0" }, "engines": { "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz", - "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/ws/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", @@ -3263,14 +3294,6 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, - "node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", - "engines": { - "node": ">=14" - } - }, "node_modules/@fastify/error": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", @@ -3772,30 +3795,33 @@ } }, "node_modules/@sapphire/async-queue": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.2.tgz", - "integrity": "sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, "node_modules/@sapphire/shapeshift": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.6.tgz", - "integrity": "sha512-4+Na/fxu2SEepZRb9z0dbsVh59QtwPuBg/UVaDib3av7ZY14b14+z09z6QVn0P6Dv6eOU2NDTsjIi0mbtgP56g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" }, "engines": { - "node": ">=v18" + "node": ">=v16" } }, "node_modules/@sapphire/snowflake": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz", - "integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" @@ -4102,11 +4128,12 @@ } }, "node_modules/@types/node": { - "version": "20.12.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.6.tgz", - "integrity": "sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==", + "version": "22.15.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", + "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.21.0" } }, "node_modules/@types/node-forge": { @@ -4266,9 +4293,10 @@ "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==" }, "node_modules/@types/ws": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", - "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -4477,9 +4505,10 @@ } }, "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.4.tgz", - "integrity": "sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", + "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" @@ -8913,38 +8942,46 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" + "version": "0.38.8", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.8.tgz", + "integrity": "sha512-xuRXPD44FcbKHrQK15FS1HFlMRNJtsaZou/SVws18vQ7zHqmlxyDktMkZpyvD6gE2ctGOVYC/jUyoMMAyBWfcw==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] }, "node_modules/discord.js": { - "version": "14.14.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz", - "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==", + "version": "14.19.3", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.19.3.tgz", + "integrity": "sha512-lncTRk0k+8Q5D3nThnODBR8fR8x2fM798o8Vsr40Krx0DjPwpZCuxxTcFMrXMQVOqM1QB9wqWgaXPg3TbmlHqA==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/builders": "^1.7.0", + "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.3.3", - "@discordjs/rest": "^2.1.0", - "@discordjs/util": "^1.0.2", - "@discordjs/ws": "^1.0.2", - "@sapphire/snowflake": "3.5.1", - "@types/ws": "8.5.9", - "discord-api-types": "0.37.61", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.5.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.2", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", - "tslib": "2.6.2", - "undici": "5.27.2", - "ws": "8.14.2" + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.1" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/discord.js/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/distributions": { "version": "2.1.0", @@ -12769,18 +12806,19 @@ } }, "node_modules/knub": { - "version": "32.0.0-next.21", - "resolved": "https://registry.npmjs.org/knub/-/knub-32.0.0-next.21.tgz", - "integrity": "sha512-ZzY9uOxKe0ZKCKQ4yp26SBxuPl6Gac1LGNpXMPGydYX/0EIL1q3zunYAwfpTpgc7LxR02hmYdDcsBCj8ri4xkA==", + "version": "32.0.0-next.23", + "resolved": "https://registry.npmjs.org/knub/-/knub-32.0.0-next.23.tgz", + "integrity": "sha512-kDv6QhE5ALL9bew0GJtzFh2HpyOECCZ6gGBnZNcKUekxWh3LdKW2nYuo1kfqTycEM/5zn7uiHTGLVxfea9LNxA==", + "license": "MIT", "dependencies": { - "discord-api-types": "^0.37.67", - "discord.js": "^14.14.1", + "discord-api-types": "^0.38.8", + "discord.js": "^14.19.3", "knub-command-manager": "^9.1.0", - "ts-essentials": "^9", - "zod": "^3.19.1" + "ts-essentials": "^10.0.4", + "zod": "^3.25.17" }, "engines": { - "node": ">=16" + "node": ">=22" } }, "node_modules/knub-command-manager": { @@ -12799,11 +12837,6 @@ "node": ">=8" } }, - "node_modules/knub/node_modules/discord-api-types": { - "version": "0.37.73", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.73.tgz", - "integrity": "sha512-mi915PBUxF1G233EwHKNegNAF/tVfiSRN9+hKwu0G3NpbtLXvWUxCuCjgSyY+QmQ6/Hvpqm0xs5HxzfvhAS20A==" - }, "node_modules/labeled-stream-splicer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", @@ -13046,9 +13079,10 @@ } }, "node_modules/magic-bytes.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.8.0.tgz", - "integrity": "sha512-lyWpfvNGVb5lu8YUAbER0+UMBTdR63w2mcSUlhhBTyVbxJvjgqwyAf3AZD6MprgK0uHuBoWXSDAMWLupX83o3Q==" + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" }, "node_modules/magic-string": { "version": "0.25.1", @@ -21027,11 +21061,12 @@ } }, "node_modules/ts-essentials": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.1.tgz", - "integrity": "sha512-oke0rI2EN9pzHsesdmrOrnqv1eQODmJpd/noJjwj2ZPC3Z4N2wbjrOEqnsEgmvlO2+4fBb0a794DCna2elEVIQ==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.0.4.tgz", + "integrity": "sha512-lwYdz28+S4nicm+jFi6V58LaAIpxzhg9rLdgNC1VsdP/xiFBseGhF1M/shwCk6zMmwahBZdXcl34LVHrEang3A==", + "license": "MIT", "peerDependencies": { - "typescript": ">=4.1.0" + "typescript": ">=4.5.0" }, "peerDependenciesMeta": { "typescript": { @@ -21069,9 +21104,10 @@ } }, "node_modules/ts-mixer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", - "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" }, "node_modules/ts-node": { "version": "10.9.2", @@ -21680,20 +21716,19 @@ "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==" }, "node_modules/undici": { - "version": "5.27.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", - "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "license": "MIT", "engines": { - "node": ">=14.0" + "node": ">=18.17" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -22778,9 +22813,10 @@ } }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -22938,9 +22974,10 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.25.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.17.tgz", + "integrity": "sha512-8hQzQ/kMOIFbwOgPrm9Sf9rtFHpFUMy4HvN0yEB0spw14aYi0uT5xG5CE2DB9cd51GWNsz+DNO7se1kztHMKnw==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index f1ff64c9..d32ce7e9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "start-dashboard": "cd dashboard && node serve.js" }, "devDependencies": { - "@types/node": "^20.12.6", + "@types/node": "^22.15.18", "@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/parser": "^5.59.5", "eslint": "^8.40.0",