From 062cb053cc3861273445f596d7e3df74468903ad Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 18 Nov 2023 12:55:01 +0200 Subject: [PATCH 01/51] fix: crash on tag round() with >100 decimals --- backend/src/templateFormatter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/templateFormatter.ts b/backend/src/templateFormatter.ts index 11c17925..abe42fe0 100644 --- a/backend/src/templateFormatter.ts +++ b/backend/src/templateFormatter.ts @@ -419,7 +419,7 @@ const baseValues = { }, round(arg, decimals = 0) { if (isNaN(arg)) return 0; - return decimals === 0 ? Math.round(arg) : arg.toFixed(decimals); + return decimals === 0 ? Math.round(arg) : arg.toFixed(Math.max(0, Math.min(decimals, 100))); }, add(...args) { return args.reduce((result, arg) => { From 9fd2bf4edb71d2c21b1e8af256361a9dc69d825a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 18 Nov 2023 12:57:54 +0200 Subject: [PATCH 02/51] fix: crash on tag round() with non-numeric argument --- backend/src/templateFormatter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/templateFormatter.ts b/backend/src/templateFormatter.ts index abe42fe0..e51506c4 100644 --- a/backend/src/templateFormatter.ts +++ b/backend/src/templateFormatter.ts @@ -418,7 +418,10 @@ const baseValues = { return Math.round(randValue * (to - from) + from); }, round(arg, decimals = 0) { - if (isNaN(arg)) return 0; + if (typeof arg !== "number") { + arg = parseFloat(arg); + } + if (Number.isNaN(arg)) return 0; return decimals === 0 ? Math.round(arg) : arg.toFixed(Math.max(0, Math.min(decimals, 100))); }, add(...args) { From c82e147ea163fcbf4ced78fe385e83560ccc9a5e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 25 Nov 2023 12:15:32 +0200 Subject: [PATCH 03/51] feat: more robust tag error handling --- .../src/plugins/Tags/commands/TagEvalCmd.ts | 14 ++++++++++---- .../plugins/Tags/util/renderTagFromString.ts | 19 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/backend/src/plugins/Tags/commands/TagEvalCmd.ts b/backend/src/plugins/Tags/commands/TagEvalCmd.ts index 55659d30..27cbe4f3 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"; import { memberToTemplateSafeMember, userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { tagsCmd } from "../types"; import { renderTagBody } from "../util/renderTagBody"; +import { logger } from "../../../logger"; export const TagEvalCmd = tagsCmd({ trigger: "tag eval", @@ -34,12 +35,17 @@ export const TagEvalCmd = tagsCmd({ msg.channel.send(rendered); } catch (e) { - if (e instanceof TemplateParseError) { - sendErrorMessage(pluginData, msg.channel, `Failed to render tag: ${e.message}`); - return; + const errorMessage = e instanceof TemplateParseError + ? e.message + : "Internal error"; + + sendErrorMessage(pluginData, msg.channel, `Failed to render tag: ${errorMessage}`); + + if (! (e instanceof TemplateParseError)) { + logger.warn(`Internal error evaluating tag in ${pluginData.guild.id}: ${e}`); } - throw e; + return; } }, }); diff --git a/backend/src/plugins/Tags/util/renderTagFromString.ts b/backend/src/plugins/Tags/util/renderTagFromString.ts index 55e699fa..f1265858 100644 --- a/backend/src/plugins/Tags/util/renderTagFromString.ts +++ b/backend/src/plugins/Tags/util/renderTagFromString.ts @@ -7,6 +7,7 @@ import { memberToTemplateSafeMember, userToTemplateSafeUser } from "../../../uti import { LogsPlugin } from "../../Logs/LogsPlugin"; import { TTag, TagsPluginType } from "../types"; import { renderTagBody } from "./renderTagBody"; +import { logger } from "../../../logger"; export async function renderTagFromString( pluginData: GuildPluginData<TagsPluginType>, @@ -34,14 +35,18 @@ export async function renderTagFromString( return validateAndParseMessageContent(rendered); } catch (e) { - if (e instanceof TemplateParseError) { - const logs = pluginData.getPlugin(LogsPlugin); - logs.logBotAlert({ - body: `Failed to render tag \`${prefix}${tagName}\`: ${e.message}`, - }); - return null; + const logs = pluginData.getPlugin(LogsPlugin); + const errorMessage = e instanceof TemplateParseError + ? e.message + : "Internal error"; + logs.logBotAlert({ + body: `Failed to render tag \`${prefix}${tagName}\`: ${errorMessage}`, + }); + + if (! (e instanceof TemplateParseError)) { + logger.warn(`Internal error rendering tag ${tagName} in ${pluginData.guild.id}: ${e}`); } - throw e; + return null; } } From fafaefa1fbafddf9216d2f5ddaad0177cf3fb4bc Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 25 Nov 2023 12:28:28 +0200 Subject: [PATCH 04/51] feat: limit logs timestamp_format length --- backend/src/plugins/Logs/LogsPlugin.ts | 5 +++-- backend/src/plugins/Logs/types.ts | 5 +++-- backend/src/utils/iotsUtils.ts | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 backend/src/utils/iotsUtils.ts diff --git a/backend/src/plugins/Logs/LogsPlugin.ts b/backend/src/plugins/Logs/LogsPlugin.ts index bd970160..e56ba756 100644 --- a/backend/src/plugins/Logs/LogsPlugin.ts +++ b/backend/src/plugins/Logs/LogsPlugin.ts @@ -110,6 +110,7 @@ import { logVoiceChannelForceMove } from "./logFunctions/logVoiceChannelForceMov import { logVoiceChannelJoin } from "./logFunctions/logVoiceChannelJoin"; import { logVoiceChannelLeave } from "./logFunctions/logVoiceChannelLeave"; import { logVoiceChannelMove } from "./logFunctions/logVoiceChannelMove"; +import { asBoundedString } from "../../utils/iotsUtils"; // The `any` cast here is to prevent TypeScript from locking up from the circular dependency function getCasesPlugin(): Promise<any> { @@ -120,12 +121,12 @@ const defaultOptions: PluginOptions<LogsPluginType> = { config: { channels: {}, format: { - timestamp: FORMAT_NO_TIMESTAMP, // Legacy/deprecated, use timestamp_format below instead + timestamp: asBoundedString(FORMAT_NO_TIMESTAMP), // Legacy/deprecated, use timestamp_format below instead ...DefaultLogMessages, }, ping_user: true, // Legacy/deprecated, if below is false mentions wont actually ping. In case you really want the old behavior, set below to true allow_user_mentions: false, - timestamp_format: "[<t:]X[>]", + timestamp_format: asBoundedString("[<t:]X[>]"), include_embed_timestamp: true, }, diff --git a/backend/src/plugins/Logs/types.ts b/backend/src/plugins/Logs/types.ts index 62008aab..759f680d 100644 --- a/backend/src/plugins/Logs/types.ts +++ b/backend/src/plugins/Logs/types.ts @@ -23,6 +23,7 @@ import { TemplateSafeUser, } from "../../utils/templateSafeObjects"; import { TRegex } from "../../validatorUtils"; +import { tBoundedString } from "../../utils/iotsUtils"; export const tLogFormats = t.record(t.string, t.union([t.string, tMessageContent])); export type TLogFormats = t.TypeOf<typeof tLogFormats>; @@ -53,12 +54,12 @@ export const ConfigSchema = t.type({ format: t.intersection([ tLogFormats, t.type({ - timestamp: t.string, // Legacy/deprecated + timestamp: tBoundedString(0, 64), // Legacy/deprecated }), ]), ping_user: t.boolean, // Legacy/deprecated, if below is false mentions wont actually ping allow_user_mentions: t.boolean, - timestamp_format: t.string, + timestamp_format: tBoundedString(0, 64), include_embed_timestamp: t.boolean, }); export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; diff --git a/backend/src/utils/iotsUtils.ts b/backend/src/utils/iotsUtils.ts new file mode 100644 index 00000000..a3636c84 --- /dev/null +++ b/backend/src/utils/iotsUtils.ts @@ -0,0 +1,17 @@ +import * as t from "io-ts"; + +interface BoundedStringBrand { + readonly BoundedString: unique symbol; +} + +export function asBoundedString(str: string) { + return str as t.Branded<string, BoundedStringBrand>; +} + +export function tBoundedString(min: number, max: number) { + return t.brand( + t.string, + (str): str is t.Branded<string, BoundedStringBrand> => (str.length >= min && str.length <= max), + "BoundedString", + ); +} From c4a2be5ff5b93d012aa021c10f03a997903e4b32 Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 13:11:38 +0000 Subject: [PATCH 05/51] resolveUserId support new usernames Signed-off-by: GitHub <noreply@github.com> --- backend/src/utils.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 1bb4d6ce..f29d990c 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1222,11 +1222,25 @@ export function resolveUserId(bot: Client, value: string) { } // A non-mention, full username? - const usernameMatch = value.match(/^@?([^#]+)#(\d{4})$/); + const oldUsernameMatch = value.match(/^@?([^#]+)#(\d{4})$/); + if (oldUsernameMatch) { + const profiler = getProfiler(); + const start = performance.now(); + const user = bot.users.cache.find( + (u) => u.username === oldUsernameMatch[1] && u.discriminator === oldUsernameMatch[2], + ); + profiler?.addDataPoint("utils:resolveUserId:usernameMatch", performance.now() - start); + if (user) { + return user.id; + } + } + + // new usernames system + const usernameMatch = value.match(/^@?([^#]+)$/); if (usernameMatch) { const profiler = getProfiler(); const start = performance.now(); - const user = bot.users.cache.find((u) => u.username === usernameMatch[1] && u.discriminator === usernameMatch[2]); + const user = bot.users.cache.find((u) => u.username === usernameMatch[1]); profiler?.addDataPoint("utils:resolveUserId:usernameMatch", performance.now() - start); if (user) { return user.id; From 43076b3db68bafed52e0963914ebf8a027dcdcba Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 13:16:44 +0000 Subject: [PATCH 06/51] update djs Signed-off-by: GitHub <noreply@github.com> --- backend/package-lock.json | 289 +++++++----------- backend/package.json | 2 +- .../plugins/Automod/actions/startThread.ts | 13 +- 3 files changed, 122 insertions(+), 182 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index b1d6d189..4c6eaa14 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,7 +14,7 @@ "cors": "^2.8.5", "cross-env": "^7.0.3", "deep-diff": "^1.0.2", - "discord.js": "^14.11.0", + "discord.js": "^14.14.1", "dotenv": "^4.0.0", "emoji-regex": "^8.0.0", "erlpack": "github:discord/erlpack", @@ -265,84 +265,109 @@ } }, "node_modules/@discordjs/builders": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.6.3.tgz", - "integrity": "sha512-CTCh8NqED3iecTNuiz49mwSsrc2iQb4d0MjMdmS/8pb69Y4IlzJ/DIy/p5GFlgOrFbNO2WzMHkWKQSiJ3VNXaw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.7.0.tgz", + "integrity": "sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==", "dependencies": { - "@discordjs/formatters": "^0.3.1", - "@discordjs/util": "^0.3.1", - "@sapphire/shapeshift": "^3.8.2", - "discord-api-types": "^0.37.41", + "@discordjs/formatters": "^0.3.3", + "@discordjs/util": "^1.0.2", + "@sapphire/shapeshift": "^3.9.3", + "discord-api-types": "0.37.61", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.3", - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=16.9.0" + "node": ">=16.11.0" } }, "node_modules/@discordjs/collection": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.1.tgz", - "integrity": "sha512-aWEc9DCf3TMDe9iaJoOnO2+JVAjeRNuRxPZQA6GVvBf+Z3gqUuWYBy2NWh4+5CLYq5uoc3MOvUQ5H5m8CJBqOA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", "engines": { - "node": ">=16.9.0" + "node": ">=16.11.0" } }, "node_modules/@discordjs/formatters": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.1.tgz", - "integrity": "sha512-M7X4IGiSeh4znwcRGcs+49B5tBkNDn4k5bmhxJDAUhRxRHTiFAOTVUNQ6yAKySu5jZTnCbSvTYHW3w0rAzV1MA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.3.tgz", + "integrity": "sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==", "dependencies": { - "discord-api-types": "^0.37.41" + "discord-api-types": "0.37.61" }, "engines": { - "node": ">=16.9.0" + "node": ">=16.11.0" } }, "node_modules/@discordjs/rest": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.7.1.tgz", - "integrity": "sha512-Ofa9UqT0U45G/eX86cURQnX7gzOJLG2oC28VhIk/G6IliYgQF7jFByBJEykPSHE4MxPhqCleYvmsrtfKh1nYmQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.2.0.tgz", + "integrity": "sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==", "dependencies": { - "@discordjs/collection": "^1.5.1", - "@discordjs/util": "^0.3.0", + "@discordjs/collection": "^2.0.0", + "@discordjs/util": "^1.0.2", "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.4.2", - "discord-api-types": "^0.37.41", - "file-type": "^18.3.0", - "tslib": "^2.5.0", - "undici": "^5.22.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" }, "engines": { - "node": ">=16.9.0" + "node": ">=16.11.0" + } + }, + "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==", + "engines": { + "node": ">=18" } }, "node_modules/@discordjs/util": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.3.1.tgz", - "integrity": "sha512-HxXKYKg7vohx2/OupUN/4Sd02Ev3PBJ5q0gtjdcvXb0ErCva8jNHWfe/v5sU3UKjIB/uxOhc+TDOnhqffj9pRA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.2.tgz", + "integrity": "sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==", "engines": { - "node": ">=16.9.0" + "node": ">=16.11.0" } }, "node_modules/@discordjs/ws": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-0.8.3.tgz", - "integrity": "sha512-hcYtppanjHecbdNyCKQNH2I4RP9UrphDgmRgLYrATEQF1oo4sYSve7ZmGsBEXSzH72MO2tBPdWSThunbxUVk0g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.0.2.tgz", + "integrity": "sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==", "dependencies": { - "@discordjs/collection": "^1.5.1", - "@discordjs/rest": "^1.7.1", - "@discordjs/util": "^0.3.1", + "@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.4", - "@vladfrangu/async_event_emitter": "^2.2.1", - "discord-api-types": "^0.37.41", - "tslib": "^2.5.0", - "ws": "^8.13.0" + "@types/ws": "^8.5.9", + "@vladfrangu/async_event_emitter": "^2.2.2", + "discord-api-types": "0.37.61", + "tslib": "^2.6.2", + "ws": "^8.14.2" }, "engines": { - "node": ">=16.9.0" + "node": ">=16.11.0" + } + }, + "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==", + "engines": { + "node": ">=18" + } + }, + "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/@jest/types": { @@ -416,9 +441,9 @@ } }, "node_modules/@sapphire/shapeshift": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.2.tgz", - "integrity": "sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==", + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.3.tgz", + "integrity": "sha512-WzKJSwDYloSkHoBbE8rkRW8UNKJiSRJ/P8NqJ5iVq7U2Yr/kriIBx2hW+wj2Z5e5EnXL1hgYomgaFsdK6b+zqQ==", "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" @@ -499,11 +524,6 @@ "yarn": ">= 1.3.2" } }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -769,9 +789,9 @@ "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==" }, "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", + "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", "dependencies": { "@types/node": "*" } @@ -3257,32 +3277,32 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.47", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.47.tgz", - "integrity": "sha512-rNif8IAv6duS2z47BMXq/V9kkrLfkAoiwpFY3sLxxbyKprk065zqf3HLTg4bEoxRSmi+Lhc7yqGDrG8C3j8GFA==" + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" }, "node_modules/discord.js": { - "version": "14.11.0", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.11.0.tgz", - "integrity": "sha512-CkueWYFQ28U38YPR8HgsBR/QT35oPpMbEsTNM30Fs8loBIhnA4s70AwQEoy6JvLcpWWJO7GY0y2BUzZmuBMepQ==", + "version": "14.14.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz", + "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==", "dependencies": { - "@discordjs/builders": "^1.6.3", - "@discordjs/collection": "^1.5.1", - "@discordjs/formatters": "^0.3.1", - "@discordjs/rest": "^1.7.1", - "@discordjs/util": "^0.3.1", - "@discordjs/ws": "^0.8.3", - "@sapphire/snowflake": "^3.4.2", - "@types/ws": "^8.5.4", - "discord-api-types": "^0.37.41", - "fast-deep-equal": "^3.1.3", - "lodash.snakecase": "^4.1.1", - "tslib": "^2.5.0", - "undici": "^5.22.0", - "ws": "^8.13.0" + "@discordjs/builders": "^1.7.0", + "@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", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "2.6.2", + "undici": "5.27.2", + "ws": "8.14.2" }, "engines": { - "node": ">=16.9.0" + "node": ">=16.11.0" } }, "node_modules/distributions": { @@ -3925,22 +3945,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/file-type": { - "version": "18.5.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.5.0.tgz", - "integrity": "sha512-yvpl5U868+V6PqXHMmsESpg6unQ5GfnPssl4dxdJudBrr9qy7Fddt7EVX1VLlddFfe8Gj9N7goCZH22FXuSQXQ==", - "dependencies": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -5769,6 +5773,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/magic-bytes.js": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.5.0.tgz", + "integrity": "sha512-wJkXvutRbNWcc37tt5j1HyOK1nosspdh3dj6LUYYAvF6JYNqs53IfRvK9oEpcwiDA1NdoIi64yAMfdivPeVAyw==" + }, "node_modules/magic-string": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.1.tgz", @@ -7118,18 +7127,6 @@ "node": ">=0.12" } }, - "node_modules/peek-readable": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -7706,34 +7703,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -8873,22 +8842,6 @@ "node": ">=0.10.0" } }, - "node_modules/strtok3": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/subarg": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", @@ -9151,22 +9104,6 @@ "node": ">=0.6" } }, - "node_modules/token-types": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -9276,9 +9213,9 @@ } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/ttest": { "version": "3.0.0", @@ -9716,11 +9653,11 @@ "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==" }, "node_modules/undici": { - "version": "5.22.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", - "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", + "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": { - "busboy": "^1.6.0" + "@fastify/busboy": "^2.0.0" }, "engines": { "node": ">=14.0" @@ -10091,9 +10028,9 @@ } }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "engines": { "node": ">=10.0.0" }, diff --git a/backend/package.json b/backend/package.json index 2ff53482..44101e51 100644 --- a/backend/package.json +++ b/backend/package.json @@ -35,7 +35,7 @@ "cors": "^2.8.5", "cross-env": "^7.0.3", "deep-diff": "^1.0.2", - "discord.js": "^14.11.0", + "discord.js": "^14.14.1", "dotenv": "^4.0.0", "emoji-regex": "^8.0.0", "erlpack": "github:discord/erlpack", diff --git a/backend/src/plugins/Automod/actions/startThread.ts b/backend/src/plugins/Automod/actions/startThread.ts index 25b840e1..66d5ec3a 100644 --- a/backend/src/plugins/Automod/actions/startThread.ts +++ b/backend/src/plugins/Automod/actions/startThread.ts @@ -57,7 +57,13 @@ export const StartThreadAction = automodAction({ for (const threadContext of threads) { const channel = pluginData.guild.channels.cache.get(threadContext.message!.channel_id); - if (!channel || !("threads" in channel) || channel.type === ChannelType.GuildForum) continue; + if ( + !channel || + !("threads" in channel) || + channel.type === ChannelType.GuildForum || + channel.type === ChannelType.GuildMedia + ) + continue; const renderThreadName = async (str: string) => renderTemplate( @@ -90,10 +96,7 @@ export const StartThreadAction = automodAction({ .create({ ...threadOptions, type: actionConfig.private ? ChannelType.PrivateThread : ChannelType.PublicThread, - startMessage: - !actionConfig.private && guild.features.includes(GuildFeature.PrivateThreads) - ? threadContext.message!.id - : undefined, + startMessage: !actionConfig.private ? threadContext.message!.id : undefined, }) .catch(() => undefined); } From f1791fac4452d63205fda3f0130cec1d46b96bdd Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 13:25:58 +0000 Subject: [PATCH 07/51] redo username check, yeet discrims entirely Signed-off-by: GitHub <noreply@github.com> --- backend/src/utils.ts | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index f29d990c..af3b5ad8 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1221,26 +1221,12 @@ export function resolveUserId(bot: Client, value: string) { return mentionMatch[1]; } - // A non-mention, full username? - const oldUsernameMatch = value.match(/^@?([^#]+)#(\d{4})$/); - if (oldUsernameMatch) { - const profiler = getProfiler(); - const start = performance.now(); - const user = bot.users.cache.find( - (u) => u.username === oldUsernameMatch[1] && u.discriminator === oldUsernameMatch[2], - ); - profiler?.addDataPoint("utils:resolveUserId:usernameMatch", performance.now() - start); - if (user) { - return user.id; - } - } - - // new usernames system - const usernameMatch = value.match(/^@?([^#]+)$/); + // a username + const usernameMatch = value.match(/^@?(\S{3,})$/); if (usernameMatch) { const profiler = getProfiler(); const start = performance.now(); - const user = bot.users.cache.find((u) => u.username === usernameMatch[1]); + const user = bot.users.cache.find((u) => u.tag === usernameMatch[1]); profiler?.addDataPoint("utils:resolveUserId:usernameMatch", performance.now() - start); if (user) { return user.id; From 4c788bc321795d6d4cfb160945017d9a3475ceed Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 13:38:42 +0000 Subject: [PATCH 08/51] some transforms Signed-off-by: GitHub <noreply@github.com> --- .../Automod/functions/matchMultipleTextTypesOnMessage.ts | 2 +- .../plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts | 7 +++---- .../src/plugins/Utility/functions/getInviteInfoEmbed.ts | 2 +- .../src/plugins/Utility/functions/getMessageInfoEmbed.ts | 2 +- backend/src/plugins/Utility/functions/getUserInfoEmbed.ts | 7 +++---- backend/src/utils.ts | 7 +++++-- backend/src/utils/templateSafeObjects.ts | 2 ++ 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts b/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts index 9e51c490..f0eee5ec 100644 --- a/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts +++ b/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts @@ -42,7 +42,7 @@ export async function* matchMultipleTextTypesOnMessage( } if (trigger.match_visible_names) { - yield ["visiblename", member.nickname || msg.data.author.username]; + yield ["visiblename", member.nickname || member.user.globalName || msg.data.author.username]; } if (trigger.match_usernames) { diff --git a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts index 13a72a87..f7350b5f 100644 --- a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts +++ b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts @@ -68,10 +68,9 @@ export const ArchiveChannelCmd = channelArchiverCmd({ for (const message of messages.values()) { const ts = moment.utc(message.createdTimestamp).format("YYYY-MM-DD HH:mm:ss"); - let content = `[${ts}] [${message.author.id}] [${renderUsername( - message.author.username, - message.author.discriminator, - )}]: ${message.content || "<no text content>"}`; + let content = `[${ts}] [${message.author.id}] [${renderUsername(message.author)}]: ${ + message.content || "<no text content>" + }`; if (message.attachments.size) { if (args["attachment-channel"]) { diff --git a/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts b/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts index 12c97a8a..b844b937 100644 --- a/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts @@ -85,7 +85,7 @@ export async function getInviteInfoEmbed( embed.fields.push({ name: preEmbedPadding + "Invite creator", value: trimLines(` - Name: **${renderUsername(invite.inviter.username, invite.inviter.discriminator)}** + Name: **${renderUsername(invite.inviter)}** ID: \`${invite.inviter.id}\` Mention: <@!${invite.inviter.id}> `), diff --git a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts index 1568a3e3..3a49a80d 100644 --- a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts @@ -71,7 +71,7 @@ export async function getMessageInfoEmbed( embed.fields.push({ name: preEmbedPadding + "Author information", value: trimLines(` - Name: **${renderUsername(message.author.username, message.author.discriminator)}** + Name: **${renderUsername(message.author)}** ID: \`${message.author.id}\` Created: **<t:${Math.round(message.author.createdTimestamp / 1000)}:R>** ${authorJoinedAtTS ? `Joined: **<t:${Math.round(authorJoinedAtTS / 1000)}:R>**` : ""} diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts index ef8d2320..2de8b4db 100644 --- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts @@ -43,7 +43,7 @@ export async function getUserInfoEmbed( const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); embed.author = { - name: `${user.bot ? "Bot" : "User"}: ${renderUsername(user.username, user.discriminator)}`, + name: `${user.bot ? "Bot" : "User"}: ${renderUsername(user)}`, }; const avatarURL = user.displayAvatarURL(); @@ -72,9 +72,8 @@ export async function getUserInfoEmbed( } const userInfoLines = [`ID: \`${user.id}\``, `Username: **${user.username}**`]; - if (user.discriminator !== "0") { - userInfoLines.push(`Discriminator: **${user.discriminator}**`); - } + if (user.discriminator !== "0") userInfoLines.push(`Discriminator: **${user.discriminator}**`); + if (user.globalName) userInfoLines.push(`Display Name: **${user.globalName}**`); userInfoLines.push(`Created: **<t:${Math.round(user.createdTimestamp / 1000)}:R>**`); userInfoLines.push(`Mention: <@!${user.id}>`); diff --git a/backend/src/utils.ts b/backend/src/utils.ts index af3b5ad8..ee6b556c 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1603,8 +1603,11 @@ export function isTruthy<T>(value: T): value is Exclude<T, false | null | undefi export const DBDateFormat = "YYYY-MM-DD HH:mm:ss"; -export function renderUsername(username: string, discriminator: string): string { - if (discriminator === "0") { +export function renderUsername(username: User): string; +export function renderUsername(username: string, discriminator?: string): string; +export function renderUsername(username: string | User, discriminator?: string): string { + if (username instanceof User) return username.tag; + if (discriminator === "0" || discriminator === "0000") { return username; } return `${username}#${discriminator}`; diff --git a/backend/src/utils/templateSafeObjects.ts b/backend/src/utils/templateSafeObjects.ts index 67517f19..fbd27754 100644 --- a/backend/src/utils/templateSafeObjects.ts +++ b/backend/src/utils/templateSafeObjects.ts @@ -49,6 +49,7 @@ export class TemplateSafeUser extends TemplateSafeValueContainer { id: Snowflake | string; username: string; discriminator: string; + globalName?: string; mention: string; tag: string; avatarURL?: string; @@ -257,6 +258,7 @@ export function userToTemplateSafeUser(user: User | UnknownUser): TemplateSafeUs id: user.id, username: user.username, discriminator: user.discriminator, + globalName: user.globalName, mention: `<@${user.id}>`, tag: user.tag, avatarURL: user.displayAvatarURL?.(), From 1579c3ec25398a5341af0b66a4f68626ddd92f70 Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 13:40:05 +0000 Subject: [PATCH 09/51] disable status-search arg Signed-off-by: GitHub <noreply@github.com> --- backend/src/plugins/Utility/commands/SearchCmd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/Utility/commands/SearchCmd.ts b/backend/src/plugins/Utility/commands/SearchCmd.ts index 777bb890..fb4d2ad1 100644 --- a/backend/src/plugins/Utility/commands/SearchCmd.ts +++ b/backend/src/plugins/Utility/commands/SearchCmd.ts @@ -15,7 +15,7 @@ export const searchCmdSignature = { export: ct.switchOption({ def: false, shortcut: "e" }), ids: ct.switchOption(), regex: ct.switchOption({ def: false, shortcut: "re" }), - "status-search": ct.switchOption({ def: false, shortcut: "ss" }), + // "status-search": ct.switchOption({ def: false, shortcut: "ss" }), }; export const SearchCmd = utilityCmd({ From c5704131cefd1b17da76ef179c2da81855c3fb25 Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 13:44:42 +0000 Subject: [PATCH 10/51] support media channels Signed-off-by: GitHub <noreply@github.com> --- backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts | 1 + backend/src/plugins/Utility/functions/getServerInfoEmbed.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts index ee006f5f..d41e3e44 100644 --- a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts @@ -56,6 +56,7 @@ export async function getChannelInfoEmbed( [ChannelType.AnnouncementThread]: "News Thread channel", [ChannelType.GuildDirectory]: "Hub channel", [ChannelType.GuildForum]: "Forum channel", + [ChannelType.GuildMedia]: "Media channel", }[channel.type] ?? "Channel"; embed.author = { diff --git a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts index d0202db7..afcefb80 100644 --- a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts @@ -149,12 +149,16 @@ export async function getServerInfoEmbed( const textChannels = thisServer.channels.cache.filter((channel) => channel.type === ChannelType.GuildText); const voiceChannels = thisServer.channels.cache.filter((channel) => channel.type === ChannelType.GuildVoice); const forumChannels = thisServer.channels.cache.filter((channel) => channel.type === ChannelType.GuildForum); + const mediaChannels = thisServer.channels.cache.filter((channel) => channel.type === ChannelType.GuildMedia); const threadChannelsText = thisServer.channels.cache.filter( (channel) => channel.isThread() && channel.parent?.type !== ChannelType.GuildForum, ); const threadChannelsForums = thisServer.channels.cache.filter( (channel) => channel.isThread() && channel.parent?.type === ChannelType.GuildForum, ); + const threadChannelsMedia = thisServer.channels.cache.filter( + (channel) => channel.isThread() && channel.parent?.type === ChannelType.GuildMedia, + ); const announcementChannels = thisServer.channels.cache.filter( (channel) => channel.type === ChannelType.GuildAnnouncement, ); @@ -169,6 +173,7 @@ export async function getServerInfoEmbed( Categories: **${categories.size}** Text: **${textChannels.size}** (**${threadChannelsText.size} threads**) Forums: **${forumChannels.size}** (**${threadChannelsForums.size} threads**) + Media: **${mediaChannels.size}** (**${threadChannelsMedia.size} threads**) Announcement: **${announcementChannels.size}** Voice: **${voiceChannels.size}** Stage: **${stageChannels.size}** From 1e66f235b2f447ef07af2d17c1cda1108872a1dc Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 14:06:23 +0000 Subject: [PATCH 11/51] media channel icon Signed-off-by: GitHub <noreply@github.com> --- backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts index d41e3e44..f601dd5c 100644 --- a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts @@ -19,6 +19,8 @@ const PRIVATE_THREAD_ICON = const FORUM_CHANNEL_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/1091681253364875294/forum-channel-icon.png"; +const MEDIA_CHANNEL_ICON = "https://cdn.discordapp.com/attachments/876134205229252658/1178335624940490792/media.png"; + export async function getChannelInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, channelId: string, @@ -42,6 +44,7 @@ export async function getChannelInfoEmbed( [ChannelType.PrivateThread]: PRIVATE_THREAD_ICON, [ChannelType.AnnouncementThread]: PUBLIC_THREAD_ICON, [ChannelType.GuildForum]: FORUM_CHANNEL_ICON, + [ChannelType.GuildMedia]: MEDIA_CHANNEL_ICON, }[channel.type] ?? TEXT_CHANNEL_ICON; const channelType = From ba4a2b45b8c73fc1ac201365e7cf9bbf7b9ea8bf Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 14:09:34 +0000 Subject: [PATCH 12/51] remove useless feature check Signed-off-by: GitHub <noreply@github.com> --- backend/src/plugins/Automod/actions/startThread.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/backend/src/plugins/Automod/actions/startThread.ts b/backend/src/plugins/Automod/actions/startThread.ts index 66d5ec3a..970e9ec8 100644 --- a/backend/src/plugins/Automod/actions/startThread.ts +++ b/backend/src/plugins/Automod/actions/startThread.ts @@ -1,10 +1,4 @@ -import { - ChannelType, - GuildFeature, - GuildTextThreadCreateOptions, - ThreadAutoArchiveDuration, - ThreadChannel, -} from "discord.js"; +import { ChannelType, GuildTextThreadCreateOptions, ThreadAutoArchiveDuration, ThreadChannel } from "discord.js"; import * as t from "io-ts"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { MINUTES, convertDelayStringToMS, noop, tDelayString, tNullable } from "../../../utils"; @@ -77,10 +71,7 @@ export const StartThreadAction = automodAction({ const threadOptions: GuildTextThreadCreateOptions<unknown> = { name: threadName, autoArchiveDuration: autoArchive, - startMessage: - !actionConfig.private && guild.features.includes(GuildFeature.PrivateThreads) - ? threadContext.message!.id - : undefined, + startMessage: !actionConfig.private ? threadContext.message!.id : undefined, }; let thread: ThreadChannel | undefined; From 10bb0b67bc22a3f8dfef1c65809a9035143ba51a Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 14:53:54 +0000 Subject: [PATCH 13/51] some more patches thanks to ruby Signed-off-by: GitHub <noreply@github.com> --- .../plugins/Automod/triggers/threadArchive.ts | 4 ++-- .../plugins/Automod/triggers/threadCreate.ts | 3 ++- .../plugins/Automod/triggers/threadDelete.ts | 3 ++- .../Automod/triggers/threadUnarchive.ts | 4 ++-- .../plugins/BotControl/commands/ServersCmd.ts | 6 +++-- .../src/plugins/Cases/functions/createCase.ts | 8 +++---- .../plugins/Cases/functions/createCaseNote.ts | 4 ++-- .../InternalPoster/functions/sendMessage.ts | 2 +- .../ModActions/commands/CasesModCmd.ts | 12 +++++----- .../ModActions/commands/CasesUserCmd.ts | 22 ++++++++++++++----- .../src/plugins/Post/util/actualPostCmd.ts | 4 ++-- .../util/createStarboardEmbedFromMessage.ts | 6 ++--- .../src/plugins/Utility/commands/AboutCmd.ts | 4 ++-- .../src/plugins/Utility/commands/AvatarCmd.ts | 8 +++---- .../Utility/functions/getUserInfoEmbed.ts | 5 +---- backend/src/plugins/Utility/search.ts | 10 ++++----- backend/src/utils.ts | 9 +++++--- backend/src/utils/templateSafeObjects.ts | 7 +++--- 18 files changed, 69 insertions(+), 52 deletions(-) diff --git a/backend/src/plugins/Automod/triggers/threadArchive.ts b/backend/src/plugins/Automod/triggers/threadArchive.ts index 0e65b10a..8a692f6d 100644 --- a/backend/src/plugins/Automod/triggers/threadArchive.ts +++ b/backend/src/plugins/Automod/triggers/threadArchive.ts @@ -1,6 +1,6 @@ import { User, escapeBold, type Snowflake } from "discord.js"; import * as t from "io-ts"; -import { tNullable } from "../../../utils"; +import { renderUsername, tNullable } from "../../../utils"; import { automodTrigger } from "../helpers"; interface ThreadArchiveResult { @@ -48,7 +48,7 @@ export const ThreadArchiveTrigger = automodTrigger<ThreadArchiveResult>()({ const parentName = matchResult.extra.matchedThreadParentName; const base = `Thread **#${threadName}** (\`${threadId}\`) has been archived in the **#${parentName}** (\`${parentId}\`) channel`; if (threadOwner) { - return `${base} by **${escapeBold(threadOwner.tag)}** (\`${threadOwner.id}\`)`; + return `${base} by **${escapeBold(renderUsername(threadOwner.tag))}** (\`${threadOwner.id}\`)`; } return base; }, diff --git a/backend/src/plugins/Automod/triggers/threadCreate.ts b/backend/src/plugins/Automod/triggers/threadCreate.ts index 7b8aca71..dc613068 100644 --- a/backend/src/plugins/Automod/triggers/threadCreate.ts +++ b/backend/src/plugins/Automod/triggers/threadCreate.ts @@ -1,5 +1,6 @@ import { User, escapeBold, type Snowflake } from "discord.js"; import * as t from "io-ts"; +import { renderUsername } from "../../../utils.js"; import { automodTrigger } from "../helpers"; interface ThreadCreateResult { @@ -40,7 +41,7 @@ export const ThreadCreateTrigger = automodTrigger<ThreadCreateResult>()({ const parentName = matchResult.extra.matchedThreadParentName; const base = `Thread **#${threadName}** (\`${threadId}\`) has been created in the **#${parentName}** (\`${parentId}\`) channel`; if (threadOwner) { - return `${base} by **${escapeBold(threadOwner.tag)}** (\`${threadOwner.id}\`)`; + return `${base} by **${escapeBold(renderUsername(threadOwner.tag))}** (\`${threadOwner.id}\`)`; } return base; }, diff --git a/backend/src/plugins/Automod/triggers/threadDelete.ts b/backend/src/plugins/Automod/triggers/threadDelete.ts index 489b5b4c..f27a1d0b 100644 --- a/backend/src/plugins/Automod/triggers/threadDelete.ts +++ b/backend/src/plugins/Automod/triggers/threadDelete.ts @@ -1,5 +1,6 @@ import { User, escapeBold, type Snowflake } from "discord.js"; import * as t from "io-ts"; +import { renderUsername } from "../../../utils.js"; import { automodTrigger } from "../helpers"; interface ThreadDeleteResult { @@ -40,7 +41,7 @@ export const ThreadDeleteTrigger = automodTrigger<ThreadDeleteResult>()({ const parentName = matchResult.extra.matchedThreadParentName; if (threadOwner) { return `Thread **#${threadName ?? "Unknown"}** (\`${threadId}\`) created by **${escapeBold( - threadOwner.tag, + renderUsername(threadOwner.tag), )}** (\`${threadOwner.id}\`) in the **#${parentName}** (\`${parentId}\`) channel has been deleted`; } return `Thread **#${ diff --git a/backend/src/plugins/Automod/triggers/threadUnarchive.ts b/backend/src/plugins/Automod/triggers/threadUnarchive.ts index f6047f48..94c69ace 100644 --- a/backend/src/plugins/Automod/triggers/threadUnarchive.ts +++ b/backend/src/plugins/Automod/triggers/threadUnarchive.ts @@ -1,6 +1,6 @@ import { User, escapeBold, type Snowflake } from "discord.js"; import * as t from "io-ts"; -import { tNullable } from "../../../utils"; +import { renderUsername, tNullable } from "../../../utils"; import { automodTrigger } from "../helpers"; interface ThreadUnarchiveResult { @@ -48,7 +48,7 @@ export const ThreadUnarchiveTrigger = automodTrigger<ThreadUnarchiveResult>()({ const parentName = matchResult.extra.matchedThreadParentName; const base = `Thread **#${threadName}** (\`${threadId}\`) has been unarchived in the **#${parentName}** (\`${parentId}\`) channel`; if (threadOwner) { - return `${base} by **${escapeBold(threadOwner.tag)}** (\`${threadOwner.id}\`)`; + return `${base} by **${escapeBold(renderUsername(threadOwner.tag))}** (\`${threadOwner.id}\`)`; } return base; }, diff --git a/backend/src/plugins/BotControl/commands/ServersCmd.ts b/backend/src/plugins/BotControl/commands/ServersCmd.ts index 23146d21..5cb62433 100644 --- a/backend/src/plugins/BotControl/commands/ServersCmd.ts +++ b/backend/src/plugins/BotControl/commands/ServersCmd.ts @@ -1,7 +1,7 @@ import escapeStringRegexp from "escape-string-regexp"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { isStaffPreFilter } from "../../../pluginUtils"; -import { createChunkedMessage, getUser, sorter } from "../../../utils"; +import { createChunkedMessage, getUser, renderUsername, sorter } from "../../../utils"; import { botControlCmd } from "../types"; export const ServersCmd = botControlCmd({ @@ -48,7 +48,9 @@ export const ServersCmd = botControlCmd({ const lines = filteredGuilds.map((g) => { const paddedId = g.id.padEnd(longestId, " "); const owner = getUser(pluginData.client, g.ownerId); - return `\`${paddedId}\` **${g.name}** (${g.memberCount} members) (owner **${owner.tag}** \`${owner.id}\`)`; + return `\`${paddedId}\` **${g.name}** (${g.memberCount} members) (owner **${renderUsername(owner.tag)}** \`${ + owner.id + }\`)`; }); createChunkedMessage(msg.channel, lines.join("\n")); } else { diff --git a/backend/src/plugins/Cases/functions/createCase.ts b/backend/src/plugins/Cases/functions/createCase.ts index c16d937b..70717f51 100644 --- a/backend/src/plugins/Cases/functions/createCase.ts +++ b/backend/src/plugins/Cases/functions/createCase.ts @@ -1,23 +1,23 @@ import type { Snowflake } from "discord.js"; import { GuildPluginData } from "knub"; import { logger } from "../../../logger"; -import { renderUserUsername, resolveUser } from "../../../utils"; +import { renderUsername, resolveUser } from "../../../utils"; import { CaseArgs, CasesPluginType } from "../types"; import { createCaseNote } from "./createCaseNote"; import { postCaseToCaseLogChannel } from "./postToCaseLogChannel"; export async function createCase(pluginData: GuildPluginData<CasesPluginType>, args: CaseArgs) { const user = await resolveUser(pluginData.client, args.userId); - const userName = renderUserUsername(user); + const userName = renderUsername(user.username, user.discriminator); const mod = await resolveUser(pluginData.client, args.modId); - const modName = mod.tag; + const modName = renderUsername(mod.username, mod.discriminator); let ppName: string | null = null; let ppId: Snowflake | null = null; if (args.ppId) { const pp = await resolveUser(pluginData.client, args.ppId); - ppName = pp.tag; + ppName = renderUsername(pp.username, pp.discriminator); ppId = pp.id; } diff --git a/backend/src/plugins/Cases/functions/createCaseNote.ts b/backend/src/plugins/Cases/functions/createCaseNote.ts index 92520b7d..71b11302 100644 --- a/backend/src/plugins/Cases/functions/createCaseNote.ts +++ b/backend/src/plugins/Cases/functions/createCaseNote.ts @@ -1,6 +1,6 @@ import { GuildPluginData } from "knub"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; -import { UnknownUser, resolveUser } from "../../../utils"; +import { UnknownUser, renderUsername, resolveUser } from "../../../utils"; import { CaseNoteArgs, CasesPluginType } from "../types"; import { postCaseToCaseLogChannel } from "./postToCaseLogChannel"; import { resolveCaseId } from "./resolveCaseId"; @@ -16,7 +16,7 @@ export async function createCaseNote(pluginData: GuildPluginData<CasesPluginType throw new RecoverablePluginError(ERRORS.INVALID_USER); } - const modName = mod.tag; + const modName = renderUsername(mod); let body = args.body; diff --git a/backend/src/plugins/InternalPoster/functions/sendMessage.ts b/backend/src/plugins/InternalPoster/functions/sendMessage.ts index ec811240..3d4d6424 100644 --- a/backend/src/plugins/InternalPoster/functions/sendMessage.ts +++ b/backend/src/plugins/InternalPoster/functions/sendMessage.ts @@ -48,7 +48,7 @@ export async function sendMessage( ...content, ...(pluginData.client.user && { username: pluginData.client.user.username, - avatarURL: pluginData.client.user.avatarURL() || pluginData.client.user.defaultAvatarURL, + avatarURL: pluginData.client.user.displayAvatarURL(), }), }) .then((apiMessage) => ({ diff --git a/backend/src/plugins/ModActions/commands/CasesModCmd.ts b/backend/src/plugins/ModActions/commands/CasesModCmd.ts index 5b0e3273..a911091d 100644 --- a/backend/src/plugins/ModActions/commands/CasesModCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesModCmd.ts @@ -1,7 +1,7 @@ -import { APIEmbed, User } from "discord.js"; +import { APIEmbed, GuildMember, User } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage } from "../../../pluginUtils"; -import { emptyEmbedValue, resolveUser, trimLines } from "../../../utils"; +import { emptyEmbedValue, renderUsername, resolveMember, resolveUser, trimLines } from "../../../utils"; import { asyncMap } from "../../../utils/async"; import { createPaginatedMessage } from "../../../utils/createPaginatedMessage"; import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; @@ -28,8 +28,10 @@ export const CasesModCmd = modActionsCmd({ async run({ pluginData, message: msg, args }) { const modId = args.mod || msg.author.id; - const mod = await resolveUser(pluginData.client, modId); - const modName = mod instanceof User ? mod.tag : modId; + const mod = + (await resolveMember(pluginData.client, pluginData.guild, modId)) || + (await resolveUser(pluginData.client, modId)); + const modName = mod instanceof User ? renderUsername(mod) : modId; const casesPlugin = pluginData.getPlugin(CasesPlugin); const totalCases = await casesPlugin.getTotalCasesByMod(modId); @@ -57,7 +59,7 @@ export const CasesModCmd = modActionsCmd({ const embed = { author: { name: title, - icon_url: mod instanceof User ? mod.displayAvatarURL() : undefined, + icon_url: mod instanceof User || mod instanceof GuildMember ? mod.displayAvatarURL() : undefined, }, fields: [ ...getChunkedEmbedFields(emptyEmbedValue, lines.join("\n")), diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts index 069ad31f..05ab65eb 100644 --- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts @@ -1,9 +1,17 @@ -import { APIEmbed, User } from "discord.js"; +import { APIEmbed } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { CaseTypes } from "../../../data/CaseTypes"; import { sendErrorMessage } from "../../../pluginUtils"; import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; -import { UnknownUser, chunkArray, emptyEmbedValue, renderUserUsername, resolveUser, trimLines } from "../../../utils"; +import { + UnknownUser, + chunkArray, + emptyEmbedValue, + renderUsername, + resolveMember, + resolveUser, + trimLines, +} from "../../../utils"; import { asyncMap } from "../../../utils/async"; import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; import { getGuildPrefix } from "../../../utils/getGuildPrefix"; @@ -35,8 +43,10 @@ export const CasesUserCmd = modActionsCmd({ ], async run({ pluginData, message: msg, args }) { - const user = await resolveUser(pluginData.client, args.user); - if (!user.id) { + const user = + (await resolveMember(pluginData.client, pluginData.guild, args.user)) || + (await resolveUser(pluginData.client, args.user)); + if (!user.id || user instanceof UnknownUser) { sendErrorMessage(pluginData, msg.channel, `User not found`); return; } @@ -62,7 +72,7 @@ export const CasesUserCmd = modActionsCmd({ const hiddenCases = cases.filter((c) => c.is_hidden); const userName = - user instanceof UnknownUser && cases.length ? cases[cases.length - 1].user_name : renderUserUsername(user); + user instanceof UnknownUser && cases.length ? cases[cases.length - 1].user_name : renderUsername(user); if (cases.length === 0) { msg.channel.send(`No cases found for **${userName}**`); @@ -123,7 +133,7 @@ export const CasesUserCmd = modActionsCmd({ lineChunks.length === 1 ? `Cases for ${userName} (${lines.length} total)` : `Cases ${chunkStart}–${chunkEnd} of ${lines.length} for ${userName}`, - icon_url: user instanceof User ? user.displayAvatarURL() : undefined, + icon_url: user.displayAvatarURL(), }, fields: [ ...getChunkedEmbedFields(emptyEmbedValue, linesInChunk.join("\n")), diff --git a/backend/src/plugins/Post/util/actualPostCmd.ts b/backend/src/plugins/Post/util/actualPostCmd.ts index c73a672b..29c3d5ec 100644 --- a/backend/src/plugins/Post/util/actualPostCmd.ts +++ b/backend/src/plugins/Post/util/actualPostCmd.ts @@ -4,7 +4,7 @@ import { GuildPluginData } from "knub"; import moment from "moment-timezone"; import { registerUpcomingScheduledPost } from "../../../data/loops/upcomingScheduledPostsLoop"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { DBDateFormat, MINUTES, StrictMessageContent, errorMessage } from "../../../utils"; +import { DBDateFormat, MINUTES, StrictMessageContent, errorMessage, renderUsername } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; import { PostPluginType } from "../types"; @@ -122,7 +122,7 @@ export async function actualPostCmd( const post = await pluginData.state.scheduledPosts.create({ author_id: msg.author.id, - author_name: msg.author.tag, + author_name: renderUsername(msg.author), channel_id: targetChannel.id, content, attachments: [...msg.attachments.values()], diff --git a/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts b/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts index 5f028c0a..5126f20f 100644 --- a/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts +++ b/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts @@ -1,6 +1,6 @@ import { GuildChannel, Message } from "discord.js"; import path from "path"; -import { EMPTY_CHAR, EmbedWith } from "../../../utils"; +import { EMPTY_CHAR, EmbedWith, renderUsername } from "../../../utils"; const imageAttachmentExtensions = ["jpeg", "jpg", "png", "gif", "webp"]; const audioAttachmentExtensions = ["wav", "mp3", "m4a"]; @@ -18,7 +18,7 @@ export function createStarboardEmbedFromMessage( text: `#${(msg.channel as GuildChannel).name}`, }, author: { - name: msg.author.tag, + name: renderUsername(msg.author), }, fields: [], timestamp: msg.createdAt.toISOString(), @@ -28,7 +28,7 @@ export function createStarboardEmbedFromMessage( embed.color = color; } - embed.author.icon_url = msg.author.displayAvatarURL(); + embed.author.icon_url = (msg.member || msg.author).displayAvatarURL(); // The second condition here checks for messages with only an image link that is then embedded. // The message content in that case is hidden by the Discord client, so we hide it here too. diff --git a/backend/src/plugins/Utility/commands/AboutCmd.ts b/backend/src/plugins/Utility/commands/AboutCmd.ts index 53408d71..d4188ad7 100644 --- a/backend/src/plugins/Utility/commands/AboutCmd.ts +++ b/backend/src/plugins/Utility/commands/AboutCmd.ts @@ -100,8 +100,8 @@ export const AboutCmd = utilityCmd({ } // Use the bot avatar as the embed image - if (pluginData.client.user!.avatarURL()) { - aboutEmbed.thumbnail = { url: pluginData.client.user!.avatarURL()! }; + if (pluginData.client.user!.displayAvatarURL()) { + aboutEmbed.thumbnail = { url: pluginData.client.user!.displayAvatarURL()! }; } msg.channel.send({ embeds: [aboutEmbed] }); diff --git a/backend/src/plugins/Utility/commands/AvatarCmd.ts b/backend/src/plugins/Utility/commands/AvatarCmd.ts index ef44a2cb..b1215df6 100644 --- a/backend/src/plugins/Utility/commands/AvatarCmd.ts +++ b/backend/src/plugins/Utility/commands/AvatarCmd.ts @@ -1,7 +1,7 @@ import { APIEmbed, ImageFormat } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage } from "../../../pluginUtils"; -import { UnknownUser, renderUserUsername } from "../../../utils"; +import { UnknownUser, renderUsername } from "../../../utils"; import { utilityCmd } from "../types"; export const AvatarCmd = utilityCmd({ @@ -10,17 +10,17 @@ export const AvatarCmd = utilityCmd({ permission: "can_avatar", signature: { - user: ct.resolvedUserLoose({ required: false }), + user: ct.resolvedMember({ required: false }) || ct.resolvedUserLoose({ required: false }), }, async run({ message: msg, args, pluginData }) { - const user = args.user || msg.author; + const user = args.user || msg.member || msg.author; if (!(user instanceof UnknownUser)) { const embed: APIEmbed = { image: { url: user.displayAvatarURL({ extension: ImageFormat.PNG, size: 2048 }), }, - title: `Avatar of ${renderUserUsername(user)}:`, + title: `Avatar of ${renderUsername(user)}:`, }; msg.channel.send({ embeds: [embed] }); } else { diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts index 2de8b4db..58d34d56 100644 --- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts @@ -13,7 +13,6 @@ import { trimLines, UnknownUser, } from "../../../utils"; -import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; import { UtilityPluginType } from "../types"; const MAX_ROLES_TO_DISPLAY = 15; @@ -40,13 +39,11 @@ export async function getUserInfoEmbed( fields: [], }; - const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); - embed.author = { name: `${user.bot ? "Bot" : "User"}: ${renderUsername(user)}`, }; - const avatarURL = user.displayAvatarURL(); + const avatarURL = (member || user).displayAvatarURL(); embed.author.icon_url = avatarURL; if (compact) { diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index 19710b58..632a85a7 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -14,7 +14,7 @@ import { ArgsFromSignatureOrArray, GuildPluginData } from "knub"; import moment from "moment-timezone"; import { RegExpRunner, allowTimeout } from "../../RegExpRunner"; import { getBaseUrl, sendErrorMessage } from "../../pluginUtils"; -import { MINUTES, multiSorter, renderUserUsername, sorter, trimLines } from "../../utils"; +import { MINUTES, multiSorter, renderUsername, sorter, trimLines } from "../../utils"; import { asyncFilter } from "../../utils/async"; import { hasDiscordPermissions } from "../../utils/hasDiscordPermissions"; import { InvalidRegexError, inputPatternToRegExp } from "../../validatorUtils"; @@ -381,7 +381,7 @@ async function performMemberSearch( return true; } - const fullUsername = renderUserUsername(member.user); + const fullUsername = renderUsername(member.user); if (await execRegExp(queryRegex, fullUsername).catch(allowTimeout)) return true; return false; @@ -448,7 +448,7 @@ async function performBanSearch( const execRegExp = getOptimizedRegExpRunner(pluginData, isSafeRegex); matchingBans = await asyncFilter(matchingBans, async (user) => { - const fullUsername = renderUserUsername(user); + const fullUsername = renderUsername(user); if (await execRegExp(queryRegex, fullUsername).catch(allowTimeout)) return true; return false; }); @@ -492,10 +492,10 @@ function formatSearchResultList(members: Array<GuildMember | User>): string { const paddedId = member.id.padEnd(longestId, " "); let line; if (member instanceof GuildMember) { - line = `${paddedId} ${renderUserUsername(member.user)}`; + line = `${paddedId} ${renderUsername(member.user)}`; if (member.nickname) line += ` (${member.nickname})`; } else { - line = `${paddedId} ${member.tag}`; + line = `${paddedId} ${renderUsername(member)}`; } return line; }); diff --git a/backend/src/utils.ts b/backend/src/utils.ts index ee6b556c..5c795072 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1603,9 +1603,12 @@ export function isTruthy<T>(value: T): value is Exclude<T, false | null | undefi export const DBDateFormat = "YYYY-MM-DD HH:mm:ss"; -export function renderUsername(username: User): string; -export function renderUsername(username: string, discriminator?: string): string; -export function renderUsername(username: string | User, discriminator?: string): string { +// TODO: Fix overloads +//export function renderUsername(username: GuildMember): string; +//export function renderUsername(username: User): string; +//export function renderUsername(username: string, discriminator?: string): string; +export function renderUsername(username: string | User | GuildMember, discriminator?: string): string { + if (username instanceof GuildMember) return username.user.tag; if (username instanceof User) return username.tag; if (discriminator === "0" || discriminator === "0000") { return username; diff --git a/backend/src/utils/templateSafeObjects.ts b/backend/src/utils/templateSafeObjects.ts index fbd27754..33e36971 100644 --- a/backend/src/utils/templateSafeObjects.ts +++ b/backend/src/utils/templateSafeObjects.ts @@ -52,7 +52,7 @@ export class TemplateSafeUser extends TemplateSafeValueContainer { globalName?: string; mention: string; tag: string; - avatarURL?: string; + avatarURL: string; bot?: boolean; createdAt?: number; renderedUsername: string; @@ -92,7 +92,7 @@ export class TemplateSafeMember extends TemplateSafeUser { nick: string; roles: TemplateSafeRole[]; joinedAt?: number; - // guildAvatarURL: string, Once DJS supports per-server avatars + guildAvatarURL: string; guildName: string; constructor(data: InputProps<TemplateSafeMember>) { @@ -261,7 +261,7 @@ export function userToTemplateSafeUser(user: User | UnknownUser): TemplateSafeUs globalName: user.globalName, mention: `<@${user.id}>`, tag: user.tag, - avatarURL: user.displayAvatarURL?.(), + avatarURL: user.displayAvatarURL(), bot: user.bot, createdAt: user.createdTimestamp, renderedUsername: renderUserUsername(user), @@ -287,6 +287,7 @@ export function memberToTemplateSafeMember(member: GuildMember | PartialGuildMem nick: member.nickname ?? "*None*", roles: [...member.roles.cache.mapValues((r) => roleToTemplateSafeRole(r)).values()], joinedAt: member.joinedTimestamp ?? undefined, + guildAvatarURL: member.displayAvatarURL(), guildName: member.guild.name, }); } From 4d0161a49f77e28ef89a819d7124a8239fd1a8c9 Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 15:50:01 +0000 Subject: [PATCH 14/51] oh god almeida looked at my code Signed-off-by: GitHub <noreply@github.com> --- backend/src/plugins/Automod/triggers/threadArchive.ts | 2 +- backend/src/plugins/Automod/triggers/threadCreate.ts | 2 +- backend/src/plugins/Automod/triggers/threadDelete.ts | 2 +- backend/src/plugins/Automod/triggers/threadUnarchive.ts | 2 +- backend/src/plugins/BotControl/commands/ServersCmd.ts | 7 ++++--- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/src/plugins/Automod/triggers/threadArchive.ts b/backend/src/plugins/Automod/triggers/threadArchive.ts index 8a692f6d..274732f5 100644 --- a/backend/src/plugins/Automod/triggers/threadArchive.ts +++ b/backend/src/plugins/Automod/triggers/threadArchive.ts @@ -48,7 +48,7 @@ export const ThreadArchiveTrigger = automodTrigger<ThreadArchiveResult>()({ const parentName = matchResult.extra.matchedThreadParentName; const base = `Thread **#${threadName}** (\`${threadId}\`) has been archived in the **#${parentName}** (\`${parentId}\`) channel`; if (threadOwner) { - return `${base} by **${escapeBold(renderUsername(threadOwner.tag))}** (\`${threadOwner.id}\`)`; + return `${base} by **${escapeBold(renderUsername(threadOwner))}** (\`${threadOwner.id}\`)`; } return base; }, diff --git a/backend/src/plugins/Automod/triggers/threadCreate.ts b/backend/src/plugins/Automod/triggers/threadCreate.ts index dc613068..793708e4 100644 --- a/backend/src/plugins/Automod/triggers/threadCreate.ts +++ b/backend/src/plugins/Automod/triggers/threadCreate.ts @@ -41,7 +41,7 @@ export const ThreadCreateTrigger = automodTrigger<ThreadCreateResult>()({ const parentName = matchResult.extra.matchedThreadParentName; const base = `Thread **#${threadName}** (\`${threadId}\`) has been created in the **#${parentName}** (\`${parentId}\`) channel`; if (threadOwner) { - return `${base} by **${escapeBold(renderUsername(threadOwner.tag))}** (\`${threadOwner.id}\`)`; + return `${base} by **${escapeBold(renderUsername(threadOwner))}** (\`${threadOwner.id}\`)`; } return base; }, diff --git a/backend/src/plugins/Automod/triggers/threadDelete.ts b/backend/src/plugins/Automod/triggers/threadDelete.ts index f27a1d0b..3c108bb0 100644 --- a/backend/src/plugins/Automod/triggers/threadDelete.ts +++ b/backend/src/plugins/Automod/triggers/threadDelete.ts @@ -41,7 +41,7 @@ export const ThreadDeleteTrigger = automodTrigger<ThreadDeleteResult>()({ const parentName = matchResult.extra.matchedThreadParentName; if (threadOwner) { return `Thread **#${threadName ?? "Unknown"}** (\`${threadId}\`) created by **${escapeBold( - renderUsername(threadOwner.tag), + renderUsername(threadOwner), )}** (\`${threadOwner.id}\`) in the **#${parentName}** (\`${parentId}\`) channel has been deleted`; } return `Thread **#${ diff --git a/backend/src/plugins/Automod/triggers/threadUnarchive.ts b/backend/src/plugins/Automod/triggers/threadUnarchive.ts index 94c69ace..0a081c2b 100644 --- a/backend/src/plugins/Automod/triggers/threadUnarchive.ts +++ b/backend/src/plugins/Automod/triggers/threadUnarchive.ts @@ -48,7 +48,7 @@ export const ThreadUnarchiveTrigger = automodTrigger<ThreadUnarchiveResult>()({ const parentName = matchResult.extra.matchedThreadParentName; const base = `Thread **#${threadName}** (\`${threadId}\`) has been unarchived in the **#${parentName}** (\`${parentId}\`) channel`; if (threadOwner) { - return `${base} by **${escapeBold(renderUsername(threadOwner.tag))}** (\`${threadOwner.id}\`)`; + return `${base} by **${escapeBold(renderUsername(threadOwner))}** (\`${threadOwner.id}\`)`; } return base; }, diff --git a/backend/src/plugins/BotControl/commands/ServersCmd.ts b/backend/src/plugins/BotControl/commands/ServersCmd.ts index 5cb62433..3658a36c 100644 --- a/backend/src/plugins/BotControl/commands/ServersCmd.ts +++ b/backend/src/plugins/BotControl/commands/ServersCmd.ts @@ -48,9 +48,10 @@ export const ServersCmd = botControlCmd({ const lines = filteredGuilds.map((g) => { const paddedId = g.id.padEnd(longestId, " "); const owner = getUser(pluginData.client, g.ownerId); - return `\`${paddedId}\` **${g.name}** (${g.memberCount} members) (owner **${renderUsername(owner.tag)}** \`${ - owner.id - }\`)`; + return `\`${paddedId}\` **${g.name}** (${g.memberCount} members) (owner **${renderUsername( + owner.username, + owner.discriminator, + )}** \`${owner.id}\`)`; }); createChunkedMessage(msg.channel, lines.join("\n")); } else { From bfc90093dc83d0c891d7ea542852835f1337e2d0 Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 15:57:23 +0000 Subject: [PATCH 15/51] yeet renderUserUsername Signed-off-by: GitHub <noreply@github.com> --- backend/src/plugins/Automod/triggers/roleAdded.ts | 4 ++-- backend/src/plugins/Automod/triggers/roleRemoved.ts | 4 ++-- .../BotControl/commands/AddDashboardUserCmd.ts | 4 ++-- .../BotControl/commands/ListDashboardPermsCmd.ts | 6 +++--- .../BotControl/commands/ListDashboardUsersCmd.ts | 4 ++-- .../BotControl/commands/RemoveDashboardUserCmd.ts | 4 ++-- .../src/plugins/ModActions/commands/AddCaseCmd.ts | 4 ++-- backend/src/plugins/ModActions/commands/BanCmd.ts | 4 ++-- backend/src/plugins/ModActions/commands/NoteCmd.ts | 4 ++-- backend/src/plugins/ModActions/commands/WarnCmd.ts | 4 ++-- .../ModActions/events/PostAlertOnMemberJoinEvt.ts | 4 ++-- .../ModActions/functions/actualKickMemberCmd.ts | 4 ++-- .../ModActions/functions/actualMuteUserCmd.ts | 10 +++++----- .../ModActions/functions/actualUnmuteUserCmd.ts | 6 +++--- backend/src/plugins/Mutes/commands/MutesCmd.ts | 6 +++--- backend/src/plugins/NameHistory/commands/NamesCmd.ts | 4 ++-- .../ReactionRoles/util/addMemberPendingRoleChange.ts | 6 ++---- .../plugins/Slowmode/commands/SlowmodeClearCmd.ts | 8 ++++---- backend/src/plugins/UsernameSaver/updateUsername.ts | 4 ++-- backend/src/plugins/Utility/commands/LevelCmd.ts | 4 ++-- .../src/plugins/Utility/commands/VcdisconnectCmd.ts | 4 ++-- backend/src/plugins/Utility/commands/VcmoveCmd.ts | 12 ++++-------- backend/src/utils.ts | 8 ++------ backend/src/utils/templateSafeObjects.ts | 6 +++--- 24 files changed, 59 insertions(+), 69 deletions(-) diff --git a/backend/src/plugins/Automod/triggers/roleAdded.ts b/backend/src/plugins/Automod/triggers/roleAdded.ts index dc62f163..754be1b3 100644 --- a/backend/src/plugins/Automod/triggers/roleAdded.ts +++ b/backend/src/plugins/Automod/triggers/roleAdded.ts @@ -1,6 +1,6 @@ import { Snowflake } from "discord.js"; import * as t from "io-ts"; -import { renderUserUsername } from "../../../utils"; +import { renderUsername } from "../../../utils"; import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges"; import { automodTrigger } from "../helpers"; @@ -38,7 +38,7 @@ export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({ const role = pluginData.guild.roles.cache.get(matchResult.extra.matchedRoleId as Snowflake); const roleName = role?.name || "Unknown"; const member = contexts[0].member!; - const memberName = `**${renderUserUsername(member.user)}** (\`${member.id}\`)`; + const memberName = `**${renderUsername(member.user)}** (\`${member.id}\`)`; return `Role ${roleName} (\`${matchResult.extra.matchedRoleId}\`) was added to ${memberName}`; }, }); diff --git a/backend/src/plugins/Automod/triggers/roleRemoved.ts b/backend/src/plugins/Automod/triggers/roleRemoved.ts index 65624827..fc5d5ae3 100644 --- a/backend/src/plugins/Automod/triggers/roleRemoved.ts +++ b/backend/src/plugins/Automod/triggers/roleRemoved.ts @@ -1,6 +1,6 @@ import { Snowflake } from "discord.js"; import * as t from "io-ts"; -import { renderUserUsername } from "../../../utils"; +import { renderUsername } from "../../../utils"; import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges"; import { automodTrigger } from "../helpers"; @@ -38,7 +38,7 @@ export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({ const role = pluginData.guild.roles.cache.get(matchResult.extra.matchedRoleId as Snowflake); const roleName = role?.name || "Unknown"; const member = contexts[0].member!; - const memberName = `**${renderUserUsername(member.user)}** (\`${member.id}\`)`; + const memberName = `**${renderUsername(member.user)}** (\`${member.id}\`)`; return `Role ${roleName} (\`${matchResult.extra.matchedRoleId}\`) was removed from ${memberName}`; }, }); diff --git a/backend/src/plugins/BotControl/commands/AddDashboardUserCmd.ts b/backend/src/plugins/BotControl/commands/AddDashboardUserCmd.ts index c1259594..0cb87f95 100644 --- a/backend/src/plugins/BotControl/commands/AddDashboardUserCmd.ts +++ b/backend/src/plugins/BotControl/commands/AddDashboardUserCmd.ts @@ -1,7 +1,7 @@ import { ApiPermissions } from "@shared/apiPermissions"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { isStaffPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { renderUserUsername } from "../../../utils"; +import { renderUsername } from "../../../utils"; import { botControlCmd } from "../types"; export const AddDashboardUserCmd = botControlCmd({ @@ -35,7 +35,7 @@ export const AddDashboardUserCmd = botControlCmd({ await pluginData.state.apiPermissionAssignments.addUser(args.guildId, user.id, [ApiPermissions.EditConfig]); } - const userNameList = args.users.map((user) => `<@!${user.id}> (**${renderUserUsername(user)}**, \`${user.id}\`)`); + const userNameList = args.users.map((user) => `<@!${user.id}> (**${renderUsername(user)}**, \`${user.id}\`)`); sendSuccessMessage( pluginData, msg.channel, diff --git a/backend/src/plugins/BotControl/commands/ListDashboardPermsCmd.ts b/backend/src/plugins/BotControl/commands/ListDashboardPermsCmd.ts index 18c7d0d7..7c6d2aa5 100644 --- a/backend/src/plugins/BotControl/commands/ListDashboardPermsCmd.ts +++ b/backend/src/plugins/BotControl/commands/ListDashboardPermsCmd.ts @@ -2,7 +2,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { AllowedGuild } from "../../../data/entities/AllowedGuild"; import { ApiPermissionAssignment } from "../../../data/entities/ApiPermissionAssignment"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { renderUserUsername, resolveUser } from "../../../utils"; +import { renderUsername, resolveUser } from "../../../utils"; import { botControlCmd } from "../types"; export const ListDashboardPermsCmd = botControlCmd({ @@ -42,7 +42,7 @@ export const ListDashboardPermsCmd = botControlCmd({ // If we have user, always display which guilds they have permissions in (or only specified guild permissions) if (args.user) { - const userInfo = `**${renderUserUsername(args.user)}** (\`${args.user.id}\`)`; + const userInfo = `**${renderUsername(args.user)}** (\`${args.user.id}\`)`; for (const assignment of existingUserAssignment!) { if (guild != null && assignment.guild_id !== args.guildId) continue; @@ -74,7 +74,7 @@ export const ListDashboardPermsCmd = botControlCmd({ finalMessage += `The server ${guildInfo} has the following assigned permissions:\n`; // Double \n for consistency with AddDashboardUserCmd for (const assignment of existingGuildAssignment) { const user = await resolveUser(pluginData.client, assignment.target_id); - finalMessage += `\n**${renderUserUsername(user)}**, \`${assignment.target_id}\`: ${assignment.permissions.join( + finalMessage += `\n**${renderUsername(user)}**, \`${assignment.target_id}\`: ${assignment.permissions.join( ", ", )}`; } diff --git a/backend/src/plugins/BotControl/commands/ListDashboardUsersCmd.ts b/backend/src/plugins/BotControl/commands/ListDashboardUsersCmd.ts index 36f1432f..1d3c1ffe 100644 --- a/backend/src/plugins/BotControl/commands/ListDashboardUsersCmd.ts +++ b/backend/src/plugins/BotControl/commands/ListDashboardUsersCmd.ts @@ -1,6 +1,6 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { renderUserUsername, resolveUser } from "../../../utils"; +import { renderUsername, resolveUser } from "../../../utils"; import { botControlCmd } from "../types"; export const ListDashboardUsersCmd = botControlCmd({ @@ -27,7 +27,7 @@ export const ListDashboardUsersCmd = botControlCmd({ ); const userNameList = users.map( ({ user, permission }) => - `<@!${user.id}> (**${renderUserUsername(user)}**, \`${user.id}\`): ${permission.permissions.join(", ")}`, + `<@!${user.id}> (**${renderUsername(user)}**, \`${user.id}\`): ${permission.permissions.join(", ")}`, ); sendSuccessMessage( diff --git a/backend/src/plugins/BotControl/commands/RemoveDashboardUserCmd.ts b/backend/src/plugins/BotControl/commands/RemoveDashboardUserCmd.ts index 3a90683c..c3d1ec99 100644 --- a/backend/src/plugins/BotControl/commands/RemoveDashboardUserCmd.ts +++ b/backend/src/plugins/BotControl/commands/RemoveDashboardUserCmd.ts @@ -1,6 +1,6 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { isStaffPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { renderUserUsername } from "../../../utils"; +import { renderUsername } from "../../../utils"; import { botControlCmd } from "../types"; export const RemoveDashboardUserCmd = botControlCmd({ @@ -34,7 +34,7 @@ export const RemoveDashboardUserCmd = botControlCmd({ await pluginData.state.apiPermissionAssignments.removeUser(args.guildId, user.id); } - const userNameList = args.users.map((user) => `<@!${user.id}> (**${renderUserUsername(user)}**, \`${user.id}\`)`); + const userNameList = args.users.map((user) => `<@!${user.id}> (**${renderUsername(user)}**, \`${user.id}\`)`); sendSuccessMessage( pluginData, msg.channel, diff --git a/backend/src/plugins/ModActions/commands/AddCaseCmd.ts b/backend/src/plugins/ModActions/commands/AddCaseCmd.ts index 43575463..3f8b9dfc 100644 --- a/backend/src/plugins/ModActions/commands/AddCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/AddCaseCmd.ts @@ -3,7 +3,7 @@ import { CaseTypes } from "../../../data/CaseTypes"; import { Case } from "../../../data/entities/Case"; import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { renderUserUsername, resolveMember, resolveUser } from "../../../utils"; +import { renderUsername, resolveMember, resolveUser } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; import { modActionsCmd } from "../types"; @@ -75,7 +75,7 @@ export const AddCaseCmd = modActionsCmd({ sendSuccessMessage( pluginData, msg.channel, - `Case #${theCase.case_number} created for **${renderUserUsername(user)}**`, + `Case #${theCase.case_number} created for **${renderUsername(user)}**`, ); } else { sendSuccessMessage(pluginData, msg.channel, `Case #${theCase.case_number} created`); diff --git a/backend/src/plugins/ModActions/commands/BanCmd.ts b/backend/src/plugins/ModActions/commands/BanCmd.ts index 9d32cd10..3e66e8a2 100644 --- a/backend/src/plugins/ModActions/commands/BanCmd.ts +++ b/backend/src/plugins/ModActions/commands/BanCmd.ts @@ -5,7 +5,7 @@ import { CaseTypes } from "../../../data/CaseTypes"; import { clearExpiringTempban, registerExpiringTempban } from "../../../data/loops/expiringTempbansLoop"; import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; -import { renderUserUsername, resolveMember, resolveUser } from "../../../utils"; +import { renderUsername, resolveMember, resolveUser } from "../../../utils"; import { banLock } from "../../../utils/lockNameHelpers"; import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; import { LogsPlugin } from "../../Logs/LogsPlugin"; @@ -207,7 +207,7 @@ export const BanCmd = modActionsCmd({ // Confirm the action to the moderator let response = ""; if (!forceban) { - response = `Banned **${renderUserUsername(user)}** ${forTime}(Case #${banResult.case.case_number})`; + response = `Banned **${renderUsername(user)}** ${forTime}(Case #${banResult.case.case_number})`; if (banResult.notifyResult.text) response += ` (${banResult.notifyResult.text})`; } else { response = `Member forcebanned ${forTime}(Case #${banResult.case.case_number})`; diff --git a/backend/src/plugins/ModActions/commands/NoteCmd.ts b/backend/src/plugins/ModActions/commands/NoteCmd.ts index b13ed498..edb2202d 100644 --- a/backend/src/plugins/ModActions/commands/NoteCmd.ts +++ b/backend/src/plugins/ModActions/commands/NoteCmd.ts @@ -1,7 +1,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { CaseTypes } from "../../../data/CaseTypes"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { renderUserUsername, resolveUser } from "../../../utils"; +import { renderUsername, resolveUser } from "../../../utils"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; @@ -29,7 +29,7 @@ export const NoteCmd = modActionsCmd({ return; } - const userName = renderUserUsername(user); + const userName = renderUsername(user); const reason = formatReasonWithAttachments(args.note, [...msg.attachments.values()]); const casesPlugin = pluginData.getPlugin(CasesPlugin); diff --git a/backend/src/plugins/ModActions/commands/WarnCmd.ts b/backend/src/plugins/ModActions/commands/WarnCmd.ts index c8192015..f917c55e 100644 --- a/backend/src/plugins/ModActions/commands/WarnCmd.ts +++ b/backend/src/plugins/ModActions/commands/WarnCmd.ts @@ -1,7 +1,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { CaseTypes } from "../../../data/CaseTypes"; import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { errorMessage, renderUserUsername, resolveMember, resolveUser } from "../../../utils"; +import { errorMessage, renderUsername, resolveMember, resolveUser } from "../../../utils"; import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments"; @@ -106,7 +106,7 @@ export const WarnCmd = modActionsCmd({ sendSuccessMessage( pluginData, msg.channel, - `Warned **${renderUserUsername(memberToWarn.user)}** (Case #${warnResult.case.case_number})${messageResultText}`, + `Warned **${renderUsername(memberToWarn.user)}** (Case #${warnResult.case.case_number})${messageResultText}`, ); }, }); diff --git a/backend/src/plugins/ModActions/events/PostAlertOnMemberJoinEvt.ts b/backend/src/plugins/ModActions/events/PostAlertOnMemberJoinEvt.ts index 677fb603..7874c241 100644 --- a/backend/src/plugins/ModActions/events/PostAlertOnMemberJoinEvt.ts +++ b/backend/src/plugins/ModActions/events/PostAlertOnMemberJoinEvt.ts @@ -1,5 +1,5 @@ import { PermissionsBitField, Snowflake, TextChannel } from "discord.js"; -import { renderUserUsername, resolveMember } from "../../../utils"; +import { renderUsername, resolveMember } from "../../../utils"; import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { modActionsEvt } from "../types"; @@ -46,7 +46,7 @@ export const PostAlertOnMemberJoinEvt = modActionsEvt({ } await alertChannel.send( - `<@!${member.id}> (${renderUserUsername(member.user)} \`${member.id}\`) joined with ${ + `<@!${member.id}> (${renderUsername(member.user)} \`${member.id}\`) joined with ${ actions.length } prior record(s)`, ); diff --git a/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts b/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts index 73a1e2d9..fac3f906 100644 --- a/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts +++ b/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts @@ -3,7 +3,7 @@ import { GuildPluginData } from "knub"; import { hasPermission } from "knub/helpers"; import { LogType } from "../../../data/LogType"; import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { DAYS, SECONDS, errorMessage, renderUserUsername, resolveMember, resolveUser } from "../../../utils"; +import { DAYS, SECONDS, errorMessage, renderUsername, resolveMember, resolveUser } from "../../../utils"; import { IgnoredEventType, ModActionsPluginType } from "../types"; import { formatReasonWithAttachments } from "./formatReasonWithAttachments"; import { ignoreEvent } from "./ignoreEvent"; @@ -103,7 +103,7 @@ export async function actualKickMemberCmd( } // Confirm the action to the moderator - let response = `Kicked **${renderUserUsername(memberToKick.user)}** (Case #${kickResult.case.case_number})`; + let response = `Kicked **${renderUsername(memberToKick.user)}** (Case #${kickResult.case.case_number})`; if (kickResult.notifyResult.text) response += ` (${kickResult.notifyResult.text})`; sendSuccessMessage(pluginData, msg.channel, response); diff --git a/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts b/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts index 5c628c4e..2f108410 100644 --- a/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts +++ b/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts @@ -4,7 +4,7 @@ import { GuildPluginData } from "knub"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; import { logger } from "../../../logger"; import { hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { UnknownUser, asSingleLine, isDiscordAPIError, renderUserUsername } from "../../../utils"; +import { UnknownUser, asSingleLine, isDiscordAPIError, renderUsername } from "../../../utils"; import { MutesPlugin } from "../../Mutes/MutesPlugin"; import { MuteResult } from "../../Mutes/types"; import { ModActionsPluginType } from "../types"; @@ -86,24 +86,24 @@ export async function actualMuteUserCmd( if (args.time) { if (muteResult.updatedExistingMute) { response = asSingleLine(` - Updated **${renderUserUsername(user)}**'s + Updated **${renderUsername(user)}**'s mute to ${timeUntilUnmute} (Case #${muteResult.case.case_number}) `); } else { response = asSingleLine(` - Muted **${renderUserUsername(user)}** + Muted **${renderUsername(user)}** for ${timeUntilUnmute} (Case #${muteResult.case.case_number}) `); } } else { if (muteResult.updatedExistingMute) { response = asSingleLine(` - Updated **${renderUserUsername(user)}**'s + Updated **${renderUsername(user)}**'s mute to indefinite (Case #${muteResult.case.case_number}) `); } else { response = asSingleLine(` - Muted **${renderUserUsername(user)}** + Muted **${renderUsername(user)}** indefinitely (Case #${muteResult.case.case_number}) `); } diff --git a/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts b/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts index d70a219c..5b28aee7 100644 --- a/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts +++ b/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts @@ -3,7 +3,7 @@ import humanizeDuration from "humanize-duration"; import { GuildPluginData } from "knub"; import { hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; import { MutesPlugin } from "../../../plugins/Mutes/MutesPlugin"; -import { UnknownUser, asSingleLine, renderUserUsername } from "../../../utils"; +import { UnknownUser, asSingleLine, renderUsername } from "../../../utils"; import { ModActionsPluginType } from "../types"; import { formatReasonWithAttachments } from "./formatReasonWithAttachments"; @@ -48,7 +48,7 @@ export async function actualUnmuteCmd( pluginData, msg.channel, asSingleLine(` - Unmuting **${renderUserUsername(user)}** + Unmuting **${renderUsername(user)}** in ${timeUntilUnmute} (Case #${result.case.case_number}) `), ); @@ -57,7 +57,7 @@ export async function actualUnmuteCmd( pluginData, msg.channel, asSingleLine(` - Unmuted **${renderUserUsername(user)}** + Unmuted **${renderUsername(user)}** (Case #${result.case.case_number}) `), ); diff --git a/backend/src/plugins/Mutes/commands/MutesCmd.ts b/backend/src/plugins/Mutes/commands/MutesCmd.ts index 179d1a65..22cd74c5 100644 --- a/backend/src/plugins/Mutes/commands/MutesCmd.ts +++ b/backend/src/plugins/Mutes/commands/MutesCmd.ts @@ -10,7 +10,7 @@ import moment from "moment-timezone"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { humanizeDurationShort } from "../../../humanizeDurationShort"; import { getBaseUrl } from "../../../pluginUtils"; -import { DBDateFormat, MINUTES, renderUserUsername, resolveMember } from "../../../utils"; +import { DBDateFormat, MINUTES, renderUsername, resolveMember } from "../../../utils"; import { IMuteWithDetails, mutesCmd } from "../types"; export const MutesCmd = mutesCmd({ @@ -74,7 +74,7 @@ export const MutesCmd = mutesCmd({ totalMutes = manuallyMutedMembers.length; lines = manuallyMutedMembers.map((member) => { - return `<@!${member.id}> (**${renderUserUsername(member.user)}**, \`${member.id}\`) 🔧 Manual mute`; + return `<@!${member.id}> (**${renderUsername(member.user)}**, \`${member.id}\`) 🔧 Manual mute`; }); } else { // Show filtered active mutes (but not manual mutes) @@ -123,7 +123,7 @@ export const MutesCmd = mutesCmd({ lines = filteredMutes.map((mute) => { const user = pluginData.client.users.resolve(mute.user_id as Snowflake); - const username = user ? renderUserUsername(user) : "Unknown#0000"; + const username = user ? renderUsername(user) : "Unknown#0000"; const theCase = muteCasesById.get(mute.case_id); const caseName = theCase ? `Case #${theCase.case_number}` : "No case"; diff --git a/backend/src/plugins/NameHistory/commands/NamesCmd.ts b/backend/src/plugins/NameHistory/commands/NamesCmd.ts index 373f1671..da8ba48a 100644 --- a/backend/src/plugins/NameHistory/commands/NamesCmd.ts +++ b/backend/src/plugins/NameHistory/commands/NamesCmd.ts @@ -5,7 +5,7 @@ import { MAX_NICKNAME_ENTRIES_PER_USER } from "../../../data/GuildNicknameHistor import { MAX_USERNAME_ENTRIES_PER_USER } from "../../../data/UsernameHistory"; import { NICKNAME_RETENTION_PERIOD } from "../../../data/cleanup/nicknames"; import { sendErrorMessage } from "../../../pluginUtils"; -import { DAYS, renderUserUsername } from "../../../utils"; +import { DAYS, renderUsername } from "../../../utils"; import { nameHistoryCmd } from "../types"; export const NamesCmd = nameHistoryCmd({ @@ -31,7 +31,7 @@ export const NamesCmd = nameHistoryCmd({ const usernameRows = usernames.map((r) => `\`[${r.timestamp}]\` **${disableCodeBlocks(r.username)}**`); const user = await pluginData.client.users.fetch(args.userId as Snowflake).catch(() => null); - const currentUsername = user ? renderUserUsername(user) : args.userId; + const currentUsername = user ? renderUsername(user) : args.userId; const nicknameDays = Math.round(NICKNAME_RETENTION_PERIOD / DAYS); const usernameDays = Math.round(NICKNAME_RETENTION_PERIOD / DAYS); diff --git a/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts b/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts index a2a82fe9..dda8a6bc 100644 --- a/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts +++ b/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts @@ -1,7 +1,7 @@ import { Snowflake } from "discord.js"; import { GuildPluginData } from "knub"; import { logger } from "../../../logger"; -import { renderUserUsername, resolveMember } from "../../../utils"; +import { renderUsername, resolveMember } from "../../../utils"; import { memberRolesLock } from "../../../utils/lockNameHelpers"; import { PendingMemberRoleChanges, ReactionRolesPluginType, RoleChangeMode } from "../types"; @@ -33,9 +33,7 @@ export async function addMemberPendingRoleChange( try { await member.roles.set(Array.from(newRoleIds.values()), "Reaction roles"); } catch (e) { - logger.warn( - `Failed to apply role changes to ${renderUserUsername(member.user)} (${member.id}): ${e.message}`, - ); + logger.warn(`Failed to apply role changes to ${renderUsername(member.user)} (${member.id}): ${e.message}`); } } lock.unlock(); diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts index 246a048e..fd133c3a 100644 --- a/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts +++ b/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts @@ -1,7 +1,7 @@ import { ChannelType, escapeInlineCode } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { asSingleLine, renderUserUsername } from "../../../utils"; +import { asSingleLine, renderUsername } from "../../../utils"; import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions"; import { missingPermissionError } from "../../../utils/missingPermissionError"; import { BOT_SLOWMODE_CLEAR_PERMISSIONS } from "../requiredPermissions"; @@ -45,7 +45,7 @@ export const SlowmodeClearCmd = slowmodeCmd({ pluginData, msg.channel, asSingleLine(` - Failed to clear slowmode from **${renderUserUsername(args.user)}** in <#${args.channel.id}>: + Failed to clear slowmode from **${renderUsername(args.user)}** in <#${args.channel.id}>: Threads cannot have Bot Slowmode `), ); @@ -56,7 +56,7 @@ export const SlowmodeClearCmd = slowmodeCmd({ pluginData, msg.channel, asSingleLine(` - Failed to clear slowmode from **${renderUserUsername(args.user)}** in <#${args.channel.id}>: + Failed to clear slowmode from **${renderUsername(args.user)}** in <#${args.channel.id}>: \`${escapeInlineCode(e.message)}\` `), ); @@ -66,7 +66,7 @@ export const SlowmodeClearCmd = slowmodeCmd({ sendSuccessMessage( pluginData, msg.channel, - `Slowmode cleared from **${renderUserUsername(args.user)}** in <#${args.channel.id}>`, + `Slowmode cleared from **${renderUsername(args.user)}** in <#${args.channel.id}>`, ); }, }); diff --git a/backend/src/plugins/UsernameSaver/updateUsername.ts b/backend/src/plugins/UsernameSaver/updateUsername.ts index 163a4aae..ed0fb73f 100644 --- a/backend/src/plugins/UsernameSaver/updateUsername.ts +++ b/backend/src/plugins/UsernameSaver/updateUsername.ts @@ -1,11 +1,11 @@ import { User } from "discord.js"; import { GuildPluginData } from "knub"; -import { renderUserUsername } from "../../utils"; +import { renderUsername } from "../../utils"; import { UsernameSaverPluginType } from "./types"; export async function updateUsername(pluginData: GuildPluginData<UsernameSaverPluginType>, user: User) { if (!user) return; - const newUsername = renderUserUsername(user); + const newUsername = renderUsername(user); const latestEntry = await pluginData.state.usernameHistory.getLastEntry(user.id); if (!latestEntry || newUsername !== latestEntry.username) { await pluginData.state.usernameHistory.addEntry(user.id, newUsername); diff --git a/backend/src/plugins/Utility/commands/LevelCmd.ts b/backend/src/plugins/Utility/commands/LevelCmd.ts index 306a1d20..df4d9325 100644 --- a/backend/src/plugins/Utility/commands/LevelCmd.ts +++ b/backend/src/plugins/Utility/commands/LevelCmd.ts @@ -1,6 +1,6 @@ import { helpers } from "knub"; import { commandTypeHelpers as ct } from "../../../commandTypes"; -import { renderUserUsername } from "../../../utils"; +import { renderUsername } from "../../../utils"; import { utilityCmd } from "../types"; const { getMemberLevel } = helpers; @@ -18,6 +18,6 @@ export const LevelCmd = utilityCmd({ run({ message, args, pluginData }) { const member = args.member || message.member; const level = getMemberLevel(pluginData, member); - message.channel.send(`The permission level of ${renderUserUsername(member.user)} is **${level}**`); + message.channel.send(`The permission level of ${renderUsername(member.user)} is **${level}**`); }, }); diff --git a/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts b/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts index 0f82458a..240c763f 100644 --- a/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts +++ b/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts @@ -1,7 +1,7 @@ import { VoiceChannel } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { renderUserUsername } from "../../../utils"; +import { renderUsername } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { utilityCmd } from "../types"; @@ -43,7 +43,7 @@ export const VcdisconnectCmd = utilityCmd({ sendSuccessMessage( pluginData, msg.channel, - `**${renderUserUsername(args.member.user)}** disconnected from **${channel.name}**`, + `**${renderUsername(args.member.user)}** disconnected from **${channel.name}**`, ); }, }); diff --git a/backend/src/plugins/Utility/commands/VcmoveCmd.ts b/backend/src/plugins/Utility/commands/VcmoveCmd.ts index db00161e..6ffe8588 100644 --- a/backend/src/plugins/Utility/commands/VcmoveCmd.ts +++ b/backend/src/plugins/Utility/commands/VcmoveCmd.ts @@ -1,7 +1,7 @@ import { ChannelType, Snowflake, VoiceChannel } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { channelMentionRegex, isSnowflake, renderUserUsername, simpleClosestStringMatch } from "../../../utils"; +import { channelMentionRegex, isSnowflake, renderUsername, simpleClosestStringMatch } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { utilityCmd } from "../types"; @@ -80,11 +80,7 @@ export const VcmoveCmd = utilityCmd({ newChannel: channel, }); - sendSuccessMessage( - pluginData, - msg.channel, - `**${renderUserUsername(args.member.user)}** moved to **${channel.name}**`, - ); + sendSuccessMessage(pluginData, msg.channel, `**${renderUsername(args.member.user)}** moved to **${channel.name}**`); }, }); @@ -157,7 +153,7 @@ export const VcmoveAllCmd = utilityCmd({ sendErrorMessage( pluginData, msg.channel, - `Failed to move ${renderUserUsername(currMember.user)} (${currMember.id}): You cannot act on this member`, + `Failed to move ${renderUsername(currMember.user)} (${currMember.id}): You cannot act on this member`, ); errAmt++; continue; @@ -175,7 +171,7 @@ export const VcmoveAllCmd = utilityCmd({ sendErrorMessage( pluginData, msg.channel, - `Failed to move ${renderUserUsername(currMember.user)} (${currMember.id})`, + `Failed to move ${renderUsername(currMember.user)} (${currMember.id})`, ); errAmt++; continue; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 5c795072..990c1f7e 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1607,15 +1607,11 @@ export const DBDateFormat = "YYYY-MM-DD HH:mm:ss"; //export function renderUsername(username: GuildMember): string; //export function renderUsername(username: User): string; //export function renderUsername(username: string, discriminator?: string): string; -export function renderUsername(username: string | User | GuildMember, discriminator?: string): string { +export function renderUsername(username: string | User | GuildMember | UnknownUser, discriminator?: string): string { if (username instanceof GuildMember) return username.user.tag; - if (username instanceof User) return username.tag; + if (username instanceof User || username instanceof UnknownUser) return username.tag; if (discriminator === "0" || discriminator === "0000") { return username; } return `${username}#${discriminator}`; } - -export function renderUserUsername(user: User | UnknownUser): string { - return renderUsername(user.username, user.discriminator); -} diff --git a/backend/src/utils/templateSafeObjects.ts b/backend/src/utils/templateSafeObjects.ts index 33e36971..d6a0dfd6 100644 --- a/backend/src/utils/templateSafeObjects.ts +++ b/backend/src/utils/templateSafeObjects.ts @@ -13,7 +13,7 @@ import { User, } from "discord.js"; import { GuildPluginData } from "knub"; -import { UnknownUser, renderUserUsername } from "src/utils"; +import { UnknownUser, renderUsername } from "src/utils"; import { Case } from "../data/entities/Case"; import { ISavedMessageAttachmentData, @@ -250,7 +250,7 @@ export function userToTemplateSafeUser(user: User | UnknownUser): TemplateSafeUs discriminator: "0000", mention: `<@${user.id}>`, tag: "Unknown#0000", - renderedUsername: renderUserUsername(user), + renderedUsername: renderUsername(user), }); } @@ -264,7 +264,7 @@ export function userToTemplateSafeUser(user: User | UnknownUser): TemplateSafeUs avatarURL: user.displayAvatarURL(), bot: user.bot, createdAt: user.createdTimestamp, - renderedUsername: renderUserUsername(user), + renderedUsername: renderUsername(user), }); } From 2e0598194f36ed9d2d12ca063cb37837b1f61556 Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 16:02:15 +0000 Subject: [PATCH 16/51] fix renderUsername overloads Signed-off-by: GitHub <noreply@github.com> --- backend/src/utils.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 990c1f7e..72f203f2 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1603,14 +1603,12 @@ export function isTruthy<T>(value: T): value is Exclude<T, false | null | undefi export const DBDateFormat = "YYYY-MM-DD HH:mm:ss"; -// TODO: Fix overloads -//export function renderUsername(username: GuildMember): string; -//export function renderUsername(username: User): string; -//export function renderUsername(username: string, discriminator?: string): string; +export function renderUsername(memberOrUser: GuildMember | UnknownUser | User): string; +export function renderUsername(username: string, discriminator: string): string; export function renderUsername(username: string | User | GuildMember | UnknownUser, discriminator?: string): string { if (username instanceof GuildMember) return username.user.tag; if (username instanceof User || username instanceof UnknownUser) return username.tag; - if (discriminator === "0" || discriminator === "0000") { + if (discriminator === "0") { return username; } return `${username}#${discriminator}`; From aa1f11e8015b192af39d77bb73f7a5e914d1696f Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 16:10:10 +0000 Subject: [PATCH 17/51] lint Signed-off-by: GitHub <noreply@github.com> --- backend/src/index.ts | 2 +- backend/src/plugins/Automod/actions/startThread.ts | 1 - backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts | 2 +- backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts | 5 +---- backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts | 2 +- backend/src/plugins/Utility/functions/getServerInfoEmbed.ts | 2 +- .../src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts | 2 +- backend/src/plugins/Utility/functions/getUserInfoEmbed.ts | 2 +- 8 files changed, 7 insertions(+), 11 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 2fe16c11..6dad14b1 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -203,7 +203,7 @@ if (env.DEBUG) { } logger.info("Connecting to database"); -connect().then(async (connection) => { +connect().then(async () => { const client = new Client({ partials: [Partials.User, Partials.Channel, Partials.GuildMember, Partials.Message, Partials.Reaction], diff --git a/backend/src/plugins/Automod/actions/startThread.ts b/backend/src/plugins/Automod/actions/startThread.ts index 970e9ec8..b5864389 100644 --- a/backend/src/plugins/Automod/actions/startThread.ts +++ b/backend/src/plugins/Automod/actions/startThread.ts @@ -41,7 +41,6 @@ export const StartThreadAction = automodAction({ return true; }); - const guild = pluginData.guild; const archiveSet = actionConfig.auto_archive ? Math.ceil(Math.max(convertDelayStringToMS(actionConfig.auto_archive) ?? 0, 0) / MINUTES) : ThreadAutoArchiveDuration.OneDay; diff --git a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts index f601dd5c..c05bd2c1 100644 --- a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts @@ -24,7 +24,7 @@ const MEDIA_CHANNEL_ICON = "https://cdn.discordapp.com/attachments/8761342052292 export async function getChannelInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, channelId: string, - requestMemberId?: string, + // requestMemberId?: string, ): Promise<APIEmbed | null> { const channel = pluginData.guild.channels.cache.get(channelId as Snowflake); if (!channel) { diff --git a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts index 3a49a80d..e272b904 100644 --- a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts @@ -9,7 +9,6 @@ import { trimEmptyLines, trimLines, } from "../../../utils"; -import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; import { UtilityPluginType } from "../types"; const MESSAGE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/740685652152025088/message.png"; @@ -18,7 +17,7 @@ export async function getMessageInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, channelId: string, messageId: string, - requestMemberId?: string, + // requestMemberId?: string, ): Promise<APIEmbed | null> { const message = await (pluginData.guild.channels.resolve(channelId as Snowflake) as TextChannel).messages .fetch(messageId as Snowflake) @@ -27,8 +26,6 @@ export async function getMessageInfoEmbed( return null; } - const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); - const embed: EmbedWith<"fields" | "author"> = { fields: [], author: { diff --git a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts index fa3188ec..c4d685d2 100644 --- a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts @@ -9,7 +9,7 @@ const MENTION_ICON = "https://cdn.discordapp.com/attachments/705009450855039042/ export async function getRoleInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, role: Role, - requestMemberId?: string, + // requestMemberId?: string, ): Promise<APIEmbed> { const embed: EmbedWith<"fields" | "author" | "color"> = { fields: [], diff --git a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts index afcefb80..8a2ebfc7 100644 --- a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts @@ -25,7 +25,7 @@ const prettifyFeature = (feature: string): string => export async function getServerInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, serverId: string, - requestMemberId?: string, + // requestMemberId?: string, ): Promise<APIEmbed | null> { const thisServer = serverId === pluginData.guild.id ? pluginData.guild : null; const [restGuild, guildPreview] = await Promise.all([ diff --git a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts index ef676fd1..ba7c3374 100644 --- a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts @@ -10,7 +10,7 @@ export async function getSnowflakeInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, snowflake: string, showUnknownWarning = false, - requestMemberId?: string, + // requestMemberId?: string, ): Promise<APIEmbed> { const embed: EmbedWith<"fields" | "author"> = { fields: [], diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts index 58d34d56..d8ccc833 100644 --- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts @@ -26,7 +26,7 @@ export async function getUserInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, userId: string, compact = false, - requestMemberId?: string, + // requestMemberId?: string, ): Promise<APIEmbed | null> { const user = await resolveUser(pluginData.client, userId); if (!user || user instanceof UnknownUser) { From 89d6e8aeecabea2ecc6c788f7ded6c447a870028 Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 16:11:12 +0000 Subject: [PATCH 18/51] missed one Signed-off-by: GitHub <noreply@github.com> --- backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts index 823bc726..c107791d 100644 --- a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts @@ -2,7 +2,7 @@ import { helpers } from "knub"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { Case } from "../../../data/entities/Case"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; -import { SECONDS, trimLines } from "../../../utils"; +import { SECONDS, renderUsername, trimLines } from "../../../utils"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; @@ -60,7 +60,7 @@ export const DeleteCaseCmd = modActionsCmd({ } } - const deletedByName = message.author.tag; + const deletedByName = renderUsername(message.author); const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); const deletedAt = timeAndDate.inGuildTz().format(timeAndDate.getDateFormat("pretty_datetime")); From e0637a206f0e9c7bae14ec61c3508b92a83136c1 Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 16:12:57 +0000 Subject: [PATCH 19/51] better startThread if checks (almeida is happy) Signed-off-by: GitHub <noreply@github.com> --- backend/src/plugins/Automod/actions/startThread.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/backend/src/plugins/Automod/actions/startThread.ts b/backend/src/plugins/Automod/actions/startThread.ts index b5864389..4930ebe5 100644 --- a/backend/src/plugins/Automod/actions/startThread.ts +++ b/backend/src/plugins/Automod/actions/startThread.ts @@ -50,13 +50,7 @@ export const StartThreadAction = automodAction({ for (const threadContext of threads) { const channel = pluginData.guild.channels.cache.get(threadContext.message!.channel_id); - if ( - !channel || - !("threads" in channel) || - channel.type === ChannelType.GuildForum || - channel.type === ChannelType.GuildMedia - ) - continue; + if (!channel || !("threads" in channel) || channel.isThreadOnly()) continue; const renderThreadName = async (str: string) => renderTemplate( From 2b5a5e636a230212383ff84af58bb20b97e9777f Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 17:02:54 +0000 Subject: [PATCH 20/51] almeida review.mp4 Signed-off-by: GitHub <noreply@github.com> --- .../functions/matchMultipleTextTypesOnMessage.ts | 2 +- backend/src/plugins/Automod/triggers/roleAdded.ts | 2 +- backend/src/plugins/Automod/triggers/roleRemoved.ts | 2 +- backend/src/plugins/BotControl/commands/ServersCmd.ts | 7 +++---- backend/src/plugins/Cases/functions/createCase.ts | 8 ++++---- backend/src/plugins/ModActions/commands/CasesModCmd.ts | 8 ++++---- .../src/plugins/ModActions/commands/CasesUserCmd.ts | 2 +- backend/src/plugins/ModActions/commands/WarnCmd.ts | 2 +- .../ModActions/events/PostAlertOnMemberJoinEvt.ts | 4 +--- .../ModActions/functions/actualKickMemberCmd.ts | 2 +- backend/src/plugins/Mutes/commands/MutesCmd.ts | 2 +- .../Starboard/util/createStarboardEmbedFromMessage.ts | 2 +- backend/src/plugins/Utility/commands/AvatarCmd.ts | 2 +- backend/src/plugins/Utility/commands/LevelCmd.ts | 2 +- .../src/plugins/Utility/commands/VcdisconnectCmd.ts | 2 +- backend/src/plugins/Utility/commands/VcmoveCmd.ts | 10 +++------- .../src/plugins/Utility/functions/getUserInfoEmbed.ts | 2 +- backend/src/plugins/Utility/search.ts | 4 ++-- 18 files changed, 29 insertions(+), 36 deletions(-) diff --git a/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts b/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts index f0eee5ec..a82c526e 100644 --- a/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts +++ b/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts @@ -42,7 +42,7 @@ export async function* matchMultipleTextTypesOnMessage( } if (trigger.match_visible_names) { - yield ["visiblename", member.nickname || member.user.globalName || msg.data.author.username]; + yield ["visiblename", member.displayName || msg.data.author.username]; } if (trigger.match_usernames) { diff --git a/backend/src/plugins/Automod/triggers/roleAdded.ts b/backend/src/plugins/Automod/triggers/roleAdded.ts index 754be1b3..5d18de99 100644 --- a/backend/src/plugins/Automod/triggers/roleAdded.ts +++ b/backend/src/plugins/Automod/triggers/roleAdded.ts @@ -38,7 +38,7 @@ export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({ const role = pluginData.guild.roles.cache.get(matchResult.extra.matchedRoleId as Snowflake); const roleName = role?.name || "Unknown"; const member = contexts[0].member!; - const memberName = `**${renderUsername(member.user)}** (\`${member.id}\`)`; + const memberName = `**${renderUsername(member)}** (\`${member.id}\`)`; return `Role ${roleName} (\`${matchResult.extra.matchedRoleId}\`) was added to ${memberName}`; }, }); diff --git a/backend/src/plugins/Automod/triggers/roleRemoved.ts b/backend/src/plugins/Automod/triggers/roleRemoved.ts index fc5d5ae3..9452bdda 100644 --- a/backend/src/plugins/Automod/triggers/roleRemoved.ts +++ b/backend/src/plugins/Automod/triggers/roleRemoved.ts @@ -38,7 +38,7 @@ export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({ const role = pluginData.guild.roles.cache.get(matchResult.extra.matchedRoleId as Snowflake); const roleName = role?.name || "Unknown"; const member = contexts[0].member!; - const memberName = `**${renderUsername(member.user)}** (\`${member.id}\`)`; + const memberName = `**${renderUsername(member)}** (\`${member.id}\`)`; return `Role ${roleName} (\`${matchResult.extra.matchedRoleId}\`) was removed from ${memberName}`; }, }); diff --git a/backend/src/plugins/BotControl/commands/ServersCmd.ts b/backend/src/plugins/BotControl/commands/ServersCmd.ts index 3658a36c..5b2cf9c7 100644 --- a/backend/src/plugins/BotControl/commands/ServersCmd.ts +++ b/backend/src/plugins/BotControl/commands/ServersCmd.ts @@ -48,10 +48,9 @@ export const ServersCmd = botControlCmd({ const lines = filteredGuilds.map((g) => { const paddedId = g.id.padEnd(longestId, " "); const owner = getUser(pluginData.client, g.ownerId); - return `\`${paddedId}\` **${g.name}** (${g.memberCount} members) (owner **${renderUsername( - owner.username, - owner.discriminator, - )}** \`${owner.id}\`)`; + return `\`${paddedId}\` **${g.name}** (${g.memberCount} members) (owner **${renderUsername(owner)}** \`${ + owner.id + }\`)`; }); createChunkedMessage(msg.channel, lines.join("\n")); } else { diff --git a/backend/src/plugins/Cases/functions/createCase.ts b/backend/src/plugins/Cases/functions/createCase.ts index 70717f51..c68b899f 100644 --- a/backend/src/plugins/Cases/functions/createCase.ts +++ b/backend/src/plugins/Cases/functions/createCase.ts @@ -8,16 +8,16 @@ import { postCaseToCaseLogChannel } from "./postToCaseLogChannel"; export async function createCase(pluginData: GuildPluginData<CasesPluginType>, args: CaseArgs) { const user = await resolveUser(pluginData.client, args.userId); - const userName = renderUsername(user.username, user.discriminator); + const name = renderUsername(user); const mod = await resolveUser(pluginData.client, args.modId); - const modName = renderUsername(mod.username, mod.discriminator); + const modName = renderUsername(mod); let ppName: string | null = null; let ppId: Snowflake | null = null; if (args.ppId) { const pp = await resolveUser(pluginData.client, args.ppId); - ppName = renderUsername(pp.username, pp.discriminator); + ppName = renderUsername(pp); ppId = pp.id; } @@ -32,7 +32,7 @@ export async function createCase(pluginData: GuildPluginData<CasesPluginType>, a const createdCase = await pluginData.state.cases.create({ type: args.type, user_id: user.id, - user_name: userName, + user_name: name, mod_id: mod.id, mod_name: modName, audit_log_id: args.auditLogId, diff --git a/backend/src/plugins/ModActions/commands/CasesModCmd.ts b/backend/src/plugins/ModActions/commands/CasesModCmd.ts index a911091d..12b6c05c 100644 --- a/backend/src/plugins/ModActions/commands/CasesModCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesModCmd.ts @@ -1,7 +1,7 @@ -import { APIEmbed, GuildMember, User } from "discord.js"; +import { APIEmbed } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage } from "../../../pluginUtils"; -import { emptyEmbedValue, renderUsername, resolveMember, resolveUser, trimLines } from "../../../utils"; +import { UnknownUser, emptyEmbedValue, renderUsername, resolveMember, resolveUser, trimLines } from "../../../utils"; import { asyncMap } from "../../../utils/async"; import { createPaginatedMessage } from "../../../utils/createPaginatedMessage"; import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; @@ -31,7 +31,7 @@ export const CasesModCmd = modActionsCmd({ const mod = (await resolveMember(pluginData.client, pluginData.guild, modId)) || (await resolveUser(pluginData.client, modId)); - const modName = mod instanceof User ? renderUsername(mod) : modId; + const modName = mod instanceof UnknownUser ? modId : renderUsername(mod); const casesPlugin = pluginData.getPlugin(CasesPlugin); const totalCases = await casesPlugin.getTotalCasesByMod(modId); @@ -59,7 +59,7 @@ export const CasesModCmd = modActionsCmd({ const embed = { author: { name: title, - icon_url: mod instanceof User || mod instanceof GuildMember ? mod.displayAvatarURL() : undefined, + icon_url: mod instanceof UnknownUser ? undefined : mod.displayAvatarURL(), }, fields: [ ...getChunkedEmbedFields(emptyEmbedValue, lines.join("\n")), diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts index 05ab65eb..7b3714b6 100644 --- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts @@ -46,7 +46,7 @@ export const CasesUserCmd = modActionsCmd({ const user = (await resolveMember(pluginData.client, pluginData.guild, args.user)) || (await resolveUser(pluginData.client, args.user)); - if (!user.id || user instanceof UnknownUser) { + if (user instanceof UnknownUser) { sendErrorMessage(pluginData, msg.channel, `User not found`); return; } diff --git a/backend/src/plugins/ModActions/commands/WarnCmd.ts b/backend/src/plugins/ModActions/commands/WarnCmd.ts index f917c55e..218eeaf3 100644 --- a/backend/src/plugins/ModActions/commands/WarnCmd.ts +++ b/backend/src/plugins/ModActions/commands/WarnCmd.ts @@ -106,7 +106,7 @@ export const WarnCmd = modActionsCmd({ sendSuccessMessage( pluginData, msg.channel, - `Warned **${renderUsername(memberToWarn.user)}** (Case #${warnResult.case.case_number})${messageResultText}`, + `Warned **${renderUsername(memberToWarn)}** (Case #${warnResult.case.case_number})${messageResultText}`, ); }, }); diff --git a/backend/src/plugins/ModActions/events/PostAlertOnMemberJoinEvt.ts b/backend/src/plugins/ModActions/events/PostAlertOnMemberJoinEvt.ts index 7874c241..82e39547 100644 --- a/backend/src/plugins/ModActions/events/PostAlertOnMemberJoinEvt.ts +++ b/backend/src/plugins/ModActions/events/PostAlertOnMemberJoinEvt.ts @@ -46,9 +46,7 @@ export const PostAlertOnMemberJoinEvt = modActionsEvt({ } await alertChannel.send( - `<@!${member.id}> (${renderUsername(member.user)} \`${member.id}\`) joined with ${ - actions.length - } prior record(s)`, + `<@!${member.id}> (${renderUsername(member)} \`${member.id}\`) joined with ${actions.length} prior record(s)`, ); } }, diff --git a/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts b/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts index fac3f906..a755ee08 100644 --- a/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts +++ b/backend/src/plugins/ModActions/functions/actualKickMemberCmd.ts @@ -103,7 +103,7 @@ export async function actualKickMemberCmd( } // Confirm the action to the moderator - let response = `Kicked **${renderUsername(memberToKick.user)}** (Case #${kickResult.case.case_number})`; + let response = `Kicked **${renderUsername(memberToKick)}** (Case #${kickResult.case.case_number})`; if (kickResult.notifyResult.text) response += ` (${kickResult.notifyResult.text})`; sendSuccessMessage(pluginData, msg.channel, response); diff --git a/backend/src/plugins/Mutes/commands/MutesCmd.ts b/backend/src/plugins/Mutes/commands/MutesCmd.ts index 22cd74c5..5000218c 100644 --- a/backend/src/plugins/Mutes/commands/MutesCmd.ts +++ b/backend/src/plugins/Mutes/commands/MutesCmd.ts @@ -74,7 +74,7 @@ export const MutesCmd = mutesCmd({ totalMutes = manuallyMutedMembers.length; lines = manuallyMutedMembers.map((member) => { - return `<@!${member.id}> (**${renderUsername(member.user)}**, \`${member.id}\`) 🔧 Manual mute`; + return `<@!${member.id}> (**${renderUsername(member)}**, \`${member.id}\`) 🔧 Manual mute`; }); } else { // Show filtered active mutes (but not manual mutes) diff --git a/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts b/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts index 5126f20f..93ba4dfd 100644 --- a/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts +++ b/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts @@ -28,7 +28,7 @@ export function createStarboardEmbedFromMessage( embed.color = color; } - embed.author.icon_url = (msg.member || msg.author).displayAvatarURL(); + embed.author.icon_url = (msg.member ?? msg.author).displayAvatarURL(); // The second condition here checks for messages with only an image link that is then embedded. // The message content in that case is hidden by the Discord client, so we hide it here too. diff --git a/backend/src/plugins/Utility/commands/AvatarCmd.ts b/backend/src/plugins/Utility/commands/AvatarCmd.ts index b1215df6..e4ba07d5 100644 --- a/backend/src/plugins/Utility/commands/AvatarCmd.ts +++ b/backend/src/plugins/Utility/commands/AvatarCmd.ts @@ -14,7 +14,7 @@ export const AvatarCmd = utilityCmd({ }, async run({ message: msg, args, pluginData }) { - const user = args.user || msg.member || msg.author; + const user = args.user ?? msg.member ?? msg.author; if (!(user instanceof UnknownUser)) { const embed: APIEmbed = { image: { diff --git a/backend/src/plugins/Utility/commands/LevelCmd.ts b/backend/src/plugins/Utility/commands/LevelCmd.ts index df4d9325..5830f66a 100644 --- a/backend/src/plugins/Utility/commands/LevelCmd.ts +++ b/backend/src/plugins/Utility/commands/LevelCmd.ts @@ -18,6 +18,6 @@ export const LevelCmd = utilityCmd({ run({ message, args, pluginData }) { const member = args.member || message.member; const level = getMemberLevel(pluginData, member); - message.channel.send(`The permission level of ${renderUsername(member.user)} is **${level}**`); + message.channel.send(`The permission level of ${renderUsername(member)} is **${level}**`); }, }); diff --git a/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts b/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts index 240c763f..b6863599 100644 --- a/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts +++ b/backend/src/plugins/Utility/commands/VcdisconnectCmd.ts @@ -43,7 +43,7 @@ export const VcdisconnectCmd = utilityCmd({ sendSuccessMessage( pluginData, msg.channel, - `**${renderUsername(args.member.user)}** disconnected from **${channel.name}**`, + `**${renderUsername(args.member)}** disconnected from **${channel.name}**`, ); }, }); diff --git a/backend/src/plugins/Utility/commands/VcmoveCmd.ts b/backend/src/plugins/Utility/commands/VcmoveCmd.ts index 6ffe8588..259c60b5 100644 --- a/backend/src/plugins/Utility/commands/VcmoveCmd.ts +++ b/backend/src/plugins/Utility/commands/VcmoveCmd.ts @@ -80,7 +80,7 @@ export const VcmoveCmd = utilityCmd({ newChannel: channel, }); - sendSuccessMessage(pluginData, msg.channel, `**${renderUsername(args.member.user)}** moved to **${channel.name}**`); + sendSuccessMessage(pluginData, msg.channel, `**${renderUsername(args.member)}** moved to **${channel.name}**`); }, }); @@ -153,7 +153,7 @@ export const VcmoveAllCmd = utilityCmd({ sendErrorMessage( pluginData, msg.channel, - `Failed to move ${renderUsername(currMember.user)} (${currMember.id}): You cannot act on this member`, + `Failed to move ${renderUsername(currMember)} (${currMember.id}): You cannot act on this member`, ); errAmt++; continue; @@ -168,11 +168,7 @@ export const VcmoveAllCmd = utilityCmd({ sendErrorMessage(pluginData, msg.channel, "Unknown error when trying to move members"); return; } - sendErrorMessage( - pluginData, - msg.channel, - `Failed to move ${renderUsername(currMember.user)} (${currMember.id})`, - ); + sendErrorMessage(pluginData, msg.channel, `Failed to move ${renderUsername(currMember)} (${currMember.id})`); errAmt++; continue; } diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts index d8ccc833..88e4aba0 100644 --- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts @@ -43,7 +43,7 @@ export async function getUserInfoEmbed( name: `${user.bot ? "Bot" : "User"}: ${renderUsername(user)}`, }; - const avatarURL = (member || user).displayAvatarURL(); + const avatarURL = (member ?? user).displayAvatarURL(); embed.author.icon_url = avatarURL; if (compact) { diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index 632a85a7..76c5af9c 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -381,7 +381,7 @@ async function performMemberSearch( return true; } - const fullUsername = renderUsername(member.user); + const fullUsername = renderUsername(member); if (await execRegExp(queryRegex, fullUsername).catch(allowTimeout)) return true; return false; @@ -492,7 +492,7 @@ function formatSearchResultList(members: Array<GuildMember | User>): string { const paddedId = member.id.padEnd(longestId, " "); let line; if (member instanceof GuildMember) { - line = `${paddedId} ${renderUsername(member.user)}`; + line = `${paddedId} ${renderUsername(member)}`; if (member.nickname) line += ` (${member.nickname})`; } else { line = `${paddedId} ${renderUsername(member)}`; From 2ac7ae85ce5db8c1551aa5cb9d18e2797a55ef4b Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 17:05:04 +0000 Subject: [PATCH 21/51] missed one Signed-off-by: GitHub <noreply@github.com> --- .../plugins/ReactionRoles/util/addMemberPendingRoleChange.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts b/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts index dda8a6bc..8f25a0ae 100644 --- a/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts +++ b/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts @@ -33,7 +33,7 @@ export async function addMemberPendingRoleChange( try { await member.roles.set(Array.from(newRoleIds.values()), "Reaction roles"); } catch (e) { - logger.warn(`Failed to apply role changes to ${renderUsername(member.user)} (${member.id}): ${e.message}`); + logger.warn(`Failed to apply role changes to ${renderUsername(member)} (${member.id}): ${e.message}`); } } lock.unlock(); From ae651c8a705fa82379d3b687e7fe46c2de474d56 Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 10:55:47 +0000 Subject: [PATCH 22/51] paginated user cases Signed-off-by: GitHub <noreply@github.com> --- .../ModActions/commands/CasesUserCmd.ts | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts index 069ad31f..6e5aaf88 100644 --- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts @@ -5,6 +5,7 @@ import { sendErrorMessage } from "../../../pluginUtils"; import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; import { UnknownUser, chunkArray, emptyEmbedValue, renderUserUsername, resolveUser, trimLines } from "../../../utils"; import { asyncMap } from "../../../utils/async"; +import { createPaginatedMessage } from "../../../utils/createPaginatedMessage.js"; import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; import { getGuildPrefix } from "../../../utils/getGuildPrefix"; import { modActionsCmd } from "../types"; @@ -21,6 +22,8 @@ const opts = { unbans: ct.switchOption({ def: false, shortcut: "ub" }), }; +const casesPerPage = 10; + export const CasesUserCmd = modActionsCmd({ trigger: ["cases", "modlogs"], permission: "can_view", @@ -90,49 +93,57 @@ export const CasesUserCmd = modActionsCmd({ } else { // Compact view (= regular message with a preview of each case) const casesPlugin = pluginData.getPlugin(CasesPlugin); - const lines = await asyncMap(casesToDisplay, (c) => casesPlugin.getCaseSummary(c, true, msg.author.id)); + const totalPages = Math.max(Math.ceil(cases.length / casesPerPage), 1); const prefix = getGuildPrefix(pluginData); - const linesPerChunk = 10; - const lineChunks = chunkArray(lines, linesPerChunk); - const footerField = { - name: emptyEmbedValue, - value: trimLines(` - Use \`${prefix}case <num>\` to see more information about an individual case - `), - }; + createPaginatedMessage( + pluginData.client, + msg.channel, + totalPages, + async (page) => { + const chunkedCases = chunkArray(cases, casesPerPage)[page - 1]; + const lines = await asyncMap(chunkedCases, (c) => casesPlugin.getCaseSummary(c, true, msg.author.id)); - for (const [i, linesInChunk] of lineChunks.entries()) { - const isLastChunk = i === lineChunks.length - 1; + const isLastPage = page === totalPages; + const firstCaseNum = (page - 1) * casesPerPage + 1; + const lastCaseNum = page * casesPerPage; + const title = + totalPages === 1 + ? `Cases for ${userName} (${lines.length} total)` + : `Most recent cases ${firstCaseNum}-${lastCaseNum} of ${cases.length} for ${userName}`; - if (isLastChunk && !args.hidden && hiddenCases.length) { - if (hiddenCases.length === 1) { - linesInChunk.push(`*+${hiddenCases.length} hidden case, use "-hidden" to show it*`); - } else { - linesInChunk.push(`*+${hiddenCases.length} hidden cases, use "-hidden" to show them*`); - } - } + const embed = { + author: { + name: title, + icon_url: user instanceof User ? user.displayAvatarURL() : undefined, + }, + fields: [ + ...getChunkedEmbedFields(emptyEmbedValue, lines.join("\n")), + { + name: emptyEmbedValue, + value: trimLines(` + Use \`${prefix}case <num>\` to see more information about an individual case + `), + }, + ], + } satisfies APIEmbed; - const chunkStart = i * linesPerChunk + 1; - const chunkEnd = Math.min((i + 1) * linesPerChunk, lines.length); + if (isLastPage && !args.hidden && hiddenCases.length) + embed.fields.push({ + name: emptyEmbedValue, + value: + hiddenCases.length === 1 + ? `*+${hiddenCases.length} hidden case, use "-hidden" to show it*` + : `*+${hiddenCases.length} hidden cases, use "-hidden" to show them*`, + }); - const embed = { - author: { - name: - lineChunks.length === 1 - ? `Cases for ${userName} (${lines.length} total)` - : `Cases ${chunkStart}–${chunkEnd} of ${lines.length} for ${userName}`, - icon_url: user instanceof User ? user.displayAvatarURL() : undefined, - }, - fields: [ - ...getChunkedEmbedFields(emptyEmbedValue, linesInChunk.join("\n")), - ...(isLastChunk ? [footerField] : []), - ], - } satisfies APIEmbed; - - msg.channel.send({ embeds: [embed] }); - } + return { embeds: [embed] }; + }, + { + limitToUserId: msg.author.id, + }, + ); } } }, From bb4fdf653095002a916b93e4daf8cceaea511022 Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Sun, 26 Nov 2023 11:49:33 +0000 Subject: [PATCH 23/51] move embed fields to embed description Signed-off-by: GitHub <noreply@github.com> --- backend/src/plugins/ModActions/commands/CasesModCmd.ts | 8 ++++---- backend/src/plugins/ModActions/commands/CasesUserCmd.ts | 9 +++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/backend/src/plugins/ModActions/commands/CasesModCmd.ts b/backend/src/plugins/ModActions/commands/CasesModCmd.ts index 5b0e3273..d520eede 100644 --- a/backend/src/plugins/ModActions/commands/CasesModCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesModCmd.ts @@ -4,7 +4,6 @@ import { sendErrorMessage } from "../../../pluginUtils"; import { emptyEmbedValue, resolveUser, trimLines } from "../../../utils"; import { asyncMap } from "../../../utils/async"; import { createPaginatedMessage } from "../../../utils/createPaginatedMessage"; -import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; import { getGuildPrefix } from "../../../utils/getGuildPrefix"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { modActionsCmd } from "../types"; @@ -13,7 +12,7 @@ const opts = { mod: ct.userId({ option: true }), }; -const casesPerPage = 5; +const casesPerPage = 10; export const CasesModCmd = modActionsCmd({ trigger: ["cases", "modlogs", "infractions"], @@ -50,8 +49,9 @@ export const CasesModCmd = modActionsCmd({ const cases = await casesPlugin.getRecentCasesByMod(modId, casesPerPage, (page - 1) * casesPerPage); const lines = await asyncMap(cases, (c) => casesPlugin.getCaseSummary(c, true, msg.author.id)); + const isLastPage = page === totalPages; const firstCaseNum = (page - 1) * casesPerPage + 1; - const lastCaseNum = page * casesPerPage; + const lastCaseNum = isLastPage ? totalCases : page * casesPerPage; const title = `Most recent cases ${firstCaseNum}-${lastCaseNum} of ${totalCases} by ${modName}`; const embed = { @@ -59,8 +59,8 @@ export const CasesModCmd = modActionsCmd({ name: title, icon_url: mod instanceof User ? mod.displayAvatarURL() : undefined, }, + description: lines.join("\n"), fields: [ - ...getChunkedEmbedFields(emptyEmbedValue, lines.join("\n")), { name: emptyEmbedValue, value: trimLines(` diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts index 6e5aaf88..15f42cf6 100644 --- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts @@ -6,7 +6,6 @@ import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; import { UnknownUser, chunkArray, emptyEmbedValue, renderUserUsername, resolveUser, trimLines } from "../../../utils"; import { asyncMap } from "../../../utils/async"; import { createPaginatedMessage } from "../../../utils/createPaginatedMessage.js"; -import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; import { getGuildPrefix } from "../../../utils/getGuildPrefix"; import { modActionsCmd } from "../types"; @@ -107,7 +106,7 @@ export const CasesUserCmd = modActionsCmd({ const isLastPage = page === totalPages; const firstCaseNum = (page - 1) * casesPerPage + 1; - const lastCaseNum = page * casesPerPage; + const lastCaseNum = isLastPage ? cases.length : page * casesPerPage; const title = totalPages === 1 ? `Cases for ${userName} (${lines.length} total)` @@ -118,13 +117,11 @@ export const CasesUserCmd = modActionsCmd({ name: title, icon_url: user instanceof User ? user.displayAvatarURL() : undefined, }, + description: lines.join("\n"), fields: [ - ...getChunkedEmbedFields(emptyEmbedValue, lines.join("\n")), { name: emptyEmbedValue, - value: trimLines(` - Use \`${prefix}case <num>\` to see more information about an individual case - `), + value: trimLines(`Use \`${prefix}case <num>\` to see more information about an individual case`), }, ], } satisfies APIEmbed; From c53bfaa2332e4fc948ba338b1188625103f5528e Mon Sep 17 00:00:00 2001 From: Tiago R <metal@i0.tf> Date: Fri, 29 Dec 2023 17:03:45 +0000 Subject: [PATCH 24/51] remove trimLines Signed-off-by: GitHub <noreply@github.com> --- backend/src/plugins/ModActions/commands/CasesUserCmd.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts index 15f42cf6..e8ce50ad 100644 --- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts @@ -3,7 +3,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { CaseTypes } from "../../../data/CaseTypes"; import { sendErrorMessage } from "../../../pluginUtils"; import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; -import { UnknownUser, chunkArray, emptyEmbedValue, renderUserUsername, resolveUser, trimLines } from "../../../utils"; +import { UnknownUser, chunkArray, emptyEmbedValue, renderUserUsername, resolveUser } from "../../../utils"; import { asyncMap } from "../../../utils/async"; import { createPaginatedMessage } from "../../../utils/createPaginatedMessage.js"; import { getGuildPrefix } from "../../../utils/getGuildPrefix"; @@ -121,7 +121,7 @@ export const CasesUserCmd = modActionsCmd({ fields: [ { name: emptyEmbedValue, - value: trimLines(`Use \`${prefix}case <num>\` to see more information about an individual case`), + value: `Use \`${prefix}case <num>\` to see more information about an individual case`, }, ], } satisfies APIEmbed; From 28692962bcd437659e05489f55a86fa5269e0681 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 14 Jan 2024 14:25:42 +0000 Subject: [PATCH 25/51] refactor: replace io-ts with zod --- backend/package-lock.json | 9 - backend/package.json | 1 - backend/src/commandTypes.ts | 2 +- backend/src/configValidator.ts | 16 +- backend/src/pluginUtils.ts | 48 +-- .../plugins/AutoDelete/AutoDeletePlugin.ts | 7 +- backend/src/plugins/AutoDelete/types.ts | 13 +- .../AutoReactions/AutoReactionsPlugin.ts | 7 +- backend/src/plugins/AutoReactions/types.ts | 9 +- backend/src/plugins/Automod/AutomodPlugin.ts | 132 +------- .../src/plugins/Automod/actions/addRoles.ts | 9 +- .../plugins/Automod/actions/addToCounter.ts | 15 +- backend/src/plugins/Automod/actions/alert.ts | 22 +- .../plugins/Automod/actions/archiveThread.ts | 7 +- .../Automod/actions/availableActions.ts | 26 +- backend/src/plugins/Automod/actions/ban.ts | 30 +- .../plugins/Automod/actions/changeNickname.ts | 14 +- .../plugins/Automod/actions/changePerms.ts | 22 +- backend/src/plugins/Automod/actions/clean.ts | 5 +- .../plugins/Automod/actions/exampleAction.ts | 9 +- backend/src/plugins/Automod/actions/kick.ts | 22 +- backend/src/plugins/Automod/actions/log.ts | 5 +- backend/src/plugins/Automod/actions/mute.ts | 28 +- .../plugins/Automod/actions/removeRoles.ts | 8 +- backend/src/plugins/Automod/actions/reply.ts | 22 +- .../Automod/actions/setAntiraidLevel.ts | 6 +- .../src/plugins/Automod/actions/setCounter.ts | 11 +- .../plugins/Automod/actions/setSlowmode.ts | 14 +- .../plugins/Automod/actions/startThread.ts | 20 +- backend/src/plugins/Automod/actions/warn.ts | 22 +- .../functions/createMessageSpamTrigger.ts | 19 +- backend/src/plugins/Automod/helpers.ts | 38 +-- backend/src/plugins/Automod/info.ts | 4 +- .../plugins/Automod/triggers/antiraidLevel.ts | 13 +- .../plugins/Automod/triggers/anyMessage.ts | 8 +- .../Automod/triggers/availableTriggers.ts | 45 +-- backend/src/plugins/Automod/triggers/ban.ts | 17 +- .../Automod/triggers/counterTrigger.ts | 17 +- .../Automod/triggers/exampleTrigger.ts | 14 +- backend/src/plugins/Automod/triggers/kick.ts | 17 +- .../Automod/triggers/matchAttachmentType.ts | 39 ++- .../plugins/Automod/triggers/matchInvites.ts | 42 +-- .../plugins/Automod/triggers/matchLinks.ts | 60 ++-- .../plugins/Automod/triggers/matchMimeType.ts | 39 ++- .../plugins/Automod/triggers/matchRegex.ts | 42 +-- .../plugins/Automod/triggers/matchWords.ts | 49 ++- .../plugins/Automod/triggers/memberJoin.ts | 19 +- .../Automod/triggers/memberJoinSpam.ts | 16 +- .../plugins/Automod/triggers/memberLeave.ts | 8 +- backend/src/plugins/Automod/triggers/mute.ts | 17 +- backend/src/plugins/Automod/triggers/note.ts | 7 +- .../src/plugins/Automod/triggers/roleAdded.ts | 13 +- .../plugins/Automod/triggers/roleRemoved.ts | 13 +- .../plugins/Automod/triggers/threadArchive.ts | 13 +- .../plugins/Automod/triggers/threadCreate.ts | 7 +- .../Automod/triggers/threadCreateSpam.ts | 16 +- .../plugins/Automod/triggers/threadDelete.ts | 7 +- .../Automod/triggers/threadUnarchive.ts | 13 +- backend/src/plugins/Automod/triggers/unban.ts | 7 +- .../src/plugins/Automod/triggers/unmute.ts | 7 +- backend/src/plugins/Automod/triggers/warn.ts | 17 +- backend/src/plugins/Automod/types.ts | 88 +++-- .../plugins/BotControl/BotControlPlugin.ts | 6 +- backend/src/plugins/BotControl/types.ts | 21 +- backend/src/plugins/Cases/CasesPlugin.ts | 8 +- backend/src/plugins/Cases/types.ts | 26 +- backend/src/plugins/Censor/CensorPlugin.ts | 7 +- backend/src/plugins/Censor/types.ts | 36 +-- .../ChannelArchiver/ChannelArchiverPlugin.ts | 7 +- .../CompanionChannelsPlugin.ts | 7 +- .../src/plugins/CompanionChannels/types.ts | 29 +- .../plugins/ContextMenus/ContextMenuPlugin.ts | 5 +- .../src/plugins/ContextMenus/actions/mute.ts | 4 +- backend/src/plugins/ContextMenus/types.ts | 24 +- .../src/plugins/Counters/CountersPlugin.ts | 68 +--- .../getPrettyNameForCounterTrigger.ts | 4 +- backend/src/plugins/Counters/types.ts | 119 +++++-- .../CustomEvents/CustomEventsPlugin.ts | 5 +- .../CustomEvents/actions/addRoleAction.ts | 14 +- .../CustomEvents/actions/createCaseAction.ts | 19 +- .../actions/makeRoleMentionableAction.ts | 14 +- .../actions/makeRoleUnmentionableAction.ts | 11 +- .../CustomEvents/actions/messageAction.ts | 13 +- .../actions/moveToVoiceChannelAction.ts | 14 +- .../actions/setChannelPermissionOverrides.ts | 25 +- backend/src/plugins/CustomEvents/types.ts | 67 ++-- .../GuildAccessMonitorPlugin.ts | 5 +- .../GuildConfigReloaderPlugin.ts | 5 +- .../GuildInfoSaver/GuildInfoSaverPlugin.ts | 5 +- .../GuildMemberCachePlugin.ts | 6 +- .../InternalPoster/InternalPosterPlugin.ts | 7 +- backend/src/plugins/InternalPoster/types.ts | 6 - .../plugins/LocateUser/LocateUserPlugin.ts | 7 +- backend/src/plugins/LocateUser/types.ts | 11 +- backend/src/plugins/Logs/LogsPlugin.ts | 18 +- .../Logs/logFunctions/logMessageDelete.ts | 2 +- backend/src/plugins/Logs/types.ts | 82 ++--- .../MessageSaver/MessageSaverPlugin.ts | 5 +- backend/src/plugins/MessageSaver/types.ts | 9 +- .../plugins/ModActions/ModActionsPlugin.ts | 8 +- backend/src/plugins/ModActions/types.ts | 73 +++-- backend/src/plugins/Mutes/MutesPlugin.ts | 8 +- backend/src/plugins/Mutes/types.ts | 39 ++- .../plugins/NameHistory/NameHistoryPlugin.ts | 5 +- backend/src/plugins/NameHistory/types.ts | 9 +- backend/src/plugins/Persist/PersistPlugin.ts | 7 +- backend/src/plugins/Persist/types.ts | 14 +- .../plugins/Phisherman/PhishermanPlugin.ts | 6 +- backend/src/plugins/Phisherman/info.ts | 4 +- backend/src/plugins/Phisherman/types.ts | 10 +- .../PingableRoles/PingableRolesPlugin.ts | 7 +- backend/src/plugins/PingableRoles/types.ts | 9 +- backend/src/plugins/Post/PostPlugin.ts | 7 +- backend/src/plugins/Post/types.ts | 9 +- .../ReactionRoles/ReactionRolesPlugin.ts | 7 +- backend/src/plugins/ReactionRoles/types.ts | 24 +- .../src/plugins/Reminders/RemindersPlugin.ts | 7 +- backend/src/plugins/Reminders/types.ts | 9 +- .../plugins/RoleButtons/RoleButtonsPlugin.ts | 41 +-- backend/src/plugins/RoleButtons/info.ts | 4 +- backend/src/plugins/RoleButtons/types.ts | 126 +++++--- .../plugins/RoleManager/RoleManagerPlugin.ts | 6 +- backend/src/plugins/RoleManager/types.ts | 7 +- backend/src/plugins/Roles/RolesPlugin.ts | 9 +- backend/src/plugins/Roles/types.ts | 13 +- .../SelfGrantableRolesPlugin.ts | 22 +- .../src/plugins/SelfGrantableRoles/types.ts | 38 ++- .../src/plugins/Slowmode/SlowmodePlugin.ts | 7 +- backend/src/plugins/Slowmode/types.ts | 15 +- backend/src/plugins/Spam/SpamPlugin.ts | 7 +- backend/src/plugins/Spam/types.ts | 53 +-- .../src/plugins/Starboard/StarboardPlugin.ts | 16 +- backend/src/plugins/Starboard/types.ts | 46 ++- .../Starboard/util/preprocessStaticConfig.ts | 12 - backend/src/plugins/Tags/TagsPlugin.ts | 28 +- backend/src/plugins/Tags/types.ts | 77 +++-- .../src/plugins/Tags/util/findTagByName.ts | 5 +- .../src/plugins/Tags/util/onMessageCreate.ts | 9 +- .../plugins/TimeAndDate/TimeAndDatePlugin.ts | 8 +- backend/src/plugins/TimeAndDate/types.ts | 20 +- .../UsernameSaver/UsernameSaverPlugin.ts | 6 +- backend/src/plugins/UsernameSaver/types.ts | 4 + backend/src/plugins/Utility/UtilityPlugin.ts | 8 +- backend/src/plugins/Utility/search.ts | 3 +- backend/src/plugins/Utility/types.ts | 61 ++-- .../WelcomeMessage/WelcomeMessagePlugin.ts | 7 +- backend/src/plugins/WelcomeMessage/types.ts | 14 +- .../src/plugins/ZeppelinPluginBlueprint.ts | 4 +- backend/src/types.ts | 31 +- backend/src/utils.test.ts | 6 +- backend/src/utils.ts | 302 ++++++------------ backend/src/utils/iotsUtils.ts | 17 - backend/src/utils/tColor.ts | 16 - backend/src/utils/tValidTimezone.ts | 13 - backend/src/utils/typeUtils.ts | 8 + backend/src/utils/zColor.ts | 15 + backend/src/utils/zValidTimezone.ts | 8 + backend/src/validation.test.ts | 40 --- backend/src/validatorUtils.ts | 140 -------- package-lock.json | 7 + package.json | 1 + 161 files changed, 1450 insertions(+), 2105 deletions(-) delete mode 100644 backend/src/plugins/Starboard/util/preprocessStaticConfig.ts delete mode 100644 backend/src/utils/iotsUtils.ts delete mode 100644 backend/src/utils/tColor.ts delete mode 100644 backend/src/utils/tValidTimezone.ts create mode 100644 backend/src/utils/zColor.ts create mode 100644 backend/src/utils/zValidTimezone.ts delete mode 100644 backend/src/validation.test.ts delete mode 100644 backend/src/validatorUtils.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index b1d6d189..1f2f5992 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -22,7 +22,6 @@ "express": "^4.17.0", "fp-ts": "^2.0.1", "humanize-duration": "^3.15.0", - "io-ts": "^2.0.0", "js-yaml": "^3.13.1", "knub": "^32.0.0-next.16", "knub-command-manager": "^9.1.0", @@ -4975,14 +4974,6 @@ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" }, - "node_modules/io-ts": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.20.tgz", - "integrity": "sha512-Rq2BsYmtwS5vVttie4rqrOCIfHCS9TgpRLFpKQCM1wZBBRY9nWVGmEvm2FnDbSE2un1UE39DvFpTR5UL47YDcA==", - "peerDependencies": { - "fp-ts": "^2.5.0" - } - }, "node_modules/iota-array": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 2ff53482..fbbe2ddc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -43,7 +43,6 @@ "express": "^4.17.0", "fp-ts": "^2.0.1", "humanize-duration": "^3.15.0", - "io-ts": "^2.0.0", "js-yaml": "^3.13.1", "knub": "^32.0.0-next.16", "knub-command-manager": "^9.1.0", diff --git a/backend/src/commandTypes.ts b/backend/src/commandTypes.ts index 47ad0405..9ace5312 100644 --- a/backend/src/commandTypes.ts +++ b/backend/src/commandTypes.ts @@ -17,6 +17,7 @@ import { createTypeHelper } from "knub-command-manager"; import { channelMentionRegex, convertDelayStringToMS, + inputPatternToRegExp, isValidSnowflake, resolveMember, resolveUser, @@ -26,7 +27,6 @@ import { } from "./utils"; import { isValidTimezone } from "./utils/isValidTimezone"; import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarget"; -import { inputPatternToRegExp } from "./validatorUtils"; export const commandTypes = { ...messageCommandBaseTypeConverters, diff --git a/backend/src/configValidator.ts b/backend/src/configValidator.ts index 3bb602a4..96e08ba3 100644 --- a/backend/src/configValidator.ts +++ b/backend/src/configValidator.ts @@ -1,9 +1,9 @@ -import { ConfigValidationError, PluginConfigManager } from "knub"; +import { PluginConfigManager } from "knub"; import moment from "moment-timezone"; +import { ZodError } from "zod"; import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin"; import { guildPlugins } from "./plugins/availablePlugins"; -import { PartialZeppelinGuildConfigSchema, ZeppelinGuildConfig } from "./types"; -import { StrictValidationError, decodeAndValidateStrict } from "./validatorUtils"; +import { ZeppelinGuildConfig, zZeppelinGuildConfig } from "./types"; const pluginNameToPlugin = new Map<string, ZeppelinPlugin>(); for (const plugin of guildPlugins) { @@ -11,8 +11,10 @@ for (const plugin of guildPlugins) { } export async function validateGuildConfig(config: any): Promise<string | null> { - const validationResult = decodeAndValidateStrict(PartialZeppelinGuildConfigSchema, config); - if (validationResult instanceof StrictValidationError) return validationResult.getErrors(); + const validationResult = zZeppelinGuildConfig.safeParse(config); + if (!validationResult.success) { + return validationResult.error.issues.join("\n"); + } const guildConfig = config as ZeppelinGuildConfig; @@ -41,8 +43,8 @@ export async function validateGuildConfig(config: any): Promise<string | null> { try { await configManager.init(); } catch (err) { - if (err instanceof ConfigValidationError || err instanceof StrictValidationError) { - return `${pluginName}: ${err.message}`; + if (err instanceof ZodError) { + return `${pluginName}: ${err.issues.join("\n")}`; } throw err; diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 8e6c6da2..a2f744d9 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -10,22 +10,18 @@ import { PermissionsBitField, TextBasedChannel, } from "discord.js"; -import * as t from "io-ts"; import { AnyPluginData, CommandContext, - ConfigValidationError, ExtendedMatchParams, GuildPluginData, - PluginOverrideCriteria, - helpers, + helpers } from "knub"; import { logger } from "./logger"; import { isStaff } from "./staff"; import { TZeppelinKnub } from "./types"; -import { errorMessage, successMessage, tNullable } from "./utils"; +import { errorMessage, successMessage } from "./utils"; import { Tail } from "./utils/typeUtils"; -import { StrictValidationError, parseIoTsSchema } from "./validatorUtils"; const { getMemberLevel } = helpers; @@ -59,46 +55,6 @@ export async function hasPermission( return helpers.hasPermission(config, permission); } -const PluginOverrideCriteriaType: t.Type<PluginOverrideCriteria<unknown>> = t.recursion( - "PluginOverrideCriteriaType", - () => - t.partial({ - channel: tNullable(t.union([t.string, t.array(t.string)])), - category: tNullable(t.union([t.string, t.array(t.string)])), - level: tNullable(t.union([t.string, t.array(t.string)])), - user: tNullable(t.union([t.string, t.array(t.string)])), - role: tNullable(t.union([t.string, t.array(t.string)])), - - all: tNullable(t.array(PluginOverrideCriteriaType)), - any: tNullable(t.array(PluginOverrideCriteriaType)), - not: tNullable(PluginOverrideCriteriaType), - - extra: t.unknown, - }), -); - -export function strictValidationErrorToConfigValidationError(err: StrictValidationError) { - return new ConfigValidationError( - err - .getErrors() - .map((e) => e.toString()) - .join("\n"), - ); -} - -export function makeIoTsConfigParser<Schema extends t.Type<any>>(schema: Schema): (input: unknown) => t.TypeOf<Schema> { - return (input: unknown) => { - try { - return parseIoTsSchema(schema, input); - } catch (err) { - if (err instanceof StrictValidationError) { - throw strictValidationErrorToConfigValidationError(err); - } - throw err; - } - }; -} - export async function sendSuccessMessage( pluginData: AnyPluginData<any>, channel: TextBasedChannel, diff --git a/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts b/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts index 0a8e0438..d1273577 100644 --- a/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts +++ b/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts @@ -1,11 +1,10 @@ import { PluginOptions } from "knub"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; -import { AutoDeletePluginType, ConfigSchema } from "./types"; +import { AutoDeletePluginType, zAutoDeleteConfig } from "./types"; import { onMessageCreate } from "./util/onMessageCreate"; import { onMessageDelete } from "./util/onMessageDelete"; import { onMessageDeleteBulk } from "./util/onMessageDeleteBulk"; @@ -24,11 +23,11 @@ export const AutoDeletePlugin = zeppelinGuildPlugin<AutoDeletePluginType>()({ prettyName: "Auto-delete", description: "Allows Zeppelin to auto-delete messages from a channel after a delay", configurationGuide: "Maximum deletion delay is currently 5 minutes", - configSchema: ConfigSchema, + configSchema: zAutoDeleteConfig, }, dependencies: () => [TimeAndDatePlugin, LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zAutoDeleteConfig.parse(input), defaultOptions, beforeLoad(pluginData) { diff --git a/backend/src/plugins/AutoDelete/types.ts b/backend/src/plugins/AutoDelete/types.ts index 24389860..69100438 100644 --- a/backend/src/plugins/AutoDelete/types.ts +++ b/backend/src/plugins/AutoDelete/types.ts @@ -1,10 +1,10 @@ -import * as t from "io-ts"; import { BasePluginType } from "knub"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { SavedMessage } from "../../data/entities/SavedMessage"; -import { MINUTES, tDelayString } from "../../utils"; +import { MINUTES, zDelayString } from "../../utils"; import Timeout = NodeJS.Timeout; +import z from "zod"; export const MAX_DELAY = 5 * MINUTES; @@ -13,14 +13,13 @@ export interface IDeletionQueueItem { message: SavedMessage; } -export const ConfigSchema = t.type({ - enabled: t.boolean, - delay: tDelayString, +export const zAutoDeleteConfig = z.strictObject({ + enabled: z.boolean(), + delay: zDelayString, }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface AutoDeletePluginType extends BasePluginType { - config: TConfigSchema; + config: z.output<typeof zAutoDeleteConfig>; state: { guildSavedMessages: GuildSavedMessages; guildLogs: GuildLogs; diff --git a/backend/src/plugins/AutoReactions/AutoReactionsPlugin.ts b/backend/src/plugins/AutoReactions/AutoReactionsPlugin.ts index 8ec1ecd7..9b01d538 100644 --- a/backend/src/plugins/AutoReactions/AutoReactionsPlugin.ts +++ b/backend/src/plugins/AutoReactions/AutoReactionsPlugin.ts @@ -1,14 +1,13 @@ import { PluginOptions } from "knub"; import { GuildAutoReactions } from "../../data/GuildAutoReactions"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { trimPluginDescription } from "../../utils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { DisableAutoReactionsCmd } from "./commands/DisableAutoReactionsCmd"; import { NewAutoReactionsCmd } from "./commands/NewAutoReactionsCmd"; import { AddReactionsEvt } from "./events/AddReactionsEvt"; -import { AutoReactionsPluginType, ConfigSchema } from "./types"; +import { AutoReactionsPluginType, zAutoReactionsConfig } from "./types"; const defaultOptions: PluginOptions<AutoReactionsPluginType> = { config: { @@ -32,7 +31,7 @@ export const AutoReactionsPlugin = zeppelinGuildPlugin<AutoReactionsPluginType>( description: trimPluginDescription(` Allows setting up automatic reactions to all new messages on a channel `), - configSchema: ConfigSchema, + configSchema: zAutoReactionsConfig, }, // prettier-ignore @@ -40,7 +39,7 @@ export const AutoReactionsPlugin = zeppelinGuildPlugin<AutoReactionsPluginType>( LogsPlugin, ], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zAutoReactionsConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/AutoReactions/types.ts b/backend/src/plugins/AutoReactions/types.ts index a02c3c26..996fba8d 100644 --- a/backend/src/plugins/AutoReactions/types.ts +++ b/backend/src/plugins/AutoReactions/types.ts @@ -1,17 +1,16 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildAutoReactions } from "../../data/GuildAutoReactions"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { AutoReaction } from "../../data/entities/AutoReaction"; -export const ConfigSchema = t.type({ - can_manage: t.boolean, +export const zAutoReactionsConfig = z.strictObject({ + can_manage: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface AutoReactionsPluginType extends BasePluginType { - config: TConfigSchema; + config: z.output<typeof zAutoReactionsConfig>; state: { logs: GuildLogs; savedMessages: GuildSavedMessages; diff --git a/backend/src/plugins/Automod/AutomodPlugin.ts b/backend/src/plugins/Automod/AutomodPlugin.ts index ff4f01fd..9634c25d 100644 --- a/backend/src/plugins/Automod/AutomodPlugin.ts +++ b/backend/src/plugins/Automod/AutomodPlugin.ts @@ -1,4 +1,4 @@ -import { configUtils, CooldownManager } from "knub"; +import { CooldownManager } from "knub"; import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; @@ -8,7 +8,6 @@ import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { MINUTES, SECONDS } from "../../utils"; import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap"; import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap"; -import { parseIoTsSchema, StrictValidationError } from "../../validatorUtils"; import { CountersPlugin } from "../Counters/CountersPlugin"; import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin"; import { LogsPlugin } from "../Logs/LogsPlugin"; @@ -17,7 +16,6 @@ import { MutesPlugin } from "../Mutes/MutesPlugin"; import { PhishermanPlugin } from "../Phisherman/PhishermanPlugin"; import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; -import { availableActions } from "./actions/availableActions"; import { AntiraidClearCmd } from "./commands/AntiraidClearCmd"; import { SetAntiraidCmd } from "./commands/SetAntiraidCmd"; import { ViewAntiraidCmd } from "./commands/ViewAntiraidCmd"; @@ -35,8 +33,7 @@ import { clearOldRecentNicknameChanges } from "./functions/clearOldNicknameChang import { clearOldRecentActions } from "./functions/clearOldRecentActions"; import { clearOldRecentSpam } from "./functions/clearOldRecentSpam"; import { pluginInfo } from "./info"; -import { availableTriggers } from "./triggers/availableTriggers"; -import { AutomodPluginType, ConfigSchema } from "./types"; +import { AutomodPluginType, zAutomodConfig } from "./types"; const defaultOptions = { config: { @@ -61,129 +58,6 @@ const defaultOptions = { ], }; -/** - * Config preprocessor to set default values for triggers and perform extra validation - * TODO: Separate input and output types - */ -const configParser = (input: unknown) => { - const rules = (input as any).rules; - if (rules) { - // Loop through each rule - for (const [name, rule] of Object.entries(rules)) { - if (rule == null) { - delete rules[name]; - continue; - } - - rule["name"] = name; - - // If the rule doesn't have an explicitly set "enabled" property, set it to true - if (rule["enabled"] == null) { - rule["enabled"] = true; - } - - if (rule["allow_further_rules"] == null) { - rule["allow_further_rules"] = false; - } - - if (rule["affects_bots"] == null) { - rule["affects_bots"] = false; - } - - if (rule["affects_self"] == null) { - rule["affects_self"] = false; - } - - // Loop through the rule's triggers - if (rule["triggers"]) { - for (const triggerObj of rule["triggers"]) { - for (const triggerName in triggerObj) { - if (!availableTriggers[triggerName]) { - throw new StrictValidationError([`Unknown trigger '${triggerName}' in rule '${rule["name"]}'`]); - } - - const triggerBlueprint = availableTriggers[triggerName]; - - if (typeof triggerBlueprint.defaultConfig === "object" && triggerBlueprint.defaultConfig != null) { - triggerObj[triggerName] = configUtils.mergeConfig( - triggerBlueprint.defaultConfig, - triggerObj[triggerName] || {}, - ); - } else { - triggerObj[triggerName] = triggerObj[triggerName] || triggerBlueprint.defaultConfig; - } - - if (triggerObj[triggerName].match_attachment_type) { - const white = triggerObj[triggerName].match_attachment_type.whitelist_enabled; - const black = triggerObj[triggerName].match_attachment_type.blacklist_enabled; - - if (white && black) { - throw new StrictValidationError([ - `Cannot have both blacklist and whitelist enabled at rule <${rule["name"]}/match_attachment_type>`, - ]); - } else if (!white && !black) { - throw new StrictValidationError([ - `Must have either blacklist or whitelist enabled at rule <${rule["name"]}/match_attachment_type>`, - ]); - } - } - - if (triggerObj[triggerName].match_mime_type) { - const white = triggerObj[triggerName].match_mime_type.whitelist_enabled; - const black = triggerObj[triggerName].match_mime_type.blacklist_enabled; - - if (white && black) { - throw new StrictValidationError([ - `Cannot have both blacklist and whitelist enabled at rule <${rule["name"]}/match_mime_type>`, - ]); - } else if (!white && !black) { - throw new StrictValidationError([ - `Must have either blacklist or whitelist enabled at rule <${rule["name"]}/match_mime_type>`, - ]); - } - } - } - } - } - - if (rule["actions"]) { - for (const actionName in rule["actions"]) { - if (!availableActions[actionName]) { - throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule["name"]}'`]); - } - - const actionBlueprint = availableActions[actionName]; - const actionConfig = rule["actions"][actionName]; - - if (typeof actionConfig !== "object" || Array.isArray(actionConfig) || actionConfig == null) { - rule["actions"][actionName] = actionConfig; - } else { - rule["actions"][actionName] = configUtils.mergeConfig(actionBlueprint.defaultConfig, actionConfig); - } - } - } - - // Enable logging of automod actions by default - if (rule["actions"]) { - for (const actionName in rule["actions"]) { - if (!availableActions[actionName]) { - throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule["name"]}'`]); - } - } - - if (rule["actions"]["log"] == null) { - rule["actions"]["log"] = true; - } - if (rule["actions"]["clean"] && rule["actions"]["start_thread"]) { - throw new StrictValidationError([`Cannot have both clean and start_thread at rule '${rule["name"]}'`]); - } - } - } - } - - return parseIoTsSchema(ConfigSchema, input); -}; - export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({ name: "automod", showInDocs: true, @@ -201,7 +75,7 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({ ], defaultOptions, - configParser, + configParser: (input) => zAutomodConfig.parse(input), customOverrideCriteriaFunctions: { antiraid_level: (pluginData, matchParams, value) => { diff --git a/backend/src/plugins/Automod/actions/addRoles.ts b/backend/src/plugins/Automod/actions/addRoles.ts index 53b00005..89294728 100644 --- a/backend/src/plugins/Automod/actions/addRoles.ts +++ b/backend/src/plugins/Automod/actions/addRoles.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits, Snowflake } from "discord.js"; -import * as t from "io-ts"; -import { nonNullish, unique } from "../../../utils"; +import z from "zod"; +import { nonNullish, unique, zSnowflake } from "../../../utils"; import { canAssignRole } from "../../../utils/canAssignRole"; import { getMissingPermissions } from "../../../utils/getMissingPermissions"; import { missingPermissionError } from "../../../utils/missingPermissionError"; @@ -11,9 +11,10 @@ import { automodAction } from "../helpers"; const p = PermissionFlagsBits; +const configSchema = z.array(zSnowflake); + export const AddRolesAction = automodAction({ - configType: t.array(t.string), - defaultConfig: [], + configSchema, async apply({ pluginData, contexts, actionConfig, ruleName }) { const members = unique(contexts.map((c) => c.member).filter(nonNullish)); diff --git a/backend/src/plugins/Automod/actions/addToCounter.ts b/backend/src/plugins/Automod/actions/addToCounter.ts index c3a72dae..30ffe8fa 100644 --- a/backend/src/plugins/Automod/actions/addToCounter.ts +++ b/backend/src/plugins/Automod/actions/addToCounter.ts @@ -1,15 +1,16 @@ -import * as t from "io-ts"; +import z from "zod"; +import { zBoundedCharacters } from "../../../utils"; import { CountersPlugin } from "../../Counters/CountersPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { automodAction } from "../helpers"; -export const AddToCounterAction = automodAction({ - configType: t.type({ - counter: t.string, - amount: t.number, - }), +const configSchema = z.object({ + counter: zBoundedCharacters(0, 100), + amount: z.number(), +}); - defaultConfig: {}, +export const AddToCounterAction = automodAction({ + configSchema, async apply({ pluginData, contexts, actionConfig, ruleName }) { const countersPlugin = pluginData.getPlugin(CountersPlugin); diff --git a/backend/src/plugins/Automod/actions/alert.ts b/backend/src/plugins/Automod/actions/alert.ts index eb6f3348..1a767a0e 100644 --- a/backend/src/plugins/Automod/actions/alert.ts +++ b/backend/src/plugins/Automod/actions/alert.ts @@ -1,6 +1,6 @@ import { Snowflake } from "discord.js"; -import * as t from "io-ts"; import { erisAllowedMentionsToDjsMentionOptions } from "src/utils/erisAllowedMentionsToDjsMentionOptions"; +import z from "zod"; import { LogType } from "../../../data/LogType"; import { createTypedTemplateSafeValueContainer, @@ -12,10 +12,12 @@ import { chunkMessageLines, isTruthy, messageLink, - tAllowedMentions, - tNormalizedNullOptional, validateAndParseMessageContent, verboseChannelMention, + zAllowedMentions, + zBoundedCharacters, + zNullishToUndefined, + zSnowflake } from "../../../utils"; import { messageIsEmpty } from "../../../utils/messageIsEmpty"; import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; @@ -23,14 +25,14 @@ import { InternalPosterPlugin } from "../../InternalPoster/InternalPosterPlugin" import { LogsPlugin } from "../../Logs/LogsPlugin"; import { automodAction } from "../helpers"; -export const AlertAction = automodAction({ - configType: t.type({ - channel: t.string, - text: t.string, - allowed_mentions: tNormalizedNullOptional(tAllowedMentions), - }), +const configSchema = z.object({ + channel: zSnowflake, + text: zBoundedCharacters(1, 4000), + allowed_mentions: zNullishToUndefined(zAllowedMentions.nullable().default(null)), +}); - defaultConfig: {}, +export const AlertAction = automodAction({ + configSchema, async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) { const channel = pluginData.guild.channels.cache.get(actionConfig.channel as Snowflake); diff --git a/backend/src/plugins/Automod/actions/archiveThread.ts b/backend/src/plugins/Automod/actions/archiveThread.ts index d94afdf7..e73cb3dd 100644 --- a/backend/src/plugins/Automod/actions/archiveThread.ts +++ b/backend/src/plugins/Automod/actions/archiveThread.ts @@ -1,11 +1,12 @@ import { AnyThreadChannel } from "discord.js"; -import * as t from "io-ts"; +import z from "zod"; import { noop } from "../../../utils"; import { automodAction } from "../helpers"; +const configSchema = z.strictObject({}); + export const ArchiveThreadAction = automodAction({ - configType: t.type({}), - defaultConfig: {}, + configSchema, async apply({ pluginData, contexts }) { const threads = contexts diff --git a/backend/src/plugins/Automod/actions/availableActions.ts b/backend/src/plugins/Automod/actions/availableActions.ts index 3f253bc0..dd148ca7 100644 --- a/backend/src/plugins/Automod/actions/availableActions.ts +++ b/backend/src/plugins/Automod/actions/availableActions.ts @@ -1,4 +1,3 @@ -import * as t from "io-ts"; import { AutomodActionBlueprint } from "../helpers"; import { AddRolesAction } from "./addRoles"; import { AddToCounterAction } from "./addToCounter"; @@ -19,7 +18,7 @@ import { SetSlowmodeAction } from "./setSlowmode"; import { StartThreadAction } from "./startThread"; import { WarnAction } from "./warn"; -export const availableActions: Record<string, AutomodActionBlueprint<any>> = { +export const availableActions = { clean: CleanAction, warn: WarnAction, mute: MuteAction, @@ -38,25 +37,4 @@ export const availableActions: Record<string, AutomodActionBlueprint<any>> = { start_thread: StartThreadAction, archive_thread: ArchiveThreadAction, change_perms: ChangePermsAction, -}; - -export const AvailableActions = t.type({ - clean: CleanAction.configType, - warn: WarnAction.configType, - mute: MuteAction.configType, - kick: KickAction.configType, - ban: BanAction.configType, - alert: AlertAction.configType, - change_nickname: ChangeNicknameAction.configType, - log: LogAction.configType, - add_roles: AddRolesAction.configType, - remove_roles: RemoveRolesAction.configType, - set_antiraid_level: SetAntiraidLevelAction.configType, - reply: ReplyAction.configType, - add_to_counter: AddToCounterAction.configType, - set_counter: SetCounterAction.configType, - set_slowmode: SetSlowmodeAction.configType, - start_thread: StartThreadAction.configType, - archive_thread: ArchiveThreadAction.configType, - change_perms: ChangePermsAction.configType, -}); +} satisfies Record<string, AutomodActionBlueprint<any>>; diff --git a/backend/src/plugins/Automod/actions/ban.ts b/backend/src/plugins/Automod/actions/ban.ts index 9cd0fd86..dcc9563f 100644 --- a/backend/src/plugins/Automod/actions/ban.ts +++ b/backend/src/plugins/Automod/actions/ban.ts @@ -1,25 +1,23 @@ -import * as t from "io-ts"; -import { convertDelayStringToMS, nonNullish, tDelayString, tNullable, unique } from "../../../utils"; +import z from "zod"; +import { convertDelayStringToMS, nonNullish, unique, zBoundedCharacters, zDelayString, zSnowflake } from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; +import { zNotify } from "../types"; + +const configSchema = z.strictObject({ + reason: zBoundedCharacters(0, 4000).nullable().default(null), + duration: zDelayString.nullable().default(null), + notify: zNotify.nullable().default(null), + notifyChannel: zSnowflake.nullable().default(null), + deleteMessageDays: z.number().nullable().default(null), + postInCaseLog: z.boolean().nullable().default(null), + hide_case: z.boolean().nullable().default(false), +}); export const BanAction = automodAction({ - configType: t.type({ - reason: tNullable(t.string), - duration: tNullable(tDelayString), - notify: tNullable(t.string), - notifyChannel: tNullable(t.string), - deleteMessageDays: tNullable(t.number), - postInCaseLog: tNullable(t.boolean), - hide_case: tNullable(t.boolean), - }), - - defaultConfig: { - notify: null, // Use defaults from ModActions - hide_case: false, - }, + configSchema, async apply({ pluginData, contexts, actionConfig, matchResult }) { const reason = actionConfig.reason || "Kicked automatically"; diff --git a/backend/src/plugins/Automod/actions/changeNickname.ts b/backend/src/plugins/Automod/actions/changeNickname.ts index d63a3b60..ce7c610e 100644 --- a/backend/src/plugins/Automod/actions/changeNickname.ts +++ b/backend/src/plugins/Automod/actions/changeNickname.ts @@ -1,18 +1,16 @@ -import * as t from "io-ts"; -import { nonNullish, unique } from "../../../utils"; +import z from "zod"; +import { nonNullish, unique, zBoundedCharacters } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { automodAction } from "../helpers"; export const ChangeNicknameAction = automodAction({ - configType: t.union([ - t.string, - t.type({ - name: t.string, + configSchema: z.union([ + zBoundedCharacters(0, 32), + z.strictObject({ + name: zBoundedCharacters(0, 32), }), ]), - defaultConfig: {}, - async apply({ pluginData, contexts, actionConfig }) { const members = unique(contexts.map((c) => c.member).filter(nonNullish)); diff --git a/backend/src/plugins/Automod/actions/changePerms.ts b/backend/src/plugins/Automod/actions/changePerms.ts index 1eaa6dd5..b5bc9686 100644 --- a/backend/src/plugins/Automod/actions/changePerms.ts +++ b/backend/src/plugins/Automod/actions/changePerms.ts @@ -1,7 +1,8 @@ import { PermissionsBitField, PermissionsString } from "discord.js"; -import * as t from "io-ts"; +import { U } from "ts-toolbelt"; +import z from "zod"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; -import { isValidSnowflake, noop, tNullable, tPartialDictionary } from "../../../utils"; +import { isValidSnowflake, keys, noop, zSnowflake } from "../../../utils"; import { guildToTemplateSafeGuild, savedMessageToTemplateSafeSavedMessage, @@ -59,16 +60,19 @@ const realToLegacyMap = Object.entries(legacyPermMap).reduce((map, pair) => { return map; }, {}) as Record<keyof typeof PermissionsBitField.Flags, keyof typeof legacyPermMap>; +const permissionNames = keys(PermissionsBitField.Flags) as U.ListOf<keyof typeof PermissionsBitField.Flags>; +const legacyPermissionNames = keys(legacyPermMap) as U.ListOf<keyof typeof legacyPermMap>; +const allPermissionNames = [...permissionNames, ...legacyPermissionNames] as const; + export const ChangePermsAction = automodAction({ - configType: t.type({ - target: t.string, - channel: tNullable(t.string), - perms: tPartialDictionary( - t.union([t.keyof(PermissionsBitField.Flags), t.keyof(legacyPermMap)]), - tNullable(t.boolean), + configSchema: z.strictObject({ + target: zSnowflake, + channel: zSnowflake.nullable().default(null), + perms: z.record( + z.enum(allPermissionNames), + z.boolean().nullable(), ), }), - defaultConfig: {}, async apply({ pluginData, contexts, actionConfig }) { const user = contexts.find((c) => c.user)?.user; diff --git a/backend/src/plugins/Automod/actions/clean.ts b/backend/src/plugins/Automod/actions/clean.ts index 91bf37ac..5c66d370 100644 --- a/backend/src/plugins/Automod/actions/clean.ts +++ b/backend/src/plugins/Automod/actions/clean.ts @@ -1,12 +1,11 @@ import { GuildTextBasedChannel, Snowflake } from "discord.js"; -import * as t from "io-ts"; +import z from "zod"; import { LogType } from "../../../data/LogType"; import { noop } from "../../../utils"; import { automodAction } from "../helpers"; export const CleanAction = automodAction({ - configType: t.boolean, - defaultConfig: false, + configSchema: z.boolean().default(false), async apply({ pluginData, contexts, ruleName }) { const messageIdsToDeleteByChannelId: Map<string, string[]> = new Map(); diff --git a/backend/src/plugins/Automod/actions/exampleAction.ts b/backend/src/plugins/Automod/actions/exampleAction.ts index 05bea0de..a43fc676 100644 --- a/backend/src/plugins/Automod/actions/exampleAction.ts +++ b/backend/src/plugins/Automod/actions/exampleAction.ts @@ -1,13 +1,12 @@ -import * as t from "io-ts"; +import z from "zod"; +import { zBoundedCharacters } from "../../../utils"; import { automodAction } from "../helpers"; export const ExampleAction = automodAction({ - configType: t.type({ - someValue: t.string, + configSchema: z.strictObject({ + someValue: zBoundedCharacters(0, 1000), }), - defaultConfig: {}, - // eslint-disable-next-line @typescript-eslint/no-unused-vars async apply({ pluginData, contexts, actionConfig }) { // TODO: Everything diff --git a/backend/src/plugins/Automod/actions/kick.ts b/backend/src/plugins/Automod/actions/kick.ts index 9e6a792d..5afba467 100644 --- a/backend/src/plugins/Automod/actions/kick.ts +++ b/backend/src/plugins/Automod/actions/kick.ts @@ -1,24 +1,20 @@ -import * as t from "io-ts"; -import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils"; +import z from "zod"; +import { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; +import { zNotify } from "../types"; export const KickAction = automodAction({ - configType: t.type({ - reason: tNullable(t.string), - notify: tNullable(t.string), - notifyChannel: tNullable(t.string), - postInCaseLog: tNullable(t.boolean), - hide_case: tNullable(t.boolean), + configSchema: z.strictObject({ + reason: zBoundedCharacters(0, 4000).nullable().default(null), + notify: zNotify.nullable().default(null), + notifyChannel: zSnowflake.nullable().default(null), + postInCaseLog: z.boolean().nullable().default(null), + hide_case: z.boolean().nullable().default(false), }), - defaultConfig: { - notify: null, // Use defaults from ModActions - hide_case: false, - }, - async apply({ pluginData, contexts, actionConfig, matchResult }) { const reason = actionConfig.reason || "Kicked automatically"; const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined; diff --git a/backend/src/plugins/Automod/actions/log.ts b/backend/src/plugins/Automod/actions/log.ts index 82075a2e..dace25f1 100644 --- a/backend/src/plugins/Automod/actions/log.ts +++ b/backend/src/plugins/Automod/actions/log.ts @@ -1,11 +1,10 @@ -import * as t from "io-ts"; +import z from "zod"; import { isTruthy, unique } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { automodAction } from "../helpers"; export const LogAction = automodAction({ - configType: t.boolean, - defaultConfig: true, + configSchema: z.boolean().default(true), async apply({ pluginData, contexts, ruleName, matchResult }) { const users = unique(contexts.map((c) => c.user)).filter(isTruthy); diff --git a/backend/src/plugins/Automod/actions/mute.ts b/backend/src/plugins/Automod/actions/mute.ts index 3219c712..4a2a42ad 100644 --- a/backend/src/plugins/Automod/actions/mute.ts +++ b/backend/src/plugins/Automod/actions/mute.ts @@ -1,29 +1,25 @@ -import * as t from "io-ts"; +import z from "zod"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; -import { convertDelayStringToMS, nonNullish, tDelayString, tNullable, unique } from "../../../utils"; +import { convertDelayStringToMS, nonNullish, unique, zBoundedCharacters, zDelayString, zSnowflake } from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { MutesPlugin } from "../../Mutes/MutesPlugin"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; +import { zNotify } from "../types"; export const MuteAction = automodAction({ - configType: t.type({ - reason: tNullable(t.string), - duration: tNullable(tDelayString), - notify: tNullable(t.string), - notifyChannel: tNullable(t.string), - remove_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])), - restore_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])), - postInCaseLog: tNullable(t.boolean), - hide_case: tNullable(t.boolean), + configSchema: z.strictObject({ + reason: zBoundedCharacters(0, 4000).nullable().default(null), + duration: zDelayString.nullable().default(null), + notify: zNotify.nullable().default(null), + notifyChannel: zSnowflake.nullable().default(null), + remove_roles_on_mute: z.union([z.boolean(), z.array(zSnowflake)]).nullable().default(null), + restore_roles_on_mute: z.union([z.boolean(), z.array(zSnowflake)]).nullable().default(null), + postInCaseLog: z.boolean().nullable().default(null), + hide_case: z.boolean().nullable().default(false), }), - defaultConfig: { - notify: null, // Use defaults from ModActions - hide_case: false, - }, - async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) { const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined; const reason = actionConfig.reason || "Muted automatically"; diff --git a/backend/src/plugins/Automod/actions/removeRoles.ts b/backend/src/plugins/Automod/actions/removeRoles.ts index a46d8262..8b065702 100644 --- a/backend/src/plugins/Automod/actions/removeRoles.ts +++ b/backend/src/plugins/Automod/actions/removeRoles.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits, Snowflake } from "discord.js"; -import * as t from "io-ts"; -import { nonNullish, unique } from "../../../utils"; +import z from "zod"; +import { nonNullish, unique, zSnowflake } from "../../../utils"; import { canAssignRole } from "../../../utils/canAssignRole"; import { getMissingPermissions } from "../../../utils/getMissingPermissions"; import { memberRolesLock } from "../../../utils/lockNameHelpers"; @@ -12,9 +12,7 @@ import { automodAction } from "../helpers"; const p = PermissionFlagsBits; export const RemoveRolesAction = automodAction({ - configType: t.array(t.string), - - defaultConfig: [], + configSchema: z.array(zSnowflake).default([]), async apply({ pluginData, contexts, actionConfig, ruleName }) { const members = unique(contexts.map((c) => c.member).filter(nonNullish)); diff --git a/backend/src/plugins/Automod/actions/reply.ts b/backend/src/plugins/Automod/actions/reply.ts index 6239cee8..02ff9545 100644 --- a/backend/src/plugins/Automod/actions/reply.ts +++ b/backend/src/plugins/Automod/actions/reply.ts @@ -1,16 +1,16 @@ import { GuildTextBasedChannel, MessageCreateOptions, PermissionsBitField, Snowflake, User } from "discord.js"; -import * as t from "io-ts"; +import z from "zod"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { convertDelayStringToMS, noop, renderRecursively, - tDelayString, - tMessageContent, - tNullable, unique, validateAndParseMessageContent, verboseChannelMention, + zBoundedCharacters, + zDelayString, + zMessageContent } from "../../../utils"; import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions"; import { messageIsEmpty } from "../../../utils/messageIsEmpty"; @@ -20,17 +20,15 @@ import { automodAction } from "../helpers"; import { AutomodContext } from "../types"; export const ReplyAction = automodAction({ - configType: t.union([ - t.string, - t.type({ - text: tMessageContent, - auto_delete: tNullable(t.union([tDelayString, t.number])), - inline: tNullable(t.boolean), + configSchema: z.union([ + zBoundedCharacters(0, 4000), + z.strictObject({ + text: zMessageContent, + auto_delete: z.union([zDelayString, z.number()]).nullable().default(null), + inline: z.boolean().default(false), }), ]), - defaultConfig: {}, - async apply({ pluginData, contexts, actionConfig, ruleName }) { const contextsWithTextChannels = contexts .filter((c) => c.message?.channel_id) diff --git a/backend/src/plugins/Automod/actions/setAntiraidLevel.ts b/backend/src/plugins/Automod/actions/setAntiraidLevel.ts index ecddabd9..db2a6bef 100644 --- a/backend/src/plugins/Automod/actions/setAntiraidLevel.ts +++ b/backend/src/plugins/Automod/actions/setAntiraidLevel.ts @@ -1,11 +1,9 @@ -import * as t from "io-ts"; -import { tNullable } from "../../../utils"; +import { zBoundedCharacters } from "../../../utils"; import { setAntiraidLevel } from "../functions/setAntiraidLevel"; import { automodAction } from "../helpers"; export const SetAntiraidLevelAction = automodAction({ - configType: tNullable(t.string), - defaultConfig: "", + configSchema: zBoundedCharacters(0, 100).nullable(), async apply({ pluginData, actionConfig }) { setAntiraidLevel(pluginData, actionConfig ?? null); diff --git a/backend/src/plugins/Automod/actions/setCounter.ts b/backend/src/plugins/Automod/actions/setCounter.ts index 3286fb4b..dea4bdd6 100644 --- a/backend/src/plugins/Automod/actions/setCounter.ts +++ b/backend/src/plugins/Automod/actions/setCounter.ts @@ -1,16 +1,15 @@ -import * as t from "io-ts"; +import z from "zod"; +import { zBoundedCharacters } from "../../../utils"; import { CountersPlugin } from "../../Counters/CountersPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { automodAction } from "../helpers"; export const SetCounterAction = automodAction({ - configType: t.type({ - counter: t.string, - value: t.number, + configSchema: z.strictObject({ + counter: zBoundedCharacters(0, 100), + value: z.number(), }), - defaultConfig: {}, - async apply({ pluginData, contexts, actionConfig, ruleName }) { const countersPlugin = pluginData.getPlugin(CountersPlugin); if (!countersPlugin.counterExists(actionConfig.counter)) { diff --git a/backend/src/plugins/Automod/actions/setSlowmode.ts b/backend/src/plugins/Automod/actions/setSlowmode.ts index 7b527f10..ecac0f21 100644 --- a/backend/src/plugins/Automod/actions/setSlowmode.ts +++ b/backend/src/plugins/Automod/actions/setSlowmode.ts @@ -1,19 +1,15 @@ import { ChannelType, GuildTextBasedChannel, Snowflake } from "discord.js"; -import * as t from "io-ts"; -import { convertDelayStringToMS, isDiscordAPIError, tDelayString, tNullable } from "../../../utils"; +import z from "zod"; +import { convertDelayStringToMS, isDiscordAPIError, zDelayString, zSnowflake } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { automodAction } from "../helpers"; export const SetSlowmodeAction = automodAction({ - configType: t.type({ - channels: t.array(t.string), - duration: tNullable(tDelayString), + configSchema: z.strictObject({ + channels: z.array(zSnowflake), + duration: zDelayString.nullable().default("10s"), }), - defaultConfig: { - duration: "10s", - }, - async apply({ pluginData, actionConfig }) { const slowmodeMs = Math.max(actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : 0, 0); diff --git a/backend/src/plugins/Automod/actions/startThread.ts b/backend/src/plugins/Automod/actions/startThread.ts index 25b840e1..e0bc11a4 100644 --- a/backend/src/plugins/Automod/actions/startThread.ts +++ b/backend/src/plugins/Automod/actions/startThread.ts @@ -5,9 +5,9 @@ import { ThreadAutoArchiveDuration, ThreadChannel, } from "discord.js"; -import * as t from "io-ts"; +import z from "zod"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; -import { MINUTES, convertDelayStringToMS, noop, tDelayString, tNullable } from "../../../utils"; +import { MINUTES, convertDelayStringToMS, noop, zBoundedCharacters, zDelayString } from "../../../utils"; import { savedMessageToTemplateSafeSavedMessage, userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { automodAction } from "../helpers"; @@ -19,18 +19,14 @@ const validThreadAutoArchiveDurations: ThreadAutoArchiveDuration[] = [ ]; export const StartThreadAction = automodAction({ - configType: t.type({ - name: tNullable(t.string), - auto_archive: tDelayString, - private: tNullable(t.boolean), - slowmode: tNullable(tDelayString), - limit_per_channel: tNullable(t.number), + configSchema: z.strictObject({ + name: zBoundedCharacters(1, 100).nullable(), + auto_archive: zDelayString, + private: z.boolean().default(false), + slowmode: zDelayString.nullable().default(null), + limit_per_channel: z.number().nullable().default(5), }), - defaultConfig: { - limit_per_channel: 5, - }, - async apply({ pluginData, contexts, actionConfig }) { // check if the message still exists, we don't want to create threads for deleted messages const threads = contexts.filter((c) => { diff --git a/backend/src/plugins/Automod/actions/warn.ts b/backend/src/plugins/Automod/actions/warn.ts index 59135cb2..e8c63340 100644 --- a/backend/src/plugins/Automod/actions/warn.ts +++ b/backend/src/plugins/Automod/actions/warn.ts @@ -1,24 +1,20 @@ -import * as t from "io-ts"; -import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils"; +import z from "zod"; +import { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; +import { zNotify } from "../types"; export const WarnAction = automodAction({ - configType: t.type({ - reason: tNullable(t.string), - notify: tNullable(t.string), - notifyChannel: tNullable(t.string), - postInCaseLog: tNullable(t.boolean), - hide_case: tNullable(t.boolean), + configSchema: z.strictObject({ + reason: zBoundedCharacters(0, 4000).nullable().default(null), + notify: zNotify.nullable().default(null), + notifyChannel: zSnowflake.nullable().default(null), + postInCaseLog: z.boolean().nullable().default(null), + hide_case: z.boolean().nullable().default(false), }), - defaultConfig: { - notify: null, // Use defaults from ModActions - hide_case: false, - }, - async apply({ pluginData, contexts, actionConfig, matchResult }) { const reason = actionConfig.reason || "Warned automatically"; const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined; diff --git a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts index a1858d37..19364eee 100644 --- a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts +++ b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts @@ -1,28 +1,27 @@ -import * as t from "io-ts"; import { SavedMessage } from "../../../data/entities/SavedMessage"; import { humanizeDurationShort } from "../../../humanizeDurationShort"; import { getBaseUrl } from "../../../pluginUtils"; -import { convertDelayStringToMS, sorter, tDelayString, tNullable } from "../../../utils"; +import { convertDelayStringToMS, sorter, zDelayString } from "../../../utils"; import { RecentActionType } from "../constants"; import { automodTrigger } from "../helpers"; import { findRecentSpam } from "./findRecentSpam"; import { getMatchingMessageRecentActions } from "./getMatchingMessageRecentActions"; import { getMessageSpamIdentifier } from "./getSpamIdentifier"; - -const MessageSpamTriggerConfig = t.type({ - amount: t.number, - within: tDelayString, - per_channel: tNullable(t.boolean), -}); +import z from "zod"; interface TMessageSpamMatchResultType { archiveId: string; } +const configSchema = z.strictObject({ + amount: z.number().int(), + within: zDelayString, + per_channel: z.boolean().optional(), +}); + export function createMessageSpamTrigger(spamType: RecentActionType, prettyName: string) { return automodTrigger<TMessageSpamMatchResultType>()({ - configType: MessageSpamTriggerConfig, - defaultConfig: {}, + configSchema, async match({ pluginData, context, triggerConfig }) { if (!context.message) { diff --git a/backend/src/plugins/Automod/helpers.ts b/backend/src/plugins/Automod/helpers.ts index 5c5f55c7..d1c04d61 100644 --- a/backend/src/plugins/Automod/helpers.ts +++ b/backend/src/plugins/Automod/helpers.ts @@ -1,7 +1,7 @@ -import * as t from "io-ts"; import { GuildPluginData } from "knub"; import { Awaitable } from "../../utils/typeUtils"; import { AutomodContext, AutomodPluginType } from "./types"; +import z, { ZodTypeAny } from "zod"; interface BaseAutomodTriggerMatchResult { extraContexts?: AutomodContext[]; @@ -31,21 +31,19 @@ type AutomodTriggerRenderMatchInformationFn<TConfigType, TMatchResultExtra> = (m matchResult: AutomodTriggerMatchResult<TMatchResultExtra>; }) => Awaitable<string>; -export interface AutomodTriggerBlueprint<TConfigType extends t.Any, TMatchResultExtra> { - configType: TConfigType; - defaultConfig: Partial<t.TypeOf<TConfigType>>; - - match: AutomodTriggerMatchFn<t.TypeOf<TConfigType>, TMatchResultExtra>; - renderMatchInformation: AutomodTriggerRenderMatchInformationFn<t.TypeOf<TConfigType>, TMatchResultExtra>; +export interface AutomodTriggerBlueprint<TConfigSchema extends ZodTypeAny, TMatchResultExtra> { + configSchema: TConfigSchema; + match: AutomodTriggerMatchFn<z.output<TConfigSchema>, TMatchResultExtra>; + renderMatchInformation: AutomodTriggerRenderMatchInformationFn<z.output<TConfigSchema>, TMatchResultExtra>; } -export function automodTrigger<TMatchResultExtra>(): <TConfigType extends t.Any>( - blueprint: AutomodTriggerBlueprint<TConfigType, TMatchResultExtra>, -) => AutomodTriggerBlueprint<TConfigType, TMatchResultExtra>; +export function automodTrigger<TMatchResultExtra>(): <TConfigSchema extends ZodTypeAny>( + blueprint: AutomodTriggerBlueprint<TConfigSchema, TMatchResultExtra>, +) => AutomodTriggerBlueprint<TConfigSchema, TMatchResultExtra>; -export function automodTrigger<TConfigType extends t.Any>( - blueprint: AutomodTriggerBlueprint<TConfigType, unknown>, -): AutomodTriggerBlueprint<TConfigType, unknown>; +export function automodTrigger<TConfigSchema extends ZodTypeAny>( + blueprint: AutomodTriggerBlueprint<TConfigSchema, unknown>, +): AutomodTriggerBlueprint<TConfigSchema, unknown>; export function automodTrigger(...args) { if (args.length) { @@ -63,15 +61,13 @@ type AutomodActionApplyFn<TConfigType> = (meta: { matchResult: AutomodTriggerMatchResult; }) => Awaitable<void>; -export interface AutomodActionBlueprint<TConfigType extends t.Any> { - configType: TConfigType; - defaultConfig: Partial<t.TypeOf<TConfigType>>; - - apply: AutomodActionApplyFn<t.TypeOf<TConfigType>>; +export interface AutomodActionBlueprint<TConfigSchema extends ZodTypeAny> { + configSchema: TConfigSchema; + apply: AutomodActionApplyFn<z.output<TConfigSchema>>; } -export function automodAction<TConfigType extends t.Any>( - blueprint: AutomodActionBlueprint<TConfigType>, -): AutomodActionBlueprint<TConfigType> { +export function automodAction<TConfigSchema extends ZodTypeAny>( + blueprint: AutomodActionBlueprint<TConfigSchema>, +): AutomodActionBlueprint<TConfigSchema> { return blueprint; } diff --git a/backend/src/plugins/Automod/info.ts b/backend/src/plugins/Automod/info.ts index e0102459..a2071e21 100644 --- a/backend/src/plugins/Automod/info.ts +++ b/backend/src/plugins/Automod/info.ts @@ -1,6 +1,6 @@ import { trimPluginDescription } from "../../utils"; import { ZeppelinGuildPluginBlueprint } from "../ZeppelinPluginBlueprint"; -import { ConfigSchema } from "./types"; +import { zAutomodConfig } from "./types"; export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = { prettyName: "Automod", @@ -100,5 +100,5 @@ export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = { {matchSummary} ~~~ `), - configSchema: ConfigSchema, + configSchema: zAutomodConfig, }; diff --git a/backend/src/plugins/Automod/triggers/antiraidLevel.ts b/backend/src/plugins/Automod/triggers/antiraidLevel.ts index 7c6d17af..c879455f 100644 --- a/backend/src/plugins/Automod/triggers/antiraidLevel.ts +++ b/backend/src/plugins/Automod/triggers/antiraidLevel.ts @@ -1,15 +1,14 @@ -import * as t from "io-ts"; -import { tNullable } from "../../../utils"; import { automodTrigger } from "../helpers"; +import z from "zod"; interface AntiraidLevelTriggerResult {} -export const AntiraidLevelTrigger = automodTrigger<AntiraidLevelTriggerResult>()({ - configType: t.type({ - level: tNullable(t.string), - }), +const configSchema = z.strictObject({ + level: z.nullable(z.string().max(100)), +}); - defaultConfig: {}, +export const AntiraidLevelTrigger = automodTrigger<AntiraidLevelTriggerResult>()({ + configSchema, async match({ triggerConfig, context }) { if (!context.antiraid) { diff --git a/backend/src/plugins/Automod/triggers/anyMessage.ts b/backend/src/plugins/Automod/triggers/anyMessage.ts index 5b611334..93ce0abc 100644 --- a/backend/src/plugins/Automod/triggers/anyMessage.ts +++ b/backend/src/plugins/Automod/triggers/anyMessage.ts @@ -1,14 +1,14 @@ import { Snowflake } from "discord.js"; -import * as t from "io-ts"; import { verboseChannelMention } from "../../../utils"; import { automodTrigger } from "../helpers"; +import z from "zod"; interface AnyMessageResultType {} -export const AnyMessageTrigger = automodTrigger<AnyMessageResultType>()({ - configType: t.type({}), +const configSchema = z.strictObject({}); - defaultConfig: {}, +export const AnyMessageTrigger = automodTrigger<AnyMessageResultType>()({ + configSchema, async match({ context }) { if (!context.message) { diff --git a/backend/src/plugins/Automod/triggers/availableTriggers.ts b/backend/src/plugins/Automod/triggers/availableTriggers.ts index 822e44ff..a2ac60fb 100644 --- a/backend/src/plugins/Automod/triggers/availableTriggers.ts +++ b/backend/src/plugins/Automod/triggers/availableTriggers.ts @@ -1,4 +1,3 @@ -import * as t from "io-ts"; import { AutomodTriggerBlueprint } from "../helpers"; import { AntiraidLevelTrigger } from "./antiraidLevel"; import { AnyMessageTrigger } from "./anyMessage"; @@ -45,6 +44,7 @@ export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any> match_attachment_type: MatchAttachmentTypeTrigger, match_mime_type: MatchMimeTypeTrigger, member_join: MemberJoinTrigger, + member_leave: MemberLeaveTrigger, role_added: RoleAddedTrigger, role_removed: RoleRemovedTrigger, @@ -76,46 +76,3 @@ export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any> thread_archive: ThreadArchiveTrigger, thread_unarchive: ThreadUnarchiveTrigger, }; - -export const AvailableTriggers = t.type({ - any_message: AnyMessageTrigger.configType, - - match_words: MatchWordsTrigger.configType, - match_regex: MatchRegexTrigger.configType, - match_invites: MatchInvitesTrigger.configType, - match_links: MatchLinksTrigger.configType, - match_attachment_type: MatchAttachmentTypeTrigger.configType, - match_mime_type: MatchMimeTypeTrigger.configType, - member_join: MemberJoinTrigger.configType, - member_leave: MemberLeaveTrigger.configType, - role_added: RoleAddedTrigger.configType, - role_removed: RoleRemovedTrigger.configType, - - message_spam: MessageSpamTrigger.configType, - mention_spam: MentionSpamTrigger.configType, - link_spam: LinkSpamTrigger.configType, - attachment_spam: AttachmentSpamTrigger.configType, - emoji_spam: EmojiSpamTrigger.configType, - line_spam: LineSpamTrigger.configType, - character_spam: CharacterSpamTrigger.configType, - member_join_spam: MemberJoinSpamTrigger.configType, - sticker_spam: StickerSpamTrigger.configType, - thread_create_spam: ThreadCreateSpamTrigger.configType, - - counter_trigger: CounterTrigger.configType, - - note: NoteTrigger.configType, - warn: WarnTrigger.configType, - mute: MuteTrigger.configType, - unmute: UnmuteTrigger.configType, - kick: KickTrigger.configType, - ban: BanTrigger.configType, - unban: UnbanTrigger.configType, - - antiraid_level: AntiraidLevelTrigger.configType, - - thread_create: ThreadCreateTrigger.configType, - thread_delete: ThreadDeleteTrigger.configType, - thread_archive: ThreadArchiveTrigger.configType, - thread_unarchive: ThreadUnarchiveTrigger.configType, -}); diff --git a/backend/src/plugins/Automod/triggers/ban.ts b/backend/src/plugins/Automod/triggers/ban.ts index f8a4730b..f6df0fd6 100644 --- a/backend/src/plugins/Automod/triggers/ban.ts +++ b/backend/src/plugins/Automod/triggers/ban.ts @@ -1,19 +1,16 @@ -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; // tslint:disable-next-line:no-empty-interface interface BanTriggerResultType {} -export const BanTrigger = automodTrigger<BanTriggerResultType>()({ - configType: t.type({ - manual: t.boolean, - automatic: t.boolean, - }), +const configSchema = z.strictObject({ + manual: z.boolean().default(true), + automatic: z.boolean().default(true), +}); - defaultConfig: { - manual: true, - automatic: true, - }, +export const BanTrigger = automodTrigger<BanTriggerResultType>()({ + configSchema, async match({ context, triggerConfig }) { if (context.modAction?.type !== "ban") { diff --git a/backend/src/plugins/Automod/triggers/counterTrigger.ts b/backend/src/plugins/Automod/triggers/counterTrigger.ts index 58f21402..22de3df1 100644 --- a/backend/src/plugins/Automod/triggers/counterTrigger.ts +++ b/backend/src/plugins/Automod/triggers/counterTrigger.ts @@ -1,18 +1,17 @@ -import * as t from "io-ts"; -import { tNullable } from "../../../utils"; +import z from "zod"; import { automodTrigger } from "../helpers"; // tslint:disable-next-line interface CounterTriggerResult {} -export const CounterTrigger = automodTrigger<CounterTriggerResult>()({ - configType: t.type({ - counter: t.string, - trigger: t.string, - reverse: tNullable(t.boolean), - }), +const configSchema = z.strictObject({ + counter: z.string().max(100), + trigger: z.string().max(100), + reverse: z.boolean().optional(), +}); - defaultConfig: {}, +export const CounterTrigger = automodTrigger<CounterTriggerResult>()({ + configSchema, async match({ triggerConfig, context }) { if (!context.counterTrigger) { diff --git a/backend/src/plugins/Automod/triggers/exampleTrigger.ts b/backend/src/plugins/Automod/triggers/exampleTrigger.ts index bf0880a9..e6005e07 100644 --- a/backend/src/plugins/Automod/triggers/exampleTrigger.ts +++ b/backend/src/plugins/Automod/triggers/exampleTrigger.ts @@ -1,18 +1,16 @@ -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; interface ExampleMatchResultType { isBanana: boolean; } -export const ExampleTrigger = automodTrigger<ExampleMatchResultType>()({ - configType: t.type({ - allowedFruits: t.array(t.string), - }), +const configSchema = z.strictObject({ + allowedFruits: z.array(z.string().max(100)).max(50).default(["peach", "banana"]), +}); - defaultConfig: { - allowedFruits: ["peach", "banana"], - }, +export const ExampleTrigger = automodTrigger<ExampleMatchResultType>()({ + configSchema, async match({ triggerConfig, context }) { const foundFruit = triggerConfig.allowedFruits.find((fruit) => context.message?.data.content === fruit); diff --git a/backend/src/plugins/Automod/triggers/kick.ts b/backend/src/plugins/Automod/triggers/kick.ts index 163d6bf3..add6803e 100644 --- a/backend/src/plugins/Automod/triggers/kick.ts +++ b/backend/src/plugins/Automod/triggers/kick.ts @@ -1,19 +1,16 @@ -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; // tslint:disable-next-line:no-empty-interface interface KickTriggerResultType {} -export const KickTrigger = automodTrigger<KickTriggerResultType>()({ - configType: t.type({ - manual: t.boolean, - automatic: t.boolean, - }), +const configSchema = z.strictObject({ + manual: z.boolean().default(true), + automatic: z.boolean().default(true), +}); - defaultConfig: { - manual: true, - automatic: true, - }, +export const KickTrigger = automodTrigger<KickTriggerResultType>()({ + configSchema, async match({ context, triggerConfig }) { if (context.modAction?.type !== "kick") { diff --git a/backend/src/plugins/Automod/triggers/matchAttachmentType.ts b/backend/src/plugins/Automod/triggers/matchAttachmentType.ts index 659be65b..45f7ede7 100644 --- a/backend/src/plugins/Automod/triggers/matchAttachmentType.ts +++ b/backend/src/plugins/Automod/triggers/matchAttachmentType.ts @@ -1,5 +1,5 @@ import { escapeInlineCode, Snowflake } from "discord.js"; -import * as t from "io-ts"; +import z from "zod"; import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils"; import { automodTrigger } from "../helpers"; @@ -8,20 +8,31 @@ interface MatchResultType { mode: "blacklist" | "whitelist"; } -export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({ - configType: t.type({ - filetype_blacklist: t.array(t.string), - blacklist_enabled: t.boolean, - filetype_whitelist: t.array(t.string), - whitelist_enabled: t.boolean, - }), +const configSchema = z.strictObject({ + filetype_blacklist: z.array(z.string().max(32)).max(255).default([]), + blacklist_enabled: z.boolean().default(false), + filetype_whitelist: z.array(z.string().max(32)).max(255).default([]), + whitelist_enabled: z.boolean().default(false), +}).transform((parsed, ctx) => { + if (parsed.blacklist_enabled && parsed.whitelist_enabled) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cannot have both blacklist and whitelist enabled", + }); + return z.NEVER; + } + if (! parsed.blacklist_enabled && ! parsed.whitelist_enabled) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Must have either blacklist or whitelist enabled", + }); + return z.NEVER; + } + return parsed; +}); - defaultConfig: { - filetype_blacklist: [], - blacklist_enabled: false, - filetype_whitelist: [], - whitelist_enabled: false, - }, +export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({ + configSchema, async match({ context, triggerConfig: trigger }) { if (!context.message) { diff --git a/backend/src/plugins/Automod/triggers/matchInvites.ts b/backend/src/plugins/Automod/triggers/matchInvites.ts index 9e0b0c4e..4ea7c3cd 100644 --- a/backend/src/plugins/Automod/triggers/matchInvites.ts +++ b/backend/src/plugins/Automod/triggers/matchInvites.ts @@ -1,5 +1,5 @@ -import * as t from "io-ts"; -import { getInviteCodesInString, GuildInvite, isGuildInvite, resolveInvite, tNullable } from "../../../utils"; +import z from "zod"; +import { getInviteCodesInString, GuildInvite, isGuildInvite, resolveInvite, zSnowflake } from "../../../utils"; import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary"; import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage"; import { automodTrigger } from "../helpers"; @@ -10,30 +10,22 @@ interface MatchResultType { invite?: GuildInvite; } -export const MatchInvitesTrigger = automodTrigger<MatchResultType>()({ - configType: t.type({ - include_guilds: tNullable(t.array(t.string)), - exclude_guilds: tNullable(t.array(t.string)), - include_invite_codes: tNullable(t.array(t.string)), - exclude_invite_codes: tNullable(t.array(t.string)), - allow_group_dm_invites: t.boolean, - match_messages: t.boolean, - match_embeds: t.boolean, - match_visible_names: t.boolean, - match_usernames: t.boolean, - match_nicknames: t.boolean, - match_custom_status: t.boolean, - }), +const configSchema = z.strictObject({ + include_guilds: z.array(zSnowflake).max(255).optional(), + exclude_guilds: z.array(zSnowflake).max(255).optional(), + include_invite_codes: z.array(z.string().max(32)).max(255).optional(), + exclude_invite_codes: z.array(z.string().max(32)).max(255).optional(), + allow_group_dm_invites: z.boolean().default(false), + match_messages: z.boolean().default(true), + match_embeds: z.boolean().default(false), + match_visible_names: z.boolean().default(false), + match_usernames: z.boolean().default(false), + match_nicknames: z.boolean().default(false), + match_custom_status: z.boolean().default(false), +}); - defaultConfig: { - allow_group_dm_invites: false, - match_messages: true, - match_embeds: false, - match_visible_names: false, - match_usernames: false, - match_nicknames: false, - match_custom_status: false, - }, +export const MatchInvitesTrigger = automodTrigger<MatchResultType>()({ + configSchema, async match({ pluginData, context, triggerConfig: trigger }) { if (!context.message) { diff --git a/backend/src/plugins/Automod/triggers/matchLinks.ts b/backend/src/plugins/Automod/triggers/matchLinks.ts index 5a8de7de..128b3cd1 100644 --- a/backend/src/plugins/Automod/triggers/matchLinks.ts +++ b/backend/src/plugins/Automod/triggers/matchLinks.ts @@ -1,11 +1,10 @@ import { escapeInlineCode } from "discord.js"; -import * as t from "io-ts"; +import z from "zod"; import { allowTimeout } from "../../../RegExpRunner"; import { phishermanDomainIsSafe } from "../../../data/Phisherman"; -import { getUrlsInString, tNullable } from "../../../utils"; +import { getUrlsInString, zRegex } from "../../../utils"; import { mergeRegexes } from "../../../utils/mergeRegexes"; import { mergeWordsIntoRegex } from "../../../utils/mergeWordsIntoRegex"; -import { TRegex } from "../../../validatorUtils"; import { PhishermanPlugin } from "../../Phisherman/PhishermanPlugin"; import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary"; import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage"; @@ -21,40 +20,29 @@ const regexCache = new WeakMap<any, RegExp[]>(); const quickLinkCheck = /^https?:\/\//i; -export const MatchLinksTrigger = automodTrigger<MatchResultType>()({ - configType: t.type({ - include_domains: tNullable(t.array(t.string)), - exclude_domains: tNullable(t.array(t.string)), - include_subdomains: t.boolean, - include_words: tNullable(t.array(t.string)), - exclude_words: tNullable(t.array(t.string)), - include_regex: tNullable(t.array(TRegex)), - exclude_regex: tNullable(t.array(TRegex)), - phisherman: tNullable( - t.type({ - include_suspected: tNullable(t.boolean), - include_verified: tNullable(t.boolean), - }), - ), - only_real_links: t.boolean, - match_messages: t.boolean, - match_embeds: t.boolean, - match_visible_names: t.boolean, - match_usernames: t.boolean, - match_nicknames: t.boolean, - match_custom_status: t.boolean, - }), +const configSchema = z.strictObject({ + include_domains: z.array(z.string().max(255)).max(255).optional(), + exclude_domains: z.array(z.string().max(255)).max(255).optional(), + include_subdomains: z.boolean().default(true), + include_words: z.array(z.string().max(2000)).max(512).optional(), + exclude_words: z.array(z.string().max(2000)).max(512).optional(), + include_regex: z.array(zRegex(z.string().max(2000))).max(512).optional(), + exclude_regex: z.array(zRegex(z.string().max(2000))).max(512).optional(), + phisherman: z.strictObject({ + include_suspected: z.boolean().optional(), + include_verified: z.boolean().optional(), + }).optional(), + only_real_links: z.boolean(), + match_messages: z.boolean().default(true), + match_embeds: z.boolean().default(true), + match_visible_names: z.boolean().default(false), + match_usernames: z.boolean().default(false), + match_nicknames: z.boolean().default(false), + match_custom_status: z.boolean().default(false), +}); - defaultConfig: { - include_subdomains: true, - match_messages: true, - match_embeds: false, - match_visible_names: false, - match_usernames: false, - match_nicknames: false, - match_custom_status: false, - only_real_links: true, - }, +export const MatchLinksTrigger = automodTrigger<MatchResultType>()({ + configSchema, async match({ pluginData, context, triggerConfig: trigger }) { if (!context.message) { diff --git a/backend/src/plugins/Automod/triggers/matchMimeType.ts b/backend/src/plugins/Automod/triggers/matchMimeType.ts index 1af12471..8da9b2e3 100644 --- a/backend/src/plugins/Automod/triggers/matchMimeType.ts +++ b/backend/src/plugins/Automod/triggers/matchMimeType.ts @@ -1,5 +1,5 @@ import { escapeInlineCode } from "discord.js"; -import * as t from "io-ts"; +import z from "zod"; import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils"; import { automodTrigger } from "../helpers"; @@ -8,20 +8,31 @@ interface MatchResultType { mode: "blacklist" | "whitelist"; } -export const MatchMimeTypeTrigger = automodTrigger<MatchResultType>()({ - configType: t.type({ - mime_type_blacklist: t.array(t.string), - blacklist_enabled: t.boolean, - mime_type_whitelist: t.array(t.string), - whitelist_enabled: t.boolean, - }), +const configSchema = z.strictObject({ + mime_type_blacklist: z.array(z.string().max(255)).max(255).default([]), + blacklist_enabled: z.boolean().default(false), + mime_type_whitelist: z.array(z.string().max(255)).max(255).default([]), + whitelist_enabled: z.boolean().default(false), +}).transform((parsed, ctx) => { + if (parsed.blacklist_enabled && parsed.whitelist_enabled) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cannot have both blacklist and whitelist enabled", + }); + return z.NEVER; + } + if (! parsed.blacklist_enabled && ! parsed.whitelist_enabled) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Must have either blacklist or whitelist enabled", + }); + return z.NEVER; + } + return parsed; +}); - defaultConfig: { - mime_type_blacklist: [], - blacklist_enabled: false, - mime_type_whitelist: [], - whitelist_enabled: false, - }, +export const MatchMimeTypeTrigger = automodTrigger<MatchResultType>()({ + configSchema, async match({ context, triggerConfig: trigger }) { if (!context.message) return; diff --git a/backend/src/plugins/Automod/triggers/matchRegex.ts b/backend/src/plugins/Automod/triggers/matchRegex.ts index 7d011891..41df0396 100644 --- a/backend/src/plugins/Automod/triggers/matchRegex.ts +++ b/backend/src/plugins/Automod/triggers/matchRegex.ts @@ -1,9 +1,9 @@ -import * as t from "io-ts"; +import z from "zod"; import { allowTimeout } from "../../../RegExpRunner"; +import { zRegex } from "../../../utils"; import { mergeRegexes } from "../../../utils/mergeRegexes"; import { normalizeText } from "../../../utils/normalizeText"; import { stripMarkdown } from "../../../utils/stripMarkdown"; -import { TRegex } from "../../../validatorUtils"; import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary"; import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage"; import { automodTrigger } from "../helpers"; @@ -13,33 +13,23 @@ interface MatchResultType { type: MatchableTextType; } +const configSchema = z.strictObject({ + patterns: z.array(zRegex(z.string().max(2000))).max(512), + case_sensitive: z.boolean().default(false), + normalize: z.boolean().default(false), + strip_markdown: z.boolean().default(false), + match_messages: z.boolean().default(true), + match_embeds: z.boolean().default(false), + match_visible_names: z.boolean().default(false), + match_usernames: z.boolean().default(false), + match_nicknames: z.boolean().default(false), + match_custom_status: z.boolean().default(false), +}); + const regexCache = new WeakMap<any, RegExp[]>(); export const MatchRegexTrigger = automodTrigger<MatchResultType>()({ - configType: t.type({ - patterns: t.array(TRegex), - case_sensitive: t.boolean, - normalize: t.boolean, - strip_markdown: t.boolean, - match_messages: t.boolean, - match_embeds: t.boolean, - match_visible_names: t.boolean, - match_usernames: t.boolean, - match_nicknames: t.boolean, - match_custom_status: t.boolean, - }), - - defaultConfig: { - case_sensitive: false, - normalize: false, - strip_markdown: false, - match_messages: true, - match_embeds: false, - match_visible_names: false, - match_usernames: false, - match_nicknames: false, - match_custom_status: false, - }, + configSchema, async match({ pluginData, context, triggerConfig: trigger }) { if (!context.message) { diff --git a/backend/src/plugins/Automod/triggers/matchWords.ts b/backend/src/plugins/Automod/triggers/matchWords.ts index c5ca79fc..e8c275e1 100644 --- a/backend/src/plugins/Automod/triggers/matchWords.ts +++ b/backend/src/plugins/Automod/triggers/matchWords.ts @@ -1,5 +1,5 @@ import escapeStringRegexp from "escape-string-regexp"; -import * as t from "io-ts"; +import z from "zod"; import { normalizeText } from "../../../utils/normalizeText"; import { stripMarkdown } from "../../../utils/stripMarkdown"; import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary"; @@ -13,37 +13,24 @@ interface MatchResultType { const regexCache = new WeakMap<any, RegExp[]>(); -export const MatchWordsTrigger = automodTrigger<MatchResultType>()({ - configType: t.type({ - words: t.array(t.string), - case_sensitive: t.boolean, - only_full_words: t.boolean, - normalize: t.boolean, - loose_matching: t.boolean, - loose_matching_threshold: t.number, - strip_markdown: t.boolean, - match_messages: t.boolean, - match_embeds: t.boolean, - match_visible_names: t.boolean, - match_usernames: t.boolean, - match_nicknames: t.boolean, - match_custom_status: t.boolean, - }), +const configSchema = z.strictObject({ + words: z.array(z.string().max(2000)).max(512), + case_sensitive: z.boolean().default(false), + only_full_words: z.boolean().default(true), + normalize: z.boolean().default(false), + loose_matching: z.boolean().default(false), + loose_matching_threshold: z.number().int().default(4), + strip_markdown: z.boolean().default(false), + match_messages: z.boolean().default(true), + match_embeds: z.boolean().default(false), + match_visible_names: z.boolean().default(false), + match_usernames: z.boolean().default(false), + match_nicknames: z.boolean().default(false), + match_custom_status: z.boolean().default(false), +}); - defaultConfig: { - case_sensitive: false, - only_full_words: true, - normalize: false, - loose_matching: false, - loose_matching_threshold: 4, - strip_markdown: false, - match_messages: true, - match_embeds: false, - match_visible_names: false, - match_usernames: false, - match_nicknames: false, - match_custom_status: false, - }, +export const MatchWordsTrigger = automodTrigger<MatchResultType>()({ + configSchema, async match({ pluginData, context, triggerConfig: trigger }) { if (!context.message) { diff --git a/backend/src/plugins/Automod/triggers/memberJoin.ts b/backend/src/plugins/Automod/triggers/memberJoin.ts index f62d87aa..af9a1fae 100644 --- a/backend/src/plugins/Automod/triggers/memberJoin.ts +++ b/backend/src/plugins/Automod/triggers/memberJoin.ts @@ -1,17 +1,14 @@ -import * as t from "io-ts"; -import { convertDelayStringToMS, tDelayString } from "../../../utils"; +import z from "zod"; +import { convertDelayStringToMS, zDelayString } from "../../../utils"; import { automodTrigger } from "../helpers"; -export const MemberJoinTrigger = automodTrigger<unknown>()({ - configType: t.type({ - only_new: t.boolean, - new_threshold: tDelayString, - }), +const configSchema = z.strictObject({ + only_new: z.boolean().default(false), + new_threshold: zDelayString.default("1h"), +}); - defaultConfig: { - only_new: false, - new_threshold: "1h", - }, +export const MemberJoinTrigger = automodTrigger<unknown>()({ + configSchema, async match({ context, triggerConfig }) { if (!context.joined || !context.member) { diff --git a/backend/src/plugins/Automod/triggers/memberJoinSpam.ts b/backend/src/plugins/Automod/triggers/memberJoinSpam.ts index c55c8895..e346a8d3 100644 --- a/backend/src/plugins/Automod/triggers/memberJoinSpam.ts +++ b/backend/src/plugins/Automod/triggers/memberJoinSpam.ts @@ -1,18 +1,18 @@ -import * as t from "io-ts"; -import { convertDelayStringToMS, tDelayString } from "../../../utils"; +import z from "zod"; +import { convertDelayStringToMS, zDelayString } from "../../../utils"; import { RecentActionType } from "../constants"; import { findRecentSpam } from "../functions/findRecentSpam"; import { getMatchingRecentActions } from "../functions/getMatchingRecentActions"; import { sumRecentActionCounts } from "../functions/sumRecentActionCounts"; import { automodTrigger } from "../helpers"; -export const MemberJoinSpamTrigger = automodTrigger<unknown>()({ - configType: t.type({ - amount: t.number, - within: tDelayString, - }), +const configSchema = z.strictObject({ + amount: z.number().int(), + within: zDelayString, +}); - defaultConfig: {}, +export const MemberJoinSpamTrigger = automodTrigger<unknown>()({ + configSchema, async match({ pluginData, context, triggerConfig }) { if (!context.joined || !context.member) { diff --git a/backend/src/plugins/Automod/triggers/memberLeave.ts b/backend/src/plugins/Automod/triggers/memberLeave.ts index c5a25033..fa9f37a8 100644 --- a/backend/src/plugins/Automod/triggers/memberLeave.ts +++ b/backend/src/plugins/Automod/triggers/memberLeave.ts @@ -1,10 +1,10 @@ -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; -export const MemberLeaveTrigger = automodTrigger<unknown>()({ - configType: t.type({}), +const configSchema = z.strictObject({}); - defaultConfig: {}, +export const MemberLeaveTrigger = automodTrigger<unknown>()({ + configSchema, async match({ context }) { if (!context.joined || !context.member) { diff --git a/backend/src/plugins/Automod/triggers/mute.ts b/backend/src/plugins/Automod/triggers/mute.ts index 144c30f2..dcaf96c5 100644 --- a/backend/src/plugins/Automod/triggers/mute.ts +++ b/backend/src/plugins/Automod/triggers/mute.ts @@ -1,19 +1,16 @@ -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; // tslint:disable-next-line:no-empty-interface interface MuteTriggerResultType {} -export const MuteTrigger = automodTrigger<MuteTriggerResultType>()({ - configType: t.type({ - manual: t.boolean, - automatic: t.boolean, - }), +const configSchema = z.strictObject({ + manual: z.boolean().default(true), + automatic: z.boolean().default(true), +}); - defaultConfig: { - manual: true, - automatic: true, - }, +export const MuteTrigger = automodTrigger<MuteTriggerResultType>()({ + configSchema, async match({ context, triggerConfig }) { if (context.modAction?.type !== "mute") { diff --git a/backend/src/plugins/Automod/triggers/note.ts b/backend/src/plugins/Automod/triggers/note.ts index c6d11be7..eeaa2c27 100644 --- a/backend/src/plugins/Automod/triggers/note.ts +++ b/backend/src/plugins/Automod/triggers/note.ts @@ -1,12 +1,13 @@ -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; // tslint:disable-next-line:no-empty-interface interface NoteTriggerResultType {} +const configSchema = z.strictObject({}); + export const NoteTrigger = automodTrigger<NoteTriggerResultType>()({ - configType: t.type({}), - defaultConfig: {}, + configSchema, async match({ context }) { if (context.modAction?.type !== "note") { diff --git a/backend/src/plugins/Automod/triggers/roleAdded.ts b/backend/src/plugins/Automod/triggers/roleAdded.ts index dc62f163..47027b6b 100644 --- a/backend/src/plugins/Automod/triggers/roleAdded.ts +++ b/backend/src/plugins/Automod/triggers/roleAdded.ts @@ -1,6 +1,6 @@ import { Snowflake } from "discord.js"; -import * as t from "io-ts"; -import { renderUserUsername } from "../../../utils"; +import z from "zod"; +import { renderUserUsername, zSnowflake } from "../../../utils"; import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges"; import { automodTrigger } from "../helpers"; @@ -8,10 +8,13 @@ interface RoleAddedMatchResult { matchedRoleId: string; } -export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({ - configType: t.union([t.string, t.array(t.string)]), +const configSchema = z.union([ + zSnowflake, + z.array(zSnowflake).max(255), +]).default([]); - defaultConfig: "", +export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({ + configSchema, async match({ triggerConfig, context, pluginData }) { if (!context.member || !context.rolesChanged || context.rolesChanged.added!.length === 0) { diff --git a/backend/src/plugins/Automod/triggers/roleRemoved.ts b/backend/src/plugins/Automod/triggers/roleRemoved.ts index 65624827..26caf227 100644 --- a/backend/src/plugins/Automod/triggers/roleRemoved.ts +++ b/backend/src/plugins/Automod/triggers/roleRemoved.ts @@ -1,6 +1,6 @@ import { Snowflake } from "discord.js"; -import * as t from "io-ts"; -import { renderUserUsername } from "../../../utils"; +import z from "zod"; +import { renderUserUsername, zSnowflake } from "../../../utils"; import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges"; import { automodTrigger } from "../helpers"; @@ -8,10 +8,13 @@ interface RoleAddedMatchResult { matchedRoleId: string; } -export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({ - configType: t.union([t.string, t.array(t.string)]), +const configSchema = z.union([ + zSnowflake, + z.array(zSnowflake).max(255), +]).default([]); - defaultConfig: "", +export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({ + configSchema, async match({ triggerConfig, context, pluginData }) { if (!context.member || !context.rolesChanged || context.rolesChanged.removed!.length === 0) { diff --git a/backend/src/plugins/Automod/triggers/threadArchive.ts b/backend/src/plugins/Automod/triggers/threadArchive.ts index 0e65b10a..87cc1b2a 100644 --- a/backend/src/plugins/Automod/triggers/threadArchive.ts +++ b/backend/src/plugins/Automod/triggers/threadArchive.ts @@ -1,6 +1,5 @@ import { User, escapeBold, type Snowflake } from "discord.js"; -import * as t from "io-ts"; -import { tNullable } from "../../../utils"; +import z from "zod"; import { automodTrigger } from "../helpers"; interface ThreadArchiveResult { @@ -11,12 +10,12 @@ interface ThreadArchiveResult { matchedThreadOwner: User | undefined; } -export const ThreadArchiveTrigger = automodTrigger<ThreadArchiveResult>()({ - configType: t.type({ - locked: tNullable(t.boolean), - }), +const configSchema = z.strictObject({ + locked: z.boolean().optional(), +}); - defaultConfig: {}, +export const ThreadArchiveTrigger = automodTrigger<ThreadArchiveResult>()({ + configSchema, async match({ context, triggerConfig }) { if (!context.threadChange?.archived) { diff --git a/backend/src/plugins/Automod/triggers/threadCreate.ts b/backend/src/plugins/Automod/triggers/threadCreate.ts index 7b8aca71..ba5553de 100644 --- a/backend/src/plugins/Automod/triggers/threadCreate.ts +++ b/backend/src/plugins/Automod/triggers/threadCreate.ts @@ -1,5 +1,5 @@ import { User, escapeBold, type Snowflake } from "discord.js"; -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; interface ThreadCreateResult { @@ -10,9 +10,10 @@ interface ThreadCreateResult { matchedThreadOwner: User | undefined; } +const configSchema = z.strictObject({}); + export const ThreadCreateTrigger = automodTrigger<ThreadCreateResult>()({ - configType: t.type({}), - defaultConfig: {}, + configSchema, async match({ context }) { if (!context.threadChange?.created) { diff --git a/backend/src/plugins/Automod/triggers/threadCreateSpam.ts b/backend/src/plugins/Automod/triggers/threadCreateSpam.ts index b1d02f47..a4352c11 100644 --- a/backend/src/plugins/Automod/triggers/threadCreateSpam.ts +++ b/backend/src/plugins/Automod/triggers/threadCreateSpam.ts @@ -1,18 +1,18 @@ -import * as t from "io-ts"; -import { convertDelayStringToMS, tDelayString } from "../../../utils"; +import z from "zod"; +import { convertDelayStringToMS, zDelayString } from "../../../utils"; import { RecentActionType } from "../constants"; import { findRecentSpam } from "../functions/findRecentSpam"; import { getMatchingRecentActions } from "../functions/getMatchingRecentActions"; import { sumRecentActionCounts } from "../functions/sumRecentActionCounts"; import { automodTrigger } from "../helpers"; -export const ThreadCreateSpamTrigger = automodTrigger<unknown>()({ - configType: t.type({ - amount: t.number, - within: tDelayString, - }), +const configSchema = z.strictObject({ + amount: z.number().int(), + within: zDelayString, +}); - defaultConfig: {}, +export const ThreadCreateSpamTrigger = automodTrigger<unknown>()({ + configSchema, async match({ pluginData, context, triggerConfig }) { if (!context.threadChange?.created) { diff --git a/backend/src/plugins/Automod/triggers/threadDelete.ts b/backend/src/plugins/Automod/triggers/threadDelete.ts index 489b5b4c..fc538cf8 100644 --- a/backend/src/plugins/Automod/triggers/threadDelete.ts +++ b/backend/src/plugins/Automod/triggers/threadDelete.ts @@ -1,5 +1,5 @@ import { User, escapeBold, type Snowflake } from "discord.js"; -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; interface ThreadDeleteResult { @@ -10,9 +10,10 @@ interface ThreadDeleteResult { matchedThreadOwner: User | undefined; } +const configSchema = z.strictObject({}); + export const ThreadDeleteTrigger = automodTrigger<ThreadDeleteResult>()({ - configType: t.type({}), - defaultConfig: {}, + configSchema, async match({ context }) { if (!context.threadChange?.deleted) { diff --git a/backend/src/plugins/Automod/triggers/threadUnarchive.ts b/backend/src/plugins/Automod/triggers/threadUnarchive.ts index f6047f48..3d4a3cdf 100644 --- a/backend/src/plugins/Automod/triggers/threadUnarchive.ts +++ b/backend/src/plugins/Automod/triggers/threadUnarchive.ts @@ -1,6 +1,5 @@ import { User, escapeBold, type Snowflake } from "discord.js"; -import * as t from "io-ts"; -import { tNullable } from "../../../utils"; +import z from "zod"; import { automodTrigger } from "../helpers"; interface ThreadUnarchiveResult { @@ -11,12 +10,12 @@ interface ThreadUnarchiveResult { matchedThreadOwner: User | undefined; } -export const ThreadUnarchiveTrigger = automodTrigger<ThreadUnarchiveResult>()({ - configType: t.type({ - locked: tNullable(t.boolean), - }), +const configSchema = z.strictObject({ + locked: z.boolean().optional(), +}); - defaultConfig: {}, +export const ThreadUnarchiveTrigger = automodTrigger<ThreadUnarchiveResult>()({ + configSchema, async match({ context, triggerConfig }) { if (!context.threadChange?.unarchived) { diff --git a/backend/src/plugins/Automod/triggers/unban.ts b/backend/src/plugins/Automod/triggers/unban.ts index c653f615..25f24ec9 100644 --- a/backend/src/plugins/Automod/triggers/unban.ts +++ b/backend/src/plugins/Automod/triggers/unban.ts @@ -1,12 +1,13 @@ -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; // tslint:disable-next-line:no-empty-interface interface UnbanTriggerResultType {} +const configSchema = z.strictObject({}); + export const UnbanTrigger = automodTrigger<UnbanTriggerResultType>()({ - configType: t.type({}), - defaultConfig: {}, + configSchema, async match({ context }) { if (context.modAction?.type !== "unban") { diff --git a/backend/src/plugins/Automod/triggers/unmute.ts b/backend/src/plugins/Automod/triggers/unmute.ts index fbd946c6..f9695ef0 100644 --- a/backend/src/plugins/Automod/triggers/unmute.ts +++ b/backend/src/plugins/Automod/triggers/unmute.ts @@ -1,12 +1,13 @@ -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; // tslint:disable-next-line:no-empty-interface interface UnmuteTriggerResultType {} +const configSchema = z.strictObject({}); + export const UnmuteTrigger = automodTrigger<UnmuteTriggerResultType>()({ - configType: t.type({}), - defaultConfig: {}, + configSchema, async match({ context }) { if (context.modAction?.type !== "unmute") { diff --git a/backend/src/plugins/Automod/triggers/warn.ts b/backend/src/plugins/Automod/triggers/warn.ts index 5c350dad..3586e82d 100644 --- a/backend/src/plugins/Automod/triggers/warn.ts +++ b/backend/src/plugins/Automod/triggers/warn.ts @@ -1,19 +1,16 @@ -import * as t from "io-ts"; +import z from "zod"; import { automodTrigger } from "../helpers"; // tslint:disable-next-line:no-empty-interface interface WarnTriggerResultType {} -export const WarnTrigger = automodTrigger<WarnTriggerResultType>()({ - configType: t.type({ - manual: t.boolean, - automatic: t.boolean, - }), +const configSchema = z.strictObject({ + manual: z.boolean().default(true), + automatic: z.boolean().default(true), +}); - defaultConfig: { - manual: true, - automatic: true, - }, +export const WarnTrigger = automodTrigger<WarnTriggerResultType>()({ + configSchema, async match({ context, triggerConfig }) { if (context.modAction?.type !== "warn") { diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index bd3f05ee..59a09a33 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -1,6 +1,6 @@ import { GuildMember, GuildTextBasedChannel, PartialGuildMember, ThreadChannel, User } from "discord.js"; -import * as t from "io-ts"; import { BasePluginType, CooldownManager } from "knub"; +import z from "zod"; import { Queue } from "../../Queue"; import { RegExpRunner } from "../../RegExpRunner"; import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels"; @@ -8,39 +8,81 @@ import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { SavedMessage } from "../../data/entities/SavedMessage"; -import { tNullable } from "../../utils"; +import { entries, zBoundedRecord, zDelayString } from "../../utils"; import { CounterEvents } from "../Counters/types"; import { ModActionType, ModActionsEvents } from "../ModActions/types"; import { MutesEvents } from "../Mutes/types"; -import { AvailableActions } from "./actions/availableActions"; +import { availableActions } from "./actions/availableActions"; import { RecentActionType } from "./constants"; -import { AvailableTriggers } from "./triggers/availableTriggers"; +import { availableTriggers } from "./triggers/availableTriggers"; import Timeout = NodeJS.Timeout; -export const Rule = t.type({ - enabled: t.boolean, - name: t.string, - presets: tNullable(t.array(t.string)), - affects_bots: t.boolean, - affects_self: t.boolean, - triggers: t.array(t.partial(AvailableTriggers.props)), - actions: t.partial(AvailableActions.props), - cooldown: tNullable(t.string), - allow_further_rules: t.boolean, -}); -export type TRule = t.TypeOf<typeof Rule>; +export type ZTriggersMapHelper = { + [TriggerName in keyof typeof availableTriggers]: typeof availableTriggers[TriggerName]["configSchema"]; +}; +const zTriggersMap = z.strictObject(entries(availableTriggers).reduce((map, [triggerName, trigger]) => { + map[triggerName] = trigger.configSchema; + return map; +}, {} as ZTriggersMapHelper)).partial(); -export const ConfigSchema = t.type({ - rules: t.record(t.string, Rule), - antiraid_levels: t.array(t.string), - can_set_antiraid: t.boolean, - can_view_antiraid: t.boolean, +type ZActionsMapHelper = { + [ActionName in keyof typeof availableActions]: typeof availableActions[ActionName]["configSchema"]; +}; +const zActionsMap = z.strictObject(entries(availableActions).reduce((map, [actionName, action]) => { + // @ts-expect-error TS can't infer this properly but it works fine thanks to our helper + map[actionName] = action.configSchema; + return map; +}, {} as ZActionsMapHelper)).partial(); + +const zRule = z.strictObject({ + enabled: z.boolean().default(true), + // Typed as "never" because you are not expected to supply this directly. + // The transform instead picks it up from the property key and the output type is a string. + name: z.never().optional().transform((_, ctx) => { + const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); + if (! ruleName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Automod rules must have names", + }); + return z.NEVER; + } + return ruleName; + }), + presets: z.array(z.string().max(100)).max(25).default([]), + affects_bots: z.boolean().default(false), + affects_self: z.boolean().default(false), + cooldown: zDelayString.nullable().default(null), + allow_further_rules: z.boolean().default(false), + triggers: z.array(zTriggersMap), + actions: zActionsMap.refine( + (v) => ! (v.clean && v.start_thread), + { + message: "Cannot have both clean and start_thread active at the same time", + } + ), +}); +export type TRule = z.infer<typeof zRule>; + +export const zNotify = z.union([ + z.literal("dm"), + z.literal("channel"), +]); + +export const zAutomodConfig = z.strictObject({ + rules: zBoundedRecord( + z.record(z.string().max(100), zRule), + 0, + 100, + ), + antiraid_levels: z.array(z.string().max(100)).max(10), + can_set_antiraid: z.boolean(), + can_view_antiraid: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface AutomodPluginType extends BasePluginType { - config: TConfigSchema; + config: z.output<typeof zAutomodConfig>; customOverrideCriteria: { antiraid_level?: string; diff --git a/backend/src/plugins/BotControl/BotControlPlugin.ts b/backend/src/plugins/BotControl/BotControlPlugin.ts index 76478ca2..653e8def 100644 --- a/backend/src/plugins/BotControl/BotControlPlugin.ts +++ b/backend/src/plugins/BotControl/BotControlPlugin.ts @@ -3,7 +3,7 @@ import { AllowedGuilds } from "../../data/AllowedGuilds"; import { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments"; import { Configs } from "../../data/Configs"; import { GuildArchives } from "../../data/GuildArchives"; -import { makeIoTsConfigParser, sendSuccessMessage } from "../../pluginUtils"; +import { sendSuccessMessage } from "../../pluginUtils"; import { zeppelinGlobalPlugin } from "../ZeppelinPluginBlueprint"; import { getActiveReload, resetActiveReload } from "./activeReload"; import { AddDashboardUserCmd } from "./commands/AddDashboardUserCmd"; @@ -22,7 +22,7 @@ import { ReloadServerCmd } from "./commands/ReloadServerCmd"; import { RemoveDashboardUserCmd } from "./commands/RemoveDashboardUserCmd"; import { RestPerformanceCmd } from "./commands/RestPerformanceCmd"; import { ServersCmd } from "./commands/ServersCmd"; -import { BotControlPluginType, ConfigSchema } from "./types"; +import { BotControlPluginType, zBotControlConfig } from "./types"; const defaultOptions = { config: { @@ -37,7 +37,7 @@ const defaultOptions = { export const BotControlPlugin = zeppelinGlobalPlugin<BotControlPluginType>()({ name: "bot_control", - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zBotControlConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/BotControl/types.ts b/backend/src/plugins/BotControl/types.ts index 6f4139f8..1c1ccea6 100644 --- a/backend/src/plugins/BotControl/types.ts +++ b/backend/src/plugins/BotControl/types.ts @@ -1,23 +1,22 @@ -import * as t from "io-ts"; import { BasePluginType, globalPluginEventListener, globalPluginMessageCommand } from "knub"; +import z from "zod"; import { AllowedGuilds } from "../../data/AllowedGuilds"; import { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments"; import { Configs } from "../../data/Configs"; import { GuildArchives } from "../../data/GuildArchives"; -import { tNullable } from "../../utils"; +import { zBoundedCharacters } from "../../utils"; -export const ConfigSchema = t.type({ - can_use: t.boolean, - can_eligible: t.boolean, - can_performance: t.boolean, - can_add_server_from_invite: t.boolean, - can_list_dashboard_perms: t.boolean, - update_cmd: tNullable(t.string), +export const zBotControlConfig = z.strictObject({ + can_use: z.boolean(), + can_eligible: z.boolean(), + can_performance: z.boolean(), + can_add_server_from_invite: z.boolean(), + can_list_dashboard_perms: z.boolean(), + update_cmd: zBoundedCharacters(0, 2000).nullable(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface BotControlPluginType extends BasePluginType { - config: TConfigSchema; + config: z.output<typeof zBotControlConfig>; state: { archives: GuildArchives; allowedGuilds: AllowedGuilds; diff --git a/backend/src/plugins/Cases/CasesPlugin.ts b/backend/src/plugins/Cases/CasesPlugin.ts index 45f63c79..9e6df53a 100644 --- a/backend/src/plugins/Cases/CasesPlugin.ts +++ b/backend/src/plugins/Cases/CasesPlugin.ts @@ -3,7 +3,7 @@ import { Case } from "../../data/entities/Case"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildCases } from "../../data/GuildCases"; import { GuildLogs } from "../../data/GuildLogs"; -import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils"; +import { mapToPublicFn } from "../../pluginUtils"; import { trimPluginDescription } from "../../utils"; import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; @@ -16,7 +16,7 @@ import { getCaseTypeAmountForUserId } from "./functions/getCaseTypeAmountForUser import { getRecentCasesByMod } from "./functions/getRecentCasesByMod"; import { getTotalCasesByMod } from "./functions/getTotalCasesByMod"; import { postCaseToCaseLogChannel } from "./functions/postToCaseLogChannel"; -import { CaseArgs, CaseNoteArgs, CasesPluginType, ConfigSchema } from "./types"; +import { CaseArgs, CaseNoteArgs, CasesPluginType, zCasesConfig } from "./types"; // The `any` cast here is to prevent TypeScript from locking up from the circular dependency function getLogsPlugin(): Promise<any> { @@ -42,11 +42,11 @@ export const CasesPlugin = zeppelinGuildPlugin<CasesPluginType>()({ description: trimPluginDescription(` This plugin contains basic configuration for cases created by other plugins `), - configSchema: ConfigSchema, + configSchema: zCasesConfig, }, dependencies: async () => [TimeAndDatePlugin, InternalPosterPlugin, (await getLogsPlugin()).LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zCasesConfig.parse(input), defaultOptions, public: { diff --git a/backend/src/plugins/Cases/types.ts b/backend/src/plugins/Cases/types.ts index 9ec1a371..e25b7ed0 100644 --- a/backend/src/plugins/Cases/types.ts +++ b/backend/src/plugins/Cases/types.ts @@ -1,24 +1,26 @@ -import * as t from "io-ts"; import { BasePluginType } from "knub"; +import { U } from "ts-toolbelt"; +import z from "zod"; import { CaseNameToType, CaseTypes } from "../../data/CaseTypes"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildCases } from "../../data/GuildCases"; import { GuildLogs } from "../../data/GuildLogs"; -import { tDelayString, tNullable, tPartialDictionary } from "../../utils"; -import { tColor } from "../../utils/tColor"; +import { keys, zBoundedCharacters, zDelayString, zSnowflake } from "../../utils"; +import { zColor } from "../../utils/zColor"; -export const ConfigSchema = t.type({ - log_automatic_actions: t.boolean, - case_log_channel: tNullable(t.string), - show_relative_times: t.boolean, - relative_time_cutoff: tDelayString, - case_colors: tNullable(tPartialDictionary(t.keyof(CaseNameToType), tColor)), - case_icons: tNullable(tPartialDictionary(t.keyof(CaseNameToType), t.string)), +const caseKeys = keys(CaseNameToType) as U.ListOf<keyof typeof CaseNameToType>; + +export const zCasesConfig = z.strictObject({ + log_automatic_actions: z.boolean(), + case_log_channel: zSnowflake.nullable(), + show_relative_times: z.boolean(), + relative_time_cutoff: zDelayString.default("1w"), + case_colors: z.record(z.enum(caseKeys), zColor).nullable(), + case_icons: z.record(z.enum(caseKeys), zBoundedCharacters(0, 32)).nullable(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface CasesPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zCasesConfig>; state: { logs: GuildLogs; cases: GuildCases; diff --git a/backend/src/plugins/Censor/CensorPlugin.ts b/backend/src/plugins/Censor/CensorPlugin.ts index 61991e12..5764d396 100644 --- a/backend/src/plugins/Censor/CensorPlugin.ts +++ b/backend/src/plugins/Censor/CensorPlugin.ts @@ -1,12 +1,11 @@ import { PluginOptions } from "knub"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { trimPluginDescription } from "../../utils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; -import { CensorPluginType, ConfigSchema } from "./types"; +import { CensorPluginType, zCensorConfig } from "./types"; import { onMessageCreate } from "./util/onMessageCreate"; import { onMessageUpdate } from "./util/onMessageUpdate"; @@ -54,11 +53,11 @@ export const CensorPlugin = zeppelinGuildPlugin<CensorPluginType>()({ For more advanced filtering, check out the Automod plugin! `), legacy: true, - configSchema: ConfigSchema, + configSchema: zCensorConfig, }, dependencies: () => [LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zCensorConfig.parse(input), defaultOptions, beforeLoad(pluginData) { diff --git a/backend/src/plugins/Censor/types.ts b/backend/src/plugins/Censor/types.ts index 5a1984ce..c1fcd8df 100644 --- a/backend/src/plugins/Censor/types.ts +++ b/backend/src/plugins/Censor/types.ts @@ -1,30 +1,28 @@ -import * as t from "io-ts"; import { BasePluginType } from "knub"; +import z from "zod"; import { RegExpRunner } from "../../RegExpRunner"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -import { tNullable } from "../../utils"; -import { TRegex } from "../../validatorUtils"; +import { zBoundedCharacters, zRegex, zSnowflake } from "../../utils"; -export const ConfigSchema = t.type({ - filter_zalgo: t.boolean, - filter_invites: t.boolean, - invite_guild_whitelist: tNullable(t.array(t.string)), - invite_guild_blacklist: tNullable(t.array(t.string)), - invite_code_whitelist: tNullable(t.array(t.string)), - invite_code_blacklist: tNullable(t.array(t.string)), - allow_group_dm_invites: t.boolean, - filter_domains: t.boolean, - domain_whitelist: tNullable(t.array(t.string)), - domain_blacklist: tNullable(t.array(t.string)), - blocked_tokens: tNullable(t.array(t.string)), - blocked_words: tNullable(t.array(t.string)), - blocked_regex: tNullable(t.array(TRegex)), +export const zCensorConfig = z.strictObject({ + filter_zalgo: z.boolean(), + filter_invites: z.boolean(), + invite_guild_whitelist: z.array(zSnowflake).nullable(), + invite_guild_blacklist: z.array(zSnowflake).nullable(), + invite_code_whitelist: z.array(zBoundedCharacters(0, 16)).nullable(), + invite_code_blacklist: z.array(zBoundedCharacters(0, 16)).nullable(), + allow_group_dm_invites: z.boolean(), + filter_domains: z.boolean(), + domain_whitelist: z.array(zBoundedCharacters(0, 255)).nullable(), + domain_blacklist: z.array(zBoundedCharacters(0, 255)).nullable(), + blocked_tokens: z.array(zBoundedCharacters(0, 2000)).nullable(), + blocked_words: z.array(zBoundedCharacters(0, 2000)).nullable(), + blocked_regex: z.array(zRegex(z.string().max(1000))).nullable(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface CensorPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zCensorConfig>; state: { serverLogs: GuildLogs; savedMessages: GuildSavedMessages; diff --git a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts index b4467819..615d3467 100644 --- a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts +++ b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts @@ -1,18 +1,15 @@ -import * as t from "io-ts"; -import { makeIoTsConfigParser } from "../../pluginUtils"; +import z from "zod"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { ArchiveChannelCmd } from "./commands/ArchiveChannelCmd"; import { ChannelArchiverPluginType } from "./types"; -const ConfigSchema = t.type({}); - export const ChannelArchiverPlugin = zeppelinGuildPlugin<ChannelArchiverPluginType>()({ name: "channel_archiver", showInDocs: false, dependencies: () => [TimeAndDatePlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => z.strictObject({}).parse(input), // prettier-ignore messageCommands: [ diff --git a/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts b/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts index bd05d800..4e7987a7 100644 --- a/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts +++ b/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts @@ -1,11 +1,10 @@ import { CooldownManager } from "knub"; import { GuildLogs } from "../../data/GuildLogs"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { trimPluginDescription } from "../../utils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { VoiceStateUpdateEvt } from "./events/VoiceStateUpdateEvt"; -import { CompanionChannelsPluginType, ConfigSchema } from "./types"; +import { CompanionChannelsPluginType, zCompanionChannelsConfig } from "./types"; const defaultOptions = { config: { @@ -23,11 +22,11 @@ export const CompanionChannelsPlugin = zeppelinGuildPlugin<CompanionChannelsPlug Once set up, any time a user joins one of the specified voice channels, they'll get channel permissions applied to them for the text channels. `), - configSchema: ConfigSchema, + configSchema: zCompanionChannelsConfig, }, dependencies: () => [LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zCompanionChannelsConfig.parse(input), defaultOptions, events: [VoiceStateUpdateEvt], diff --git a/backend/src/plugins/CompanionChannels/types.ts b/backend/src/plugins/CompanionChannels/types.ts index 46a8a45f..7cee4b3a 100644 --- a/backend/src/plugins/CompanionChannels/types.ts +++ b/backend/src/plugins/CompanionChannels/types.ts @@ -1,28 +1,23 @@ -import * as t from "io-ts"; import { BasePluginType, CooldownManager, guildPluginEventListener } from "knub"; +import z from "zod"; import { GuildLogs } from "../../data/GuildLogs"; -import { tNullable } from "../../utils"; +import { zBoundedCharacters, zSnowflake } from "../../utils"; -// Permissions using these numbers: https://abal.moe/Eris/docs/reference (add all allowed/denied ones up) -export const CompanionChannelOpts = t.type({ - voice_channel_ids: t.array(t.string), - text_channel_ids: t.array(t.string), - permissions: t.number, - enabled: tNullable(t.boolean), +export const zCompanionChannelOpts = z.strictObject({ + voice_channel_ids: z.array(zSnowflake), + text_channel_ids: z.array(zSnowflake), + // See https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags + permissions: z.number(), + enabled: z.boolean().nullable().default(true), }); -export type TCompanionChannelOpts = t.TypeOf<typeof CompanionChannelOpts>; +export type TCompanionChannelOpts = z.infer<typeof zCompanionChannelOpts>; -export const ConfigSchema = t.type({ - entries: t.record(t.string, CompanionChannelOpts), +export const zCompanionChannelsConfig = z.strictObject({ + entries: z.record(zBoundedCharacters(0, 100), zCompanionChannelOpts), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; - -export interface ICompanionChannelMap { - [channelId: string]: TCompanionChannelOpts; -} export interface CompanionChannelsPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zCompanionChannelsConfig>; state: { errorCooldownManager: CooldownManager; serverLogs: GuildLogs; diff --git a/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts b/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts index c41e8c09..57d1a763 100644 --- a/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts +++ b/backend/src/plugins/ContextMenus/ContextMenuPlugin.ts @@ -1,12 +1,11 @@ import { PluginOptions } from "knub"; import { GuildContextMenuLinks } from "../../data/GuildContextMenuLinks"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { MutesPlugin } from "../Mutes/MutesPlugin"; import { UtilityPlugin } from "../Utility/UtilityPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { ContextClickedEvt } from "./events/ContextClickedEvt"; -import { ConfigSchema, ContextMenuPluginType } from "./types"; +import { ContextMenuPluginType, zContextMenusConfig } from "./types"; import { loadAllCommands } from "./utils/loadAllCommands"; const defaultOptions: PluginOptions<ContextMenuPluginType> = { @@ -37,7 +36,7 @@ export const ContextMenuPlugin = zeppelinGuildPlugin<ContextMenuPluginType>()({ showInDocs: false, dependencies: () => [MutesPlugin, LogsPlugin, UtilityPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zContextMenusConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/ContextMenus/actions/mute.ts b/backend/src/plugins/ContextMenus/actions/mute.ts index 7fe2f5d5..556ee248 100644 --- a/backend/src/plugins/ContextMenus/actions/mute.ts +++ b/backend/src/plugins/ContextMenus/actions/mute.ts @@ -45,9 +45,9 @@ export async function muteAction( try { const result = await mutes.muteUser(userId, durationMs, "Context Menu Action", { caseArgs }); - const muteMessage = `Muted **${result.case.user_name}** ${ + const muteMessage = `Muted **${result.case!.user_name}** ${ durationMs ? `for ${humanizeDuration(durationMs)}` : "indefinitely" - } (Case #${result.case.case_number}) (user notified via ${ + } (Case #${result.case!.case_number}) (user notified via ${ result.notifyResult.method ?? "dm" })\nPlease update the new case with the \`update\` command`; diff --git a/backend/src/plugins/ContextMenus/types.ts b/backend/src/plugins/ContextMenus/types.ts index 02c4a29c..5dbcd9ce 100644 --- a/backend/src/plugins/ContextMenus/types.ts +++ b/backend/src/plugins/ContextMenus/types.ts @@ -1,22 +1,20 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener } from "knub"; +import z from "zod"; import { GuildContextMenuLinks } from "../../data/GuildContextMenuLinks"; -export const ConfigSchema = t.type({ - can_use: t.boolean, - - user_muteindef: t.boolean, - user_mute1d: t.boolean, - user_mute1h: t.boolean, - user_info: t.boolean, - message_clean10: t.boolean, - message_clean25: t.boolean, - message_clean50: t.boolean, +export const zContextMenusConfig = z.strictObject({ + can_use: z.boolean(), + user_muteindef: z.boolean(), + user_mute1d: z.boolean(), + user_mute1h: z.boolean(), + user_info: z.boolean(), + message_clean10: z.boolean(), + message_clean25: z.boolean(), + message_clean50: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface ContextMenuPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zContextMenusConfig>; state: { contextMenuLinks: GuildContextMenuLinks; }; diff --git a/backend/src/plugins/Counters/CountersPlugin.ts b/backend/src/plugins/Counters/CountersPlugin.ts index 14e3432e..85be2cf5 100644 --- a/backend/src/plugins/Counters/CountersPlugin.ts +++ b/backend/src/plugins/Counters/CountersPlugin.ts @@ -1,15 +1,12 @@ import { EventEmitter } from "events"; import { PluginOptions } from "knub"; -import { - buildCounterConditionString, - CounterTrigger, - getReverseCounterComparisonOp, - parseCounterConditionString, -} from "../../data/entities/CounterTrigger"; import { GuildCounters } from "../../data/GuildCounters"; +import { + CounterTrigger, + parseCounterConditionString +} from "../../data/entities/CounterTrigger"; import { mapToPublicFn } from "../../pluginUtils"; -import { convertDelayStringToMS, MINUTES } from "../../utils"; -import { parseIoTsSchema, StrictValidationError } from "../../validatorUtils"; +import { MINUTES, convertDelayStringToMS, values } from "../../utils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { AddCounterCmd } from "./commands/AddCounterCmd"; import { CountersListCmd } from "./commands/CountersListCmd"; @@ -25,10 +22,8 @@ import { getPrettyNameForCounterTrigger } from "./functions/getPrettyNameForCoun import { offCounterEvent } from "./functions/offCounterEvent"; import { onCounterEvent } from "./functions/onCounterEvent"; import { setCounterValue } from "./functions/setCounterValue"; -import { ConfigSchema, CountersPluginType, TTrigger } from "./types"; +import { CountersPluginType, zCountersConfig } from "./types"; -const MAX_COUNTERS = 5; -const MAX_TRIGGERS_PER_COUNTER = 5; const DECAY_APPLY_INTERVAL = 5 * MINUTES; const defaultOptions: PluginOptions<CountersPluginType> = { @@ -72,50 +67,12 @@ export const CountersPlugin = zeppelinGuildPlugin<CountersPluginType>()({ description: "Keep track of per-user, per-channel, or global numbers and trigger specific actions based on this number", configurationGuide: "See <a href='/docs/setup-guides/counters'>Counters setup guide</a>", - configSchema: ConfigSchema, + configSchema: zCountersConfig, }, defaultOptions, // TODO: Separate input and output types - configParser: (input) => { - for (const [counterName, counter] of Object.entries<any>((input as any).counters || {})) { - counter.name = counterName; - counter.per_user = counter.per_user ?? false; - counter.per_channel = counter.per_channel ?? false; - counter.initial_value = counter.initial_value ?? 0; - counter.triggers = counter.triggers || {}; - - if (Object.values(counter.triggers).length > MAX_TRIGGERS_PER_COUNTER) { - throw new StrictValidationError([`You can only have at most ${MAX_TRIGGERS_PER_COUNTER} triggers per counter`]); - } - - // Normalize triggers - for (const [triggerName, trigger] of Object.entries(counter.triggers)) { - const triggerObj = (typeof trigger === "string" ? { condition: trigger } : trigger) as Partial<TTrigger>; - - triggerObj.name = triggerName; - const parsedCondition = parseCounterConditionString(triggerObj.condition || ""); - if (!parsedCondition) { - throw new StrictValidationError([ - `Invalid comparison in counter trigger ${counterName}/${triggerName}: "${triggerObj.condition}"`, - ]); - } - - triggerObj.condition = buildCounterConditionString(parsedCondition[0], parsedCondition[1]); - triggerObj.reverse_condition = - triggerObj.reverse_condition || - buildCounterConditionString(getReverseCounterComparisonOp(parsedCondition[0]), parsedCondition[1]); - - counter.triggers[triggerName] = triggerObj as TTrigger; - } - } - - if (Object.values((input as any).counters || {}).length > MAX_COUNTERS) { - throw new StrictValidationError([`You can only have at most ${MAX_COUNTERS} counters`]); - } - - return parseIoTsSchema(ConfigSchema, input); - }, + configParser: (input) => zCountersConfig.parse(input), public: { counterExists: mapToPublicFn(counterExists), @@ -163,13 +120,12 @@ export const CountersPlugin = zeppelinGuildPlugin<CountersPluginType>()({ state.counterTriggersByCounterId.set(dbCounter.id, thisCounterTriggers); // Initialize triggers - for (const trigger of Object.values(counter.triggers)) { - const theTrigger = trigger as TTrigger; - const parsedCondition = parseCounterConditionString(theTrigger.condition)!; - const parsedReverseCondition = parseCounterConditionString(theTrigger.reverse_condition)!; + for (const trigger of values(counter.triggers)) { + const parsedCondition = parseCounterConditionString(trigger.condition)!; + const parsedReverseCondition = parseCounterConditionString(trigger.reverse_condition)!; const counterTrigger = await state.counters.initCounterTrigger( dbCounter.id, - theTrigger.name, + trigger.name, parsedCondition[0], parsedCondition[1], parsedReverseCondition[0], diff --git a/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts b/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts index 1445bdd8..5e891c18 100644 --- a/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts +++ b/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts @@ -1,5 +1,5 @@ import { GuildPluginData } from "knub"; -import { CountersPluginType, TTrigger } from "../types"; +import { CountersPluginType } from "../types"; export function getPrettyNameForCounterTrigger( pluginData: GuildPluginData<CountersPluginType>, @@ -12,6 +12,6 @@ export function getPrettyNameForCounterTrigger( return "Unknown Counter Trigger"; } - const trigger = counter.triggers[triggerName] as TTrigger | undefined; + const trigger = counter.triggers[triggerName]; return trigger ? trigger.pretty_name || trigger.name : "Unknown Counter Trigger"; } diff --git a/backend/src/plugins/Counters/types.ts b/backend/src/plugins/Counters/types.ts index 4187c9e4..2b8cce16 100644 --- a/backend/src/plugins/Counters/types.ts +++ b/backend/src/plugins/Counters/types.ts @@ -1,45 +1,98 @@ import { EventEmitter } from "events"; -import * as t from "io-ts"; import { BasePluginType } from "knub"; +import z from "zod"; import { GuildCounters } from "../../data/GuildCounters"; -import { CounterTrigger } from "../../data/entities/CounterTrigger"; -import { tDelayString, tNullable } from "../../utils"; +import { CounterTrigger, buildCounterConditionString, getReverseCounterComparisonOp, parseCounterConditionString } from "../../data/entities/CounterTrigger"; +import { zBoundedCharacters, zBoundedRecord, zDelayString } from "../../utils"; import Timeout = NodeJS.Timeout; -export const Trigger = t.type({ - name: t.string, - pretty_name: tNullable(t.string), - condition: t.string, - reverse_condition: t.string, -}); -export type TTrigger = t.TypeOf<typeof Trigger>; +const MAX_COUNTERS = 5; +const MAX_TRIGGERS_PER_COUNTER = 5; -export const Counter = t.type({ - name: t.string, - pretty_name: tNullable(t.string), - per_channel: t.boolean, - per_user: t.boolean, - initial_value: t.number, - triggers: t.record(t.string, t.union([t.string, Trigger])), - decay: tNullable( - t.type({ - amount: t.number, - every: tDelayString, - }), +export const zTrigger = z.strictObject({ + // Dummy type because name gets replaced by the property key in zTriggerInput + 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" }, + ), + reverse_condition: zBoundedCharacters(1, 64).refine( + (str) => parseCounterConditionString(str) !== null, + { message: "Invalid counter trigger reverse condition" }, ), - can_view: tNullable(t.boolean), - can_edit: tNullable(t.boolean), - can_reset_all: tNullable(t.boolean), }); -export type TCounter = t.TypeOf<typeof Counter>; -export const ConfigSchema = t.type({ - counters: t.record(t.string, Counter), - can_view: t.boolean, - can_edit: t.boolean, - can_reset_all: t.boolean, +const zTriggerInput = z.union([zBoundedCharacters(0, 100), zTrigger]) + .transform((val, ctx) => { + const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); + if (typeof val === "string") { + const parsedCondition = parseCounterConditionString(val); + if (!parsedCondition) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid counter trigger condition", + }); + return z.NEVER; + } + return { + name: ruleName, + pretty_name: null, + condition: buildCounterConditionString(parsedCondition[0], parsedCondition[1]), + reverse_condition: buildCounterConditionString(getReverseCounterComparisonOp(parsedCondition[0]), parsedCondition[1]), + }; + } + return { + ...val, + name: ruleName, + }; + }); + +export const zCounter = z.strictObject({ + // Typed as "never" because you are not expected to supply this directly. + // The transform instead picks it up from the property key and the output type is a string. + name: z.never().optional().transform((_, ctx) => { + const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); + if (! ruleName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Counters must have names", + }); + return z.NEVER; + } + return ruleName; + }), + pretty_name: zBoundedCharacters(0, 100).nullable().default(null), + per_channel: z.boolean().default(false), + per_user: z.boolean().default(false), + initial_value: z.number().default(0), + triggers: zBoundedRecord( + z.record( + zBoundedCharacters(0, 100), + zTriggerInput, + ), + 1, + MAX_TRIGGERS_PER_COUNTER, + ), + decay: z.strictObject({ + amount: z.number(), + every: zDelayString, + }).nullable().default(null), + can_view: z.boolean(), + can_edit: z.boolean(), + can_reset_all: z.boolean(), +}); + +export const zCountersConfig = z.strictObject({ + counters: zBoundedRecord( + z.record(zBoundedCharacters(0, 100), zCounter), + 0, + MAX_COUNTERS, + ), + can_view: z.boolean(), + can_edit: z.boolean(), + can_reset_all: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface CounterEvents { trigger: (counterName: string, triggerName: string, channelId: string | null, userId: string | null) => void; @@ -52,7 +105,7 @@ export interface CounterEventEmitter extends EventEmitter { } export interface CountersPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zCountersConfig>; state: { counters: GuildCounters; counterIds: Record<string, number>; diff --git a/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts b/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts index 6d5ba2ba..e17cd96b 100644 --- a/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts +++ b/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts @@ -2,7 +2,6 @@ import { GuildChannel, GuildMember, User } from "discord.js"; import { guildPluginMessageCommand, parseSignature } from "knub"; import { TSignature } from "knub-command-manager"; import { commandTypes } from "../../commandTypes"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { TemplateSafeValueContainer, createTypedTemplateSafeValueContainer } from "../../templateFormatter"; import { UnknownUser } from "../../utils"; import { isScalar } from "../../utils/isScalar"; @@ -14,7 +13,7 @@ import { } from "../../utils/templateSafeObjects"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { runEvent } from "./functions/runEvent"; -import { ConfigSchema, CustomEventsPluginType } from "./types"; +import { CustomEventsPluginType, zCustomEventsConfig } from "./types"; const defaultOptions = { config: { @@ -26,7 +25,7 @@ export const CustomEventsPlugin = zeppelinGuildPlugin<CustomEventsPluginType>()( name: "custom_events", showInDocs: false, - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zCustomEventsConfig.parse(input), defaultOptions, afterLoad(pluginData) { diff --git a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts index 29ded857..f0870958 100644 --- a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts +++ b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts @@ -1,17 +1,17 @@ -import * as t from "io-ts"; import { GuildPluginData } from "knub"; +import z from "zod"; import { canActOn } from "../../../pluginUtils"; import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter"; -import { resolveMember } from "../../../utils"; +import { resolveMember, zSnowflake } from "../../../utils"; import { ActionError } from "../ActionError"; import { CustomEventsPluginType, TCustomEvent } from "../types"; -export const AddRoleAction = t.type({ - type: t.literal("add_role"), - target: t.string, - role: t.union([t.string, t.array(t.string)]), +export const zAddRoleAction = z.strictObject({ + type: z.literal("add_role"), + target: zSnowflake, + role: z.union([zSnowflake, z.array(zSnowflake)]), }); -export type TAddRoleAction = t.TypeOf<typeof AddRoleAction>; +export type TAddRoleAction = z.infer<typeof zAddRoleAction>; export async function addRoleAction( pluginData: GuildPluginData<CustomEventsPluginType>, diff --git a/backend/src/plugins/CustomEvents/actions/createCaseAction.ts b/backend/src/plugins/CustomEvents/actions/createCaseAction.ts index d8d766b5..e894a446 100644 --- a/backend/src/plugins/CustomEvents/actions/createCaseAction.ts +++ b/backend/src/plugins/CustomEvents/actions/createCaseAction.ts @@ -1,19 +1,20 @@ -import * as t from "io-ts"; import { GuildPluginData } from "knub"; +import z from "zod"; import { CaseTypes } from "../../../data/CaseTypes"; import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter"; +import { zBoundedCharacters, zSnowflake } from "../../../utils"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { ActionError } from "../ActionError"; import { CustomEventsPluginType, TCustomEvent } from "../types"; -export const CreateCaseAction = t.type({ - type: t.literal("create_case"), - case_type: t.string, - mod: t.string, - target: t.string, - reason: t.string, +export const zCreateCaseAction = z.strictObject({ + type: z.literal("create_case"), + case_type: zBoundedCharacters(0, 32), + mod: zSnowflake, + target: zSnowflake, + reason: zBoundedCharacters(0, 4000), }); -export type TCreateCaseAction = t.TypeOf<typeof CreateCaseAction>; +export type TCreateCaseAction = z.infer<typeof zCreateCaseAction>; export async function createCaseAction( pluginData: GuildPluginData<CustomEventsPluginType>, @@ -32,7 +33,7 @@ export async function createCaseAction( } const casesPlugin = pluginData.getPlugin(CasesPlugin); - await casesPlugin.createCase({ + await casesPlugin!.createCase({ userId: targetId, modId, type: CaseTypes[action.case_type], diff --git a/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts b/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts index bf314429..7727e339 100644 --- a/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts +++ b/backend/src/plugins/CustomEvents/actions/makeRoleMentionableAction.ts @@ -1,17 +1,17 @@ import { Snowflake } from "discord.js"; -import * as t from "io-ts"; import { GuildPluginData } from "knub"; +import z from "zod"; import { TemplateSafeValueContainer } from "../../../templateFormatter"; -import { convertDelayStringToMS, noop, tDelayString } from "../../../utils"; +import { convertDelayStringToMS, noop, zDelayString, zSnowflake } from "../../../utils"; import { ActionError } from "../ActionError"; import { CustomEventsPluginType, TCustomEvent } from "../types"; -export const MakeRoleMentionableAction = t.type({ - type: t.literal("make_role_mentionable"), - role: t.string, - timeout: tDelayString, +export const zMakeRoleMentionableAction = z.strictObject({ + type: z.literal("make_role_mentionable"), + role: zSnowflake, + timeout: zDelayString, }); -export type TMakeRoleMentionableAction = t.TypeOf<typeof MakeRoleMentionableAction>; +export type TMakeRoleMentionableAction = z.infer<typeof zMakeRoleMentionableAction>; export async function makeRoleMentionableAction( pluginData: GuildPluginData<CustomEventsPluginType>, diff --git a/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts b/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts index e86d03b5..8dca7323 100644 --- a/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts +++ b/backend/src/plugins/CustomEvents/actions/makeRoleUnmentionableAction.ts @@ -1,15 +1,16 @@ import { Snowflake } from "discord.js"; -import * as t from "io-ts"; import { GuildPluginData } from "knub"; +import z from "zod"; import { TemplateSafeValueContainer } from "../../../templateFormatter"; +import { zSnowflake } from "../../../utils"; import { ActionError } from "../ActionError"; import { CustomEventsPluginType, TCustomEvent } from "../types"; -export const MakeRoleUnmentionableAction = t.type({ - type: t.literal("make_role_unmentionable"), - role: t.string, +export const zMakeRoleUnmentionableAction = z.strictObject({ + type: z.literal("make_role_unmentionable"), + role: zSnowflake, }); -export type TMakeRoleUnmentionableAction = t.TypeOf<typeof MakeRoleUnmentionableAction>; +export type TMakeRoleUnmentionableAction = z.infer<typeof zMakeRoleUnmentionableAction>; export async function makeRoleUnmentionableAction( pluginData: GuildPluginData<CustomEventsPluginType>, diff --git a/backend/src/plugins/CustomEvents/actions/messageAction.ts b/backend/src/plugins/CustomEvents/actions/messageAction.ts index f06534ad..40eee4b8 100644 --- a/backend/src/plugins/CustomEvents/actions/messageAction.ts +++ b/backend/src/plugins/CustomEvents/actions/messageAction.ts @@ -1,16 +1,17 @@ import { Snowflake, TextChannel } from "discord.js"; -import * as t from "io-ts"; import { GuildPluginData } from "knub"; +import z from "zod"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; +import { zBoundedCharacters, zSnowflake } from "../../../utils"; import { ActionError } from "../ActionError"; import { CustomEventsPluginType } from "../types"; -export const MessageAction = t.type({ - type: t.literal("message"), - channel: t.string, - content: t.string, +export const zMessageAction = z.strictObject({ + type: z.literal("message"), + channel: zSnowflake, + content: zBoundedCharacters(0, 4000), }); -export type TMessageAction = t.TypeOf<typeof MessageAction>; +export type TMessageAction = z.infer<typeof zMessageAction>; export async function messageAction( pluginData: GuildPluginData<CustomEventsPluginType>, diff --git a/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts b/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts index 8ef746cd..d42059f5 100644 --- a/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts +++ b/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts @@ -1,18 +1,18 @@ import { Snowflake, VoiceChannel } from "discord.js"; -import * as t from "io-ts"; import { GuildPluginData } from "knub"; +import z from "zod"; import { canActOn } from "../../../pluginUtils"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; -import { resolveMember } from "../../../utils"; +import { resolveMember, zSnowflake } from "../../../utils"; import { ActionError } from "../ActionError"; import { CustomEventsPluginType, TCustomEvent } from "../types"; -export const MoveToVoiceChannelAction = t.type({ - type: t.literal("move_to_vc"), - target: t.string, - channel: t.string, +export const zMoveToVoiceChannelAction = z.strictObject({ + type: z.literal("move_to_vc"), + target: zSnowflake, + channel: zSnowflake, }); -export type TMoveToVoiceChannelAction = t.TypeOf<typeof MoveToVoiceChannelAction>; +export type TMoveToVoiceChannelAction = z.infer<typeof zMoveToVoiceChannelAction>; export async function moveToVoiceChannelAction( pluginData: GuildPluginData<CustomEventsPluginType>, diff --git a/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts b/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts index 6bae4c35..dbd7b932 100644 --- a/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts +++ b/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts @@ -1,23 +1,24 @@ import { PermissionsBitField, PermissionsString, Snowflake } from "discord.js"; -import * as t from "io-ts"; import { GuildPluginData } from "knub"; +import z from "zod"; import { TemplateSafeValueContainer } from "../../../templateFormatter"; +import { zSnowflake } from "../../../utils"; import { ActionError } from "../ActionError"; import { CustomEventsPluginType, TCustomEvent } from "../types"; -export const SetChannelPermissionOverridesAction = t.type({ - type: t.literal("set_channel_permission_overrides"), - channel: t.string, - overrides: t.array( - t.type({ - type: t.union([t.literal("member"), t.literal("role")]), - id: t.string, - allow: t.number, - deny: t.number, +export const zSetChannelPermissionOverridesAction = z.strictObject({ + type: z.literal("set_channel_permission_overrides"), + channel: zSnowflake, + overrides: z.array( + z.strictObject({ + type: z.union([z.literal("member"), z.literal("role")]), + id: zSnowflake, + allow: z.number(), + deny: z.number(), }), - ), + ).max(15), }); -export type TSetChannelPermissionOverridesAction = t.TypeOf<typeof SetChannelPermissionOverridesAction>; +export type TSetChannelPermissionOverridesAction = z.infer<typeof zSetChannelPermissionOverridesAction>; export async function setChannelPermissionOverridesAction( pluginData: GuildPluginData<CustomEventsPluginType>, diff --git a/backend/src/plugins/CustomEvents/types.ts b/backend/src/plugins/CustomEvents/types.ts index e273e7b6..4572b1cf 100644 --- a/backend/src/plugins/CustomEvents/types.ts +++ b/backend/src/plugins/CustomEvents/types.ts @@ -1,47 +1,50 @@ -import * as t from "io-ts"; import { BasePluginType } from "knub"; -import { AddRoleAction } from "./actions/addRoleAction"; -import { CreateCaseAction } from "./actions/createCaseAction"; -import { MakeRoleMentionableAction } from "./actions/makeRoleMentionableAction"; -import { MakeRoleUnmentionableAction } from "./actions/makeRoleUnmentionableAction"; -import { MessageAction } from "./actions/messageAction"; -import { MoveToVoiceChannelAction } from "./actions/moveToVoiceChannelAction"; -import { SetChannelPermissionOverridesAction } from "./actions/setChannelPermissionOverrides"; +import z from "zod"; +import { zBoundedCharacters, zBoundedRecord } from "../../utils"; +import { zAddRoleAction } from "./actions/addRoleAction"; +import { zCreateCaseAction } from "./actions/createCaseAction"; +import { zMakeRoleMentionableAction } from "./actions/makeRoleMentionableAction"; +import { zMakeRoleUnmentionableAction } from "./actions/makeRoleUnmentionableAction"; +import { zMessageAction } from "./actions/messageAction"; +import { zMoveToVoiceChannelAction } from "./actions/moveToVoiceChannelAction"; +import { zSetChannelPermissionOverridesAction } from "./actions/setChannelPermissionOverrides"; -// Triggers -const CommandTrigger = t.type({ - type: t.literal("command"), - name: t.string, - params: t.string, - can_use: t.boolean, +const zCommandTrigger = z.strictObject({ + type: z.literal("command"), + name: zBoundedCharacters(0, 100), + params: zBoundedCharacters(0, 255), + can_use: z.boolean(), }); -const AnyTrigger = CommandTrigger; // TODO: Make into a union once we have more triggers +const zAnyTrigger = zCommandTrigger; // TODO: Make into a union once we have more triggers -const AnyAction = t.union([ - AddRoleAction, - CreateCaseAction, - MoveToVoiceChannelAction, - MessageAction, - MakeRoleMentionableAction, - MakeRoleUnmentionableAction, - SetChannelPermissionOverridesAction, +const zAnyAction = z.union([ + zAddRoleAction, + zCreateCaseAction, + zMoveToVoiceChannelAction, + zMessageAction, + zMakeRoleMentionableAction, + zMakeRoleUnmentionableAction, + zSetChannelPermissionOverridesAction, ]); -export const CustomEvent = t.type({ - name: t.string, - trigger: AnyTrigger, - actions: t.array(AnyAction), +export const zCustomEvent = z.strictObject({ + name: zBoundedCharacters(0, 100), + trigger: zAnyTrigger, + actions: z.array(zAnyAction).max(10), }); -export type TCustomEvent = t.TypeOf<typeof CustomEvent>; +export type TCustomEvent = z.infer<typeof zCustomEvent>; -export const ConfigSchema = t.type({ - events: t.record(t.string, CustomEvent), +export const zCustomEventsConfig = z.strictObject({ + events: zBoundedRecord( + z.record(zBoundedCharacters(0, 100), zCustomEvent), + 0, + 100, + ), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface CustomEventsPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zCustomEventsConfig>; state: { clearTriggers: () => void; }; diff --git a/backend/src/plugins/GuildAccessMonitor/GuildAccessMonitorPlugin.ts b/backend/src/plugins/GuildAccessMonitor/GuildAccessMonitorPlugin.ts index c4f7eaaf..b8639134 100644 --- a/backend/src/plugins/GuildAccessMonitor/GuildAccessMonitorPlugin.ts +++ b/backend/src/plugins/GuildAccessMonitor/GuildAccessMonitorPlugin.ts @@ -1,11 +1,10 @@ import { Guild } from "discord.js"; -import * as t from "io-ts"; import { BasePluginType, GlobalPluginData, globalPluginEventListener } from "knub"; import { AllowedGuilds } from "../../data/AllowedGuilds"; import { Configs } from "../../data/Configs"; import { env } from "../../env"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { zeppelinGlobalPlugin } from "../ZeppelinPluginBlueprint"; +import z from "zod"; interface GuildAccessMonitorPluginType extends BasePluginType { state: { @@ -26,7 +25,7 @@ async function checkGuild(pluginData: GlobalPluginData<GuildAccessMonitorPluginT */ export const GuildAccessMonitorPlugin = zeppelinGlobalPlugin<GuildAccessMonitorPluginType>()({ name: "guild_access_monitor", - configParser: makeIoTsConfigParser(t.type({})), + configParser: (input) => z.strictObject({}).parse(input), events: [ globalPluginEventListener<GuildAccessMonitorPluginType>()({ diff --git a/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts b/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts index 5ae04fb5..6fdc467a 100644 --- a/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts +++ b/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts @@ -1,6 +1,5 @@ -import * as t from "io-ts"; +import z from "zod"; import { Configs } from "../../data/Configs"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { zeppelinGlobalPlugin } from "../ZeppelinPluginBlueprint"; import { reloadChangedGuilds } from "./functions/reloadChangedGuilds"; import { GuildConfigReloaderPluginType } from "./types"; @@ -9,7 +8,7 @@ export const GuildConfigReloaderPlugin = zeppelinGlobalPlugin<GuildConfigReloade name: "guild_config_reloader", showInDocs: false, - configParser: makeIoTsConfigParser(t.type({})), + configParser: (input) => z.strictObject({}).parse(input), async beforeLoad(pluginData) { const { state } = pluginData; diff --git a/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts b/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts index 687175f1..a8d71e06 100644 --- a/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts +++ b/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts @@ -1,18 +1,17 @@ import { Guild } from "discord.js"; -import * as t from "io-ts"; import { guildPluginEventListener } from "knub"; import { AllowedGuilds } from "../../data/AllowedGuilds"; import { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { MINUTES } from "../../utils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { GuildInfoSaverPluginType } from "./types"; +import z from "zod"; export const GuildInfoSaverPlugin = zeppelinGuildPlugin<GuildInfoSaverPluginType>()({ name: "guild_info_saver", showInDocs: false, - configParser: makeIoTsConfigParser(t.type({})), + configParser: (input) => z.strictObject({}).parse(input), events: [ guildPluginEventListener({ diff --git a/backend/src/plugins/GuildMemberCache/GuildMemberCachePlugin.ts b/backend/src/plugins/GuildMemberCache/GuildMemberCachePlugin.ts index d29b342d..293d582e 100644 --- a/backend/src/plugins/GuildMemberCache/GuildMemberCachePlugin.ts +++ b/backend/src/plugins/GuildMemberCache/GuildMemberCachePlugin.ts @@ -1,6 +1,6 @@ -import * as t from "io-ts"; +import z from "zod"; import { GuildMemberCache } from "../../data/GuildMemberCache"; -import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils"; +import { mapToPublicFn } from "../../pluginUtils"; import { SECONDS } from "../../utils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { cancelDeletionOnMemberJoin } from "./events/cancelDeletionOnMemberJoin"; @@ -18,7 +18,7 @@ export const GuildMemberCachePlugin = zeppelinGuildPlugin<GuildMemberCachePlugin name: "guild_member_cache", showInDocs: false, - configParser: makeIoTsConfigParser(t.type({})), + configParser: (input) => z.strictObject({}).parse(input), events: [ updateMemberCacheOnMemberUpdate, diff --git a/backend/src/plugins/InternalPoster/InternalPosterPlugin.ts b/backend/src/plugins/InternalPoster/InternalPosterPlugin.ts index 3fbcbbf8..9365109e 100644 --- a/backend/src/plugins/InternalPoster/InternalPosterPlugin.ts +++ b/backend/src/plugins/InternalPoster/InternalPosterPlugin.ts @@ -1,11 +1,12 @@ import { PluginOptions } from "knub"; +import z from "zod"; import { Queue } from "../../Queue"; import { Webhooks } from "../../data/Webhooks"; -import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils"; +import { mapToPublicFn } from "../../pluginUtils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { editMessage } from "./functions/editMessage"; import { sendMessage } from "./functions/sendMessage"; -import { ConfigSchema, InternalPosterPluginType } from "./types"; +import { InternalPosterPluginType } from "./types"; const defaultOptions: PluginOptions<InternalPosterPluginType> = { config: {}, @@ -16,7 +17,7 @@ export const InternalPosterPlugin = zeppelinGuildPlugin<InternalPosterPluginType name: "internal_poster", showInDocs: false, - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => z.strictObject({}).parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/InternalPoster/types.ts b/backend/src/plugins/InternalPoster/types.ts index e5f1a1a3..78dabd42 100644 --- a/backend/src/plugins/InternalPoster/types.ts +++ b/backend/src/plugins/InternalPoster/types.ts @@ -1,15 +1,9 @@ import { WebhookClient } from "discord.js"; -import * as t from "io-ts"; import { BasePluginType } from "knub"; import { Queue } from "../../Queue"; import { Webhooks } from "../../data/Webhooks"; -export const ConfigSchema = t.type({}); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; - export interface InternalPosterPluginType extends BasePluginType { - config: TConfigSchema; - state: { queue: Queue; webhooks: Webhooks; diff --git a/backend/src/plugins/LocateUser/LocateUserPlugin.ts b/backend/src/plugins/LocateUser/LocateUserPlugin.ts index 51d47945..a99dc84c 100644 --- a/backend/src/plugins/LocateUser/LocateUserPlugin.ts +++ b/backend/src/plugins/LocateUser/LocateUserPlugin.ts @@ -1,7 +1,6 @@ import { PluginOptions } from "knub"; import { onGuildEvent } from "../../data/GuildEvents"; import { GuildVCAlerts } from "../../data/GuildVCAlerts"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { trimPluginDescription } from "../../utils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { FollowCmd } from "./commands/FollowCmd"; @@ -9,7 +8,7 @@ import { DeleteFollowCmd, ListFollowCmd } from "./commands/ListFollowCmd"; import { WhereCmd } from "./commands/WhereCmd"; import { GuildBanRemoveAlertsEvt } from "./events/BanRemoveAlertsEvt"; import { VoiceStateUpdateAlertEvt } from "./events/SendAlertsEvts"; -import { ConfigSchema, LocateUserPluginType } from "./types"; +import { LocateUserPluginType, zLocateUserConfig } from "./types"; import { clearExpiredAlert } from "./utils/clearExpiredAlert"; import { fillActiveAlertsList } from "./utils/fillAlertsList"; @@ -39,10 +38,10 @@ export const LocateUserPlugin = zeppelinGuildPlugin<LocateUserPluginType>()({ * Instantly receive an invite to the voice channel of a user * Be notified as soon as a user switches or joins a voice channel `), - configSchema: ConfigSchema, + configSchema: zLocateUserConfig, }, - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zLocateUserConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/LocateUser/types.ts b/backend/src/plugins/LocateUser/types.ts index 1bfb063e..4139f465 100644 --- a/backend/src/plugins/LocateUser/types.ts +++ b/backend/src/plugins/LocateUser/types.ts @@ -1,15 +1,14 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildVCAlerts } from "../../data/GuildVCAlerts"; -export const ConfigSchema = t.type({ - can_where: t.boolean, - can_alert: t.boolean, +export const zLocateUserConfig = z.strictObject({ + can_where: z.boolean(), + can_alert: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface LocateUserPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zLocateUserConfig>; state: { alerts: GuildVCAlerts; usersWithAlerts: string[]; diff --git a/backend/src/plugins/Logs/LogsPlugin.ts b/backend/src/plugins/Logs/LogsPlugin.ts index e56ba756..923ac623 100644 --- a/backend/src/plugins/Logs/LogsPlugin.ts +++ b/backend/src/plugins/Logs/LogsPlugin.ts @@ -6,7 +6,7 @@ import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { LogType } from "../../data/LogType"; import { logger } from "../../logger"; -import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils"; +import { mapToPublicFn } from "../../pluginUtils"; import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { TypedTemplateSafeValueContainer, createTypedTemplateSafeValueContainer } from "../../templateFormatter"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; @@ -31,7 +31,7 @@ import { import { LogsThreadCreateEvt, LogsThreadDeleteEvt, LogsThreadUpdateEvt } from "./events/LogsThreadModifyEvts"; import { LogsGuildMemberUpdateEvt } from "./events/LogsUserUpdateEvts"; import { LogsVoiceStateUpdateEvt } from "./events/LogsVoiceChannelEvts"; -import { ConfigSchema, FORMAT_NO_TIMESTAMP, ILogTypeData, LogsPluginType, TLogChannel } from "./types"; +import { FORMAT_NO_TIMESTAMP, ILogTypeData, LogsPluginType, TLogChannel, zLogsConfig } from "./types"; import { getLogMessage } from "./util/getLogMessage"; import { log } from "./util/log"; import { onMessageDelete } from "./util/onMessageDelete"; @@ -110,7 +110,6 @@ import { logVoiceChannelForceMove } from "./logFunctions/logVoiceChannelForceMov import { logVoiceChannelJoin } from "./logFunctions/logVoiceChannelJoin"; import { logVoiceChannelLeave } from "./logFunctions/logVoiceChannelLeave"; import { logVoiceChannelMove } from "./logFunctions/logVoiceChannelMove"; -import { asBoundedString } from "../../utils/iotsUtils"; // The `any` cast here is to prevent TypeScript from locking up from the circular dependency function getCasesPlugin(): Promise<any> { @@ -121,12 +120,12 @@ const defaultOptions: PluginOptions<LogsPluginType> = { config: { channels: {}, format: { - timestamp: asBoundedString(FORMAT_NO_TIMESTAMP), // Legacy/deprecated, use timestamp_format below instead + timestamp: FORMAT_NO_TIMESTAMP, ...DefaultLogMessages, }, - ping_user: true, // Legacy/deprecated, if below is false mentions wont actually ping. In case you really want the old behavior, set below to true + ping_user: true, allow_user_mentions: false, - timestamp_format: asBoundedString("[<t:]X[>]"), + timestamp_format: "[<t:]X[>]", include_embed_timestamp: true, }, @@ -134,7 +133,8 @@ const defaultOptions: PluginOptions<LogsPluginType> = { { level: ">=50", config: { - ping_user: false, // Legacy/deprecated, read comment on global ping_user option + // Legacy/deprecated, read comment on global ping_user option + ping_user: false, }, }, ], @@ -145,11 +145,11 @@ export const LogsPlugin = zeppelinGuildPlugin<LogsPluginType>()({ showInDocs: true, info: { prettyName: "Logs", - configSchema: ConfigSchema, + configSchema: zLogsConfig, }, dependencies: async () => [TimeAndDatePlugin, InternalPosterPlugin, (await getCasesPlugin()).CasesPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zLogsConfig.parse(input), defaultOptions, events: [ diff --git a/backend/src/plugins/Logs/logFunctions/logMessageDelete.ts b/backend/src/plugins/Logs/logFunctions/logMessageDelete.ts index 0596869c..25c8c340 100644 --- a/backend/src/plugins/Logs/logFunctions/logMessageDelete.ts +++ b/backend/src/plugins/Logs/logFunctions/logMessageDelete.ts @@ -32,7 +32,7 @@ export function logMessageDelete(pluginData: GuildPluginData<LogsPluginType>, da // See comment on FORMAT_NO_TIMESTAMP in types.ts const config = pluginData.config.get(); const timestampFormat = - (config.format.timestamp !== FORMAT_NO_TIMESTAMP ? config.format.timestamp : null) ?? config.timestamp_format; + (config.format.timestamp !== FORMAT_NO_TIMESTAMP ? config.format.timestamp : null) ?? config.timestamp_format ?? undefined; return log( pluginData, diff --git a/backend/src/plugins/Logs/types.ts b/backend/src/plugins/Logs/types.ts index 759f680d..51f67b87 100644 --- a/backend/src/plugins/Logs/types.ts +++ b/backend/src/plugins/Logs/types.ts @@ -1,4 +1,3 @@ -import * as t from "io-ts"; import { BasePluginType, CooldownManager, guildPluginEventListener } from "knub"; import { z } from "zod"; import { RegExpRunner } from "../../RegExpRunner"; @@ -7,7 +6,7 @@ import { GuildCases } from "../../data/GuildCases"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { LogType } from "../../data/LogType"; -import { tMessageContent, tNullable } from "../../utils"; +import { zBoundedCharacters, zMessageContent, zRegex, zSnowflake } from "../../utils"; import { MessageBuffer } from "../../utils/MessageBuffer"; import { TemplateSafeCase, @@ -22,55 +21,56 @@ import { TemplateSafeUnknownUser, TemplateSafeUser, } from "../../utils/templateSafeObjects"; -import { TRegex } from "../../validatorUtils"; -import { tBoundedString } from "../../utils/iotsUtils"; -export const tLogFormats = t.record(t.string, t.union([t.string, tMessageContent])); -export type TLogFormats = t.TypeOf<typeof tLogFormats>; +const DEFAULT_BATCH_TIME = 1000; +const MIN_BATCH_TIME = 250; +const MAX_BATCH_TIME = 5000; -const LogChannel = t.partial({ - include: t.array(t.string), - exclude: t.array(t.string), - batched: t.boolean, - batch_time: t.number, - excluded_users: t.array(t.string), - excluded_message_regexes: t.array(TRegex), - excluded_channels: t.array(t.string), - excluded_categories: t.array(t.string), - excluded_threads: t.array(t.string), - exclude_bots: t.boolean, - excluded_roles: t.array(t.string), - format: tNullable(tLogFormats), - timestamp_format: t.string, - include_embed_timestamp: t.boolean, +export const zLogFormats = z.record( + zBoundedCharacters(1, 255), + zMessageContent, +); +export type TLogFormats = z.infer<typeof zLogFormats>; + +const zLogChannel = z.strictObject({ + include: z.array(zBoundedCharacters(1, 255)).default([]), + exclude: z.array(zBoundedCharacters(1, 255)).default([]), + batched: z.boolean().default(true), + batch_time: z.number().min(MIN_BATCH_TIME).max(MAX_BATCH_TIME).default(DEFAULT_BATCH_TIME), + excluded_users: z.array(zSnowflake).nullable().default(null), + excluded_message_regexes: z.array(zRegex(z.string())).nullable().default(null), + excluded_channels: z.array(zSnowflake).nullable().default(null), + excluded_categories: z.array(zSnowflake).nullable().default(null), + 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({}), + timestamp_format: z.string().nullable().default(null), + include_embed_timestamp: z.boolean().nullable().default(null), }); -export type TLogChannel = t.TypeOf<typeof LogChannel>; +export type TLogChannel = z.infer<typeof zLogChannel>; -const LogChannelMap = t.record(t.string, LogChannel); -export type TLogChannelMap = t.TypeOf<typeof LogChannelMap>; +const zLogChannelMap = z.record(zSnowflake, zLogChannel); +export type TLogChannelMap = z.infer<typeof zLogChannelMap>; -export const ConfigSchema = t.type({ - channels: LogChannelMap, - format: t.intersection([ - tLogFormats, - t.type({ - timestamp: tBoundedString(0, 64), // Legacy/deprecated - }), - ]), - ping_user: t.boolean, // Legacy/deprecated, if below is false mentions wont actually ping - allow_user_mentions: t.boolean, - timestamp_format: tBoundedString(0, 64), - include_embed_timestamp: t.boolean, +export const zLogsConfig = z.strictObject({ + channels: zLogChannelMap, + format: z.intersection(zLogFormats, z.strictObject({ + // Legacy/deprecated, use timestamp_format below instead + timestamp: zBoundedCharacters(0, 64).nullable(), + })), + // Legacy/deprecated, if below is false mentions wont actually ping. In case you really want the old behavior, set below to true + ping_user: z.boolean(), + allow_user_mentions: z.boolean(), + timestamp_format: z.string().nullable(), + include_embed_timestamp: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; -// Hacky way of allowing a """null""" default value for config.format.timestamp -// The type cannot be made nullable properly because io-ts's intersection type still considers -// that it has to match the record type of tLogFormats, which includes string. +// Hacky way of allowing a """null""" default value for config.format.timestamp due to legacy io-ts reasons export const FORMAT_NO_TIMESTAMP = "__NO_TIMESTAMP__"; export interface LogsPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zLogsConfig>; state: { guildLogs: GuildLogs; savedMessages: GuildSavedMessages; diff --git a/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts b/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts index 9e775251..0b140cd7 100644 --- a/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts +++ b/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts @@ -1,11 +1,10 @@ import { PluginOptions } from "knub"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { SaveMessagesToDBCmd } from "./commands/SaveMessagesToDB"; import { SavePinsToDBCmd } from "./commands/SavePinsToDB"; import { MessageCreateEvt, MessageDeleteBulkEvt, MessageDeleteEvt, MessageUpdateEvt } from "./events/SaveMessagesEvts"; -import { ConfigSchema, MessageSaverPluginType } from "./types"; +import { MessageSaverPluginType, zMessageSaverConfig } from "./types"; const defaultOptions: PluginOptions<MessageSaverPluginType> = { config: { @@ -25,7 +24,7 @@ export const MessageSaverPlugin = zeppelinGuildPlugin<MessageSaverPluginType>()( name: "message_saver", showInDocs: false, - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zMessageSaverConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/MessageSaver/types.ts b/backend/src/plugins/MessageSaver/types.ts index 2fdef665..f42fa2c3 100644 --- a/backend/src/plugins/MessageSaver/types.ts +++ b/backend/src/plugins/MessageSaver/types.ts @@ -1,14 +1,13 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -export const ConfigSchema = t.type({ - can_manage: t.boolean, +export const zMessageSaverConfig = z.strictObject({ + can_manage: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface MessageSaverPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zMessageSaverConfig>; state: { savedMessages: GuildSavedMessages; }; diff --git a/backend/src/plugins/ModActions/ModActionsPlugin.ts b/backend/src/plugins/ModActions/ModActionsPlugin.ts index bf83b13e..4c9c8958 100644 --- a/backend/src/plugins/ModActions/ModActionsPlugin.ts +++ b/backend/src/plugins/ModActions/ModActionsPlugin.ts @@ -6,7 +6,7 @@ import { onGuildEvent } from "../../data/GuildEvents"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildMutes } from "../../data/GuildMutes"; import { GuildTempbans } from "../../data/GuildTempbans"; -import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils"; +import { mapToPublicFn } from "../../pluginUtils"; import { MINUTES, trimPluginDescription } from "../../utils"; import { CasesPlugin } from "../Cases/CasesPlugin"; import { LogsPlugin } from "../Logs/LogsPlugin"; @@ -47,7 +47,7 @@ import { offModActionsEvent } from "./functions/offModActionsEvent"; import { onModActionsEvent } from "./functions/onModActionsEvent"; import { updateCase } from "./functions/updateCase"; import { warnMember } from "./functions/warnMember"; -import { BanOptions, ConfigSchema, KickOptions, ModActionsPluginType, WarnOptions } from "./types"; +import { BanOptions, KickOptions, ModActionsPluginType, WarnOptions, zModActionsConfig } from "./types"; const defaultOptions = { config: { @@ -121,11 +121,11 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()({ description: trimPluginDescription(` This plugin contains the 'typical' mod actions such as warning, muting, kicking, banning, etc. `), - configSchema: ConfigSchema, + configSchema: zModActionsConfig, }, dependencies: () => [TimeAndDatePlugin, CasesPlugin, MutesPlugin, LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zModActionsConfig.parse(input), defaultOptions, events: [CreateBanCaseOnManualBanEvt, CreateUnbanCaseOnManualUnbanEvt, PostAlertOnMemberJoinEvt, AuditLogEvents], diff --git a/backend/src/plugins/ModActions/types.ts b/backend/src/plugins/ModActions/types.ts index 447b9638..fbe7890a 100644 --- a/backend/src/plugins/ModActions/types.ts +++ b/backend/src/plugins/ModActions/types.ts @@ -1,51 +1,50 @@ import { GuildTextBasedChannel } from "discord.js"; import { EventEmitter } from "events"; -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { Queue } from "../../Queue"; import { GuildCases } from "../../data/GuildCases"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildMutes } from "../../data/GuildMutes"; import { GuildTempbans } from "../../data/GuildTempbans"; import { Case } from "../../data/entities/Case"; -import { UserNotificationMethod, UserNotificationResult, tNullable } from "../../utils"; +import { UserNotificationMethod, UserNotificationResult } from "../../utils"; import { CaseArgs } from "../Cases/types"; -export const ConfigSchema = t.type({ - dm_on_warn: t.boolean, - dm_on_kick: t.boolean, - dm_on_ban: t.boolean, - message_on_warn: t.boolean, - message_on_kick: t.boolean, - message_on_ban: t.boolean, - message_channel: tNullable(t.string), - warn_message: tNullable(t.string), - kick_message: tNullable(t.string), - ban_message: tNullable(t.string), - tempban_message: tNullable(t.string), - alert_on_rejoin: t.boolean, - alert_channel: tNullable(t.string), - warn_notify_enabled: t.boolean, - warn_notify_threshold: t.number, - warn_notify_message: t.string, - ban_delete_message_days: t.number, - can_note: t.boolean, - can_warn: t.boolean, - can_mute: t.boolean, - can_kick: t.boolean, - can_ban: t.boolean, - can_unban: t.boolean, - can_view: t.boolean, - can_addcase: t.boolean, - can_massunban: t.boolean, - can_massban: t.boolean, - can_massmute: t.boolean, - can_hidecase: t.boolean, - can_deletecase: t.boolean, - can_act_as_other: t.boolean, - create_cases_for_manual_actions: t.boolean, +export const zModActionsConfig = z.strictObject({ + dm_on_warn: z.boolean(), + dm_on_kick: z.boolean(), + dm_on_ban: z.boolean(), + message_on_warn: z.boolean(), + message_on_kick: z.boolean(), + message_on_ban: z.boolean(), + message_channel: z.nullable(z.string()), + warn_message: z.nullable(z.string()), + kick_message: z.nullable(z.string()), + ban_message: z.nullable(z.string()), + tempban_message: z.nullable(z.string()), + alert_on_rejoin: z.boolean(), + alert_channel: z.nullable(z.string()), + warn_notify_enabled: z.boolean(), + warn_notify_threshold: z.number(), + warn_notify_message: z.string(), + ban_delete_message_days: z.number(), + can_note: z.boolean(), + can_warn: z.boolean(), + can_mute: z.boolean(), + can_kick: z.boolean(), + can_ban: z.boolean(), + can_unban: z.boolean(), + can_view: z.boolean(), + can_addcase: z.boolean(), + can_massunban: z.boolean(), + can_massban: z.boolean(), + can_massmute: z.boolean(), + can_hidecase: z.boolean(), + can_deletecase: z.boolean(), + can_act_as_other: z.boolean(), + create_cases_for_manual_actions: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface ModActionsEvents { note: (userId: string, reason?: string) => void; @@ -62,7 +61,7 @@ export interface ModActionsEventEmitter extends EventEmitter { } export interface ModActionsPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zModActionsConfig>; state: { mutes: GuildMutes; cases: GuildCases; diff --git a/backend/src/plugins/Mutes/MutesPlugin.ts b/backend/src/plugins/Mutes/MutesPlugin.ts index 1a205bbe..7c9eada8 100644 --- a/backend/src/plugins/Mutes/MutesPlugin.ts +++ b/backend/src/plugins/Mutes/MutesPlugin.ts @@ -5,7 +5,7 @@ import { GuildCases } from "../../data/GuildCases"; import { onGuildEvent } from "../../data/GuildEvents"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildMutes } from "../../data/GuildMutes"; -import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils"; +import { mapToPublicFn } from "../../pluginUtils"; import { CasesPlugin } from "../Cases/CasesPlugin"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; @@ -22,7 +22,7 @@ import { offMutesEvent } from "./functions/offMutesEvent"; import { onMutesEvent } from "./functions/onMutesEvent"; import { renewTimeoutMute } from "./functions/renewTimeoutMute"; import { unmuteUser } from "./functions/unmuteUser"; -import { ConfigSchema, MutesPluginType } from "./types"; +import { MutesPluginType, zMutesConfig } from "./types"; const defaultOptions = { config: { @@ -65,11 +65,11 @@ export const MutesPlugin = zeppelinGuildPlugin<MutesPluginType>()({ showInDocs: true, info: { prettyName: "Mutes", - configSchema: ConfigSchema, + configSchema: zMutesConfig, }, dependencies: () => [CasesPlugin, LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zMutesConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/Mutes/types.ts b/backend/src/plugins/Mutes/types.ts index 0c69c657..e1f266a9 100644 --- a/backend/src/plugins/Mutes/types.ts +++ b/backend/src/plugins/Mutes/types.ts @@ -1,36 +1,35 @@ import { GuildMember } from "discord.js"; import { EventEmitter } from "events"; -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildCases } from "../../data/GuildCases"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildMutes } from "../../data/GuildMutes"; import { Case } from "../../data/entities/Case"; import { Mute } from "../../data/entities/Mute"; -import { UserNotificationMethod, UserNotificationResult, tNullable } from "../../utils"; +import { UserNotificationMethod, UserNotificationResult, zSnowflake } from "../../utils"; import { CaseArgs } from "../Cases/types"; -export const ConfigSchema = t.type({ - mute_role: tNullable(t.string), - move_to_voice_channel: tNullable(t.string), - kick_from_voice_channel: t.boolean, +export const zMutesConfig = z.strictObject({ + mute_role: zSnowflake.nullable(), + move_to_voice_channel: zSnowflake.nullable(), + kick_from_voice_channel: z.boolean(), - dm_on_mute: t.boolean, - dm_on_update: t.boolean, - message_on_mute: t.boolean, - message_on_update: t.boolean, - message_channel: tNullable(t.string), - mute_message: tNullable(t.string), - timed_mute_message: tNullable(t.string), - update_mute_message: tNullable(t.string), - remove_roles_on_mute: t.union([t.boolean, t.array(t.string)]), - restore_roles_on_mute: t.union([t.boolean, t.array(t.string)]), + dm_on_mute: z.boolean(), + dm_on_update: z.boolean(), + message_on_mute: z.boolean(), + message_on_update: z.boolean(), + message_channel: z.string().nullable(), + mute_message: z.string().nullable(), + timed_mute_message: z.string().nullable(), + update_mute_message: z.string().nullable(), + remove_roles_on_mute: z.union([z.boolean(), z.array(zSnowflake)]).default(false), + restore_roles_on_mute: z.union([z.boolean(), z.array(zSnowflake)]).default(false), - can_view_list: t.boolean, - can_cleanup: t.boolean, + can_view_list: z.boolean(), + can_cleanup: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface MutesEvents { mute: (userId: string, reason?: string, isAutomodAction?: boolean) => void; @@ -43,7 +42,7 @@ export interface MutesEventEmitter extends EventEmitter { } export interface MutesPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zMutesConfig>; state: { mutes: GuildMutes; cases: GuildCases; diff --git a/backend/src/plugins/NameHistory/NameHistoryPlugin.ts b/backend/src/plugins/NameHistory/NameHistoryPlugin.ts index 7b9273e3..915ffa3c 100644 --- a/backend/src/plugins/NameHistory/NameHistoryPlugin.ts +++ b/backend/src/plugins/NameHistory/NameHistoryPlugin.ts @@ -2,10 +2,9 @@ import { PluginOptions } from "knub"; import { Queue } from "../../Queue"; import { GuildNicknameHistory } from "../../data/GuildNicknameHistory"; import { UsernameHistory } from "../../data/UsernameHistory"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { NamesCmd } from "./commands/NamesCmd"; -import { ConfigSchema, NameHistoryPluginType } from "./types"; +import { NameHistoryPluginType, zNameHistoryConfig } from "./types"; const defaultOptions: PluginOptions<NameHistoryPluginType> = { config: { @@ -25,7 +24,7 @@ export const NameHistoryPlugin = zeppelinGuildPlugin<NameHistoryPluginType>()({ name: "name_history", showInDocs: false, - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zNameHistoryConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/NameHistory/types.ts b/backend/src/plugins/NameHistory/types.ts index e85f2f40..70101b53 100644 --- a/backend/src/plugins/NameHistory/types.ts +++ b/backend/src/plugins/NameHistory/types.ts @@ -1,16 +1,15 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { Queue } from "../../Queue"; import { GuildNicknameHistory } from "../../data/GuildNicknameHistory"; import { UsernameHistory } from "../../data/UsernameHistory"; -export const ConfigSchema = t.type({ - can_view: t.boolean, +export const zNameHistoryConfig = z.strictObject({ + can_view: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface NameHistoryPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zNameHistoryConfig>; state: { nicknameHistory: GuildNicknameHistory; usernameHistory: UsernameHistory; diff --git a/backend/src/plugins/Persist/PersistPlugin.ts b/backend/src/plugins/Persist/PersistPlugin.ts index ecd50067..dc489a25 100644 --- a/backend/src/plugins/Persist/PersistPlugin.ts +++ b/backend/src/plugins/Persist/PersistPlugin.ts @@ -1,7 +1,6 @@ import { PluginOptions } from "knub"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildPersistedData } from "../../data/GuildPersistedData"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { trimPluginDescription } from "../../utils"; import { GuildMemberCachePlugin } from "../GuildMemberCache/GuildMemberCachePlugin"; import { LogsPlugin } from "../Logs/LogsPlugin"; @@ -9,7 +8,7 @@ import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { LoadDataEvt } from "./events/LoadDataEvt"; import { StoreDataEvt } from "./events/StoreDataEvt"; -import { ConfigSchema, PersistPluginType } from "./types"; +import { PersistPluginType, zPersistConfig } from "./types"; const defaultOptions: PluginOptions<PersistPluginType> = { config: { @@ -28,11 +27,11 @@ export const PersistPlugin = zeppelinGuildPlugin<PersistPluginType>()({ Re-apply roles or nicknames for users when they rejoin the server. Mute roles are re-applied automatically, this plugin is not required for that. `), - configSchema: ConfigSchema, + configSchema: zPersistConfig, }, dependencies: () => [LogsPlugin, RoleManagerPlugin, GuildMemberCachePlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zPersistConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/Persist/types.ts b/backend/src/plugins/Persist/types.ts index 8ab258b7..4f42faa9 100644 --- a/backend/src/plugins/Persist/types.ts +++ b/backend/src/plugins/Persist/types.ts @@ -1,17 +1,17 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener } from "knub"; +import z from "zod"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildPersistedData } from "../../data/GuildPersistedData"; +import { zSnowflake } from "../../utils"; -export const ConfigSchema = t.type({ - persisted_roles: t.array(t.string), - persist_nicknames: t.boolean, - persist_voice_mutes: t.boolean, // Deprecated, here to not break old configs +export const zPersistConfig = z.strictObject({ + persisted_roles: z.array(zSnowflake), + persist_nicknames: z.boolean(), + persist_voice_mutes: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface PersistPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zPersistConfig>; state: { persistedData: GuildPersistedData; diff --git a/backend/src/plugins/Phisherman/PhishermanPlugin.ts b/backend/src/plugins/Phisherman/PhishermanPlugin.ts index e8f74ce3..8e895d84 100644 --- a/backend/src/plugins/Phisherman/PhishermanPlugin.ts +++ b/backend/src/plugins/Phisherman/PhishermanPlugin.ts @@ -1,10 +1,10 @@ import { PluginOptions } from "knub"; import { hasPhishermanMasterAPIKey, phishermanApiKeyIsValid } from "../../data/Phisherman"; -import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils"; +import { mapToPublicFn } from "../../pluginUtils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { getDomainInfo } from "./functions/getDomainInfo"; import { pluginInfo } from "./info"; -import { ConfigSchema, PhishermanPluginType } from "./types"; +import { PhishermanPluginType, zPhishermanConfig } from "./types"; const defaultOptions: PluginOptions<PhishermanPluginType> = { config: { @@ -18,7 +18,7 @@ export const PhishermanPlugin = zeppelinGuildPlugin<PhishermanPluginType>()({ showInDocs: true, info: pluginInfo, - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zPhishermanConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/Phisherman/info.ts b/backend/src/plugins/Phisherman/info.ts index 826da43d..24530dce 100644 --- a/backend/src/plugins/Phisherman/info.ts +++ b/backend/src/plugins/Phisherman/info.ts @@ -1,6 +1,6 @@ import { trimPluginDescription } from "../../utils"; import { ZeppelinGuildPluginBlueprint } from "../ZeppelinPluginBlueprint"; -import { ConfigSchema } from "./types"; +import { zPhishermanConfig } from "./types"; export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = { prettyName: "Phisherman", @@ -39,5 +39,5 @@ export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = { clean: true ~~~ `), - configSchema: ConfigSchema, + configSchema: zPhishermanConfig, }; diff --git a/backend/src/plugins/Phisherman/types.ts b/backend/src/plugins/Phisherman/types.ts index 56ed7aca..d21eb38e 100644 --- a/backend/src/plugins/Phisherman/types.ts +++ b/backend/src/plugins/Phisherman/types.ts @@ -1,14 +1,12 @@ -import * as t from "io-ts"; import { BasePluginType } from "knub"; -import { tNullable } from "../../utils"; +import z from "zod"; -export const ConfigSchema = t.type({ - api_key: tNullable(t.string), +export const zPhishermanConfig = z.strictObject({ + api_key: z.string().max(255).nullable(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface PhishermanPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zPhishermanConfig>; state: { validApiKey: string | null; diff --git a/backend/src/plugins/PingableRoles/PingableRolesPlugin.ts b/backend/src/plugins/PingableRoles/PingableRolesPlugin.ts index 331410f9..a33103bf 100644 --- a/backend/src/plugins/PingableRoles/PingableRolesPlugin.ts +++ b/backend/src/plugins/PingableRoles/PingableRolesPlugin.ts @@ -1,10 +1,9 @@ import { PluginOptions } from "knub"; import { GuildPingableRoles } from "../../data/GuildPingableRoles"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { PingableRoleDisableCmd } from "./commands/PingableRoleDisableCmd"; import { PingableRoleEnableCmd } from "./commands/PingableRoleEnableCmd"; -import { ConfigSchema, PingableRolesPluginType } from "./types"; +import { PingableRolesPluginType, zPingableRolesConfig } from "./types"; const defaultOptions: PluginOptions<PingableRolesPluginType> = { config: { @@ -25,10 +24,10 @@ export const PingableRolesPlugin = zeppelinGuildPlugin<PingableRolesPluginType>( showInDocs: true, info: { prettyName: "Pingable roles", - configSchema: ConfigSchema, + configSchema: zPingableRolesConfig, }, - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zPingableRolesConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/PingableRoles/types.ts b/backend/src/plugins/PingableRoles/types.ts index 272f7594..3bd6faa8 100644 --- a/backend/src/plugins/PingableRoles/types.ts +++ b/backend/src/plugins/PingableRoles/types.ts @@ -1,15 +1,14 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildPingableRoles } from "../../data/GuildPingableRoles"; import { PingableRole } from "../../data/entities/PingableRole"; -export const ConfigSchema = t.type({ - can_manage: t.boolean, +export const zPingableRolesConfig = z.strictObject({ + can_manage: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface PingableRolesPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zPingableRolesConfig>; state: { pingableRoles: GuildPingableRoles; diff --git a/backend/src/plugins/Post/PostPlugin.ts b/backend/src/plugins/Post/PostPlugin.ts index 09d069a2..304d6b65 100644 --- a/backend/src/plugins/Post/PostPlugin.ts +++ b/backend/src/plugins/Post/PostPlugin.ts @@ -3,7 +3,6 @@ import { onGuildEvent } from "../../data/GuildEvents"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildScheduledPosts } from "../../data/GuildScheduledPosts"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; @@ -14,7 +13,7 @@ import { PostEmbedCmd } from "./commands/PostEmbedCmd"; import { ScheduledPostsDeleteCmd } from "./commands/ScheduledPostsDeleteCmd"; import { ScheduledPostsListCmd } from "./commands/ScheduledPostsListCmd"; import { ScheduledPostsShowCmd } from "./commands/ScheduledPostsShowCmd"; -import { ConfigSchema, PostPluginType } from "./types"; +import { PostPluginType, zPostConfig } from "./types"; import { postScheduledPost } from "./util/postScheduledPost"; const defaultOptions: PluginOptions<PostPluginType> = { @@ -36,11 +35,11 @@ export const PostPlugin = zeppelinGuildPlugin<PostPluginType>()({ showInDocs: true, info: { prettyName: "Post", - configSchema: ConfigSchema, + configSchema: zPostConfig, }, dependencies: () => [TimeAndDatePlugin, LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zPostConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/Post/types.ts b/backend/src/plugins/Post/types.ts index 815b19b8..e0ec7d2c 100644 --- a/backend/src/plugins/Post/types.ts +++ b/backend/src/plugins/Post/types.ts @@ -1,16 +1,15 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildScheduledPosts } from "../../data/GuildScheduledPosts"; -export const ConfigSchema = t.type({ - can_post: t.boolean, +export const zPostConfig = z.strictObject({ + can_post: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface PostPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zPostConfig>; state: { savedMessages: GuildSavedMessages; scheduledPosts: GuildScheduledPosts; diff --git a/backend/src/plugins/ReactionRoles/ReactionRolesPlugin.ts b/backend/src/plugins/ReactionRoles/ReactionRolesPlugin.ts index ac68de33..06fff977 100644 --- a/backend/src/plugins/ReactionRoles/ReactionRolesPlugin.ts +++ b/backend/src/plugins/ReactionRoles/ReactionRolesPlugin.ts @@ -2,7 +2,6 @@ import { PluginOptions } from "knub"; import { Queue } from "../../Queue"; import { GuildReactionRoles } from "../../data/GuildReactionRoles"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { ClearReactionRolesCmd } from "./commands/ClearReactionRolesCmd"; @@ -10,7 +9,7 @@ import { InitReactionRolesCmd } from "./commands/InitReactionRolesCmd"; import { RefreshReactionRolesCmd } from "./commands/RefreshReactionRolesCmd"; import { AddReactionRoleEvt } from "./events/AddReactionRoleEvt"; import { MessageDeletedEvt } from "./events/MessageDeletedEvt"; -import { ConfigSchema, ReactionRolesPluginType } from "./types"; +import { ReactionRolesPluginType, zReactionRolesConfig } from "./types"; const MIN_AUTO_REFRESH = 1000 * 60 * 15; // 15min minimum, let's not abuse the API @@ -40,11 +39,11 @@ export const ReactionRolesPlugin = zeppelinGuildPlugin<ReactionRolesPluginType>( info: { prettyName: "Reaction roles", legacy: "Consider using the [Role buttons](/docs/plugins/role_buttons) plugin instead.", - configSchema: ConfigSchema, + configSchema: zReactionRolesConfig, }, dependencies: () => [LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zReactionRolesConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/ReactionRoles/types.ts b/backend/src/plugins/ReactionRoles/types.ts index 6f65ad98..a1c09f00 100644 --- a/backend/src/plugins/ReactionRoles/types.ts +++ b/backend/src/plugins/ReactionRoles/types.ts @@ -1,17 +1,15 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { Queue } from "../../Queue"; import { GuildReactionRoles } from "../../data/GuildReactionRoles"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -import { tNullable } from "../../utils"; -export const ConfigSchema = t.type({ - auto_refresh_interval: t.number, - remove_user_reactions: t.boolean, - can_manage: t.boolean, - button_groups: tNullable(t.unknown), +export const zReactionRolesConfig = z.strictObject({ + auto_refresh_interval: z.number(), + remove_user_reactions: z.boolean(), + can_manage: z.boolean(), + button_groups: z.nullable(z.unknown()), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export type RoleChangeMode = "+" | "-"; @@ -24,12 +22,14 @@ export type PendingMemberRoleChanges = { }>; }; -const ReactionRolePair = t.union([t.tuple([t.string, t.string, t.string]), t.tuple([t.string, t.string])]); -export type TReactionRolePair = t.TypeOf<typeof ReactionRolePair>; -type ReactionRolePair = [string, string, string?]; +const zReactionRolePair = z.union([ + z.tuple([z.string(), z.string(), z.string()]), + z.tuple([z.string(), z.string()]), +]); +export type TReactionRolePair = z.infer<typeof zReactionRolePair>; export interface ReactionRolesPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zReactionRolesConfig>; state: { reactionRoles: GuildReactionRoles; savedMessages: GuildSavedMessages; diff --git a/backend/src/plugins/Reminders/RemindersPlugin.ts b/backend/src/plugins/Reminders/RemindersPlugin.ts index 128e945c..5e86ab49 100644 --- a/backend/src/plugins/Reminders/RemindersPlugin.ts +++ b/backend/src/plugins/Reminders/RemindersPlugin.ts @@ -1,14 +1,13 @@ import { PluginOptions } from "knub"; import { onGuildEvent } from "../../data/GuildEvents"; import { GuildReminders } from "../../data/GuildReminders"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { RemindCmd } from "./commands/RemindCmd"; import { RemindersCmd } from "./commands/RemindersCmd"; import { RemindersDeleteCmd } from "./commands/RemindersDeleteCmd"; import { postReminder } from "./functions/postReminder"; -import { ConfigSchema, RemindersPluginType } from "./types"; +import { RemindersPluginType, zRemindersConfig } from "./types"; const defaultOptions: PluginOptions<RemindersPluginType> = { config: { @@ -29,11 +28,11 @@ export const RemindersPlugin = zeppelinGuildPlugin<RemindersPluginType>()({ showInDocs: true, info: { prettyName: "Reminders", - configSchema: ConfigSchema, + configSchema: zRemindersConfig, }, dependencies: () => [TimeAndDatePlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zRemindersConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/Reminders/types.ts b/backend/src/plugins/Reminders/types.ts index 5bc896ba..4356fac0 100644 --- a/backend/src/plugins/Reminders/types.ts +++ b/backend/src/plugins/Reminders/types.ts @@ -1,14 +1,13 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildReminders } from "../../data/GuildReminders"; -export const ConfigSchema = t.type({ - can_use: t.boolean, +export const zRemindersConfig = z.strictObject({ + can_use: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface RemindersPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zRemindersConfig>; state: { reminders: GuildReminders; diff --git a/backend/src/plugins/RoleButtons/RoleButtonsPlugin.ts b/backend/src/plugins/RoleButtons/RoleButtonsPlugin.ts index 79cfb8b4..3f1e34ea 100644 --- a/backend/src/plugins/RoleButtons/RoleButtonsPlugin.ts +++ b/backend/src/plugins/RoleButtons/RoleButtonsPlugin.ts @@ -1,15 +1,12 @@ import { GuildRoleButtons } from "../../data/GuildRoleButtons"; -import { parseIoTsSchema, StrictValidationError } from "../../validatorUtils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { resetButtonsCmd } from "./commands/resetButtons"; import { onButtonInteraction } from "./events/buttonInteraction"; import { applyAllRoleButtons } from "./functions/applyAllRoleButtons"; -import { createButtonComponents } from "./functions/createButtonComponents"; -import { TooManyComponentsError } from "./functions/TooManyComponentsError"; import { pluginInfo } from "./info"; -import { ConfigSchema, RoleButtonsPluginType } from "./types"; +import { RoleButtonsPluginType, zRoleButtonsConfig } from "./types"; export const RoleButtonsPlugin = zeppelinGuildPlugin<RoleButtonsPluginType>()({ name: "role_buttons", @@ -31,41 +28,7 @@ export const RoleButtonsPlugin = zeppelinGuildPlugin<RoleButtonsPluginType>()({ ], }, - configParser(input) { - // Auto-fill "name" property for buttons based on the object key - const seenMessages = new Set(); - for (const [name, buttonsConfig] of Object.entries<any>((input as any).buttons ?? {})) { - if (name.length > 16) { - throw new StrictValidationError(["Name for role buttons can be at most 16 characters long"]); - } - - if (buttonsConfig) { - buttonsConfig.name = name; - - if (buttonsConfig.message) { - if ("message_id" in buttonsConfig.message) { - if (seenMessages.has(buttonsConfig.message.message_id)) { - throw new StrictValidationError(["Can't target the same message with two sets of role buttons"]); - } - seenMessages.add(buttonsConfig.message.message_id); - } - } - - if (buttonsConfig.options) { - try { - createButtonComponents(buttonsConfig); - } catch (err) { - if (err instanceof TooManyComponentsError) { - throw new StrictValidationError(["Too many options; can only have max 5 buttons per row on max 5 rows."]); - } - throw new StrictValidationError(["Error validating options"]); - } - } - } - } - - return parseIoTsSchema(ConfigSchema, input); - }, + configParser: (input) => zRoleButtonsConfig.parse(input), dependencies: () => [LogsPlugin, RoleManagerPlugin], diff --git a/backend/src/plugins/RoleButtons/info.ts b/backend/src/plugins/RoleButtons/info.ts index d63fb17c..6076419c 100644 --- a/backend/src/plugins/RoleButtons/info.ts +++ b/backend/src/plugins/RoleButtons/info.ts @@ -1,6 +1,6 @@ import { trimPluginDescription } from "../../utils"; import { ZeppelinGuildPluginBlueprint } from "../ZeppelinPluginBlueprint"; -import { ConfigSchema } from "./types"; +import { zRoleButtonsConfig } from "./types"; export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = { prettyName: "Role buttons", @@ -78,5 +78,5 @@ export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = { ... # See above for examples for options ~~~ `), - configSchema: ConfigSchema, + configSchema: zRoleButtonsConfig, }; diff --git a/backend/src/plugins/RoleButtons/types.ts b/backend/src/plugins/RoleButtons/types.ts index f5792f36..3c94f8a8 100644 --- a/backend/src/plugins/RoleButtons/types.ts +++ b/backend/src/plugins/RoleButtons/types.ts @@ -1,58 +1,102 @@ import { ButtonStyle } from "discord.js"; -import * as t from "io-ts"; import { BasePluginType } from "knub"; +import z from "zod"; import { GuildRoleButtons } from "../../data/GuildRoleButtons"; -import { tMessageContent, tNullable } from "../../utils"; +import { zBoundedCharacters, zBoundedRecord, zMessageContent, zSnowflake } from "../../utils"; +import { TooManyComponentsError } from "./functions/TooManyComponentsError"; +import { createButtonComponents } from "./functions/createButtonComponents"; -const RoleButtonOption = t.type({ - role_id: t.string, - label: tNullable(t.string), - emoji: tNullable(t.string), +const zRoleButtonOption = z.strictObject({ + role_id: zSnowflake, + label: z.string().nullable().default(null), + emoji: z.string().nullable().default(null), // https://discord.js.org/#/docs/discord.js/v13/typedef/MessageButtonStyle - style: tNullable( - t.union([ - t.literal(ButtonStyle.Primary), - t.literal(ButtonStyle.Secondary), - t.literal(ButtonStyle.Success), - t.literal(ButtonStyle.Danger), + style: z.union([ + z.literal(ButtonStyle.Primary), + z.literal(ButtonStyle.Secondary), + z.literal(ButtonStyle.Success), + z.literal(ButtonStyle.Danger), - // The following are deprecated - t.literal("PRIMARY"), - t.literal("SECONDARY"), - t.literal("SUCCESS"), - t.literal("DANGER"), - // t.literal("LINK"), // Role buttons don't use link buttons, but adding this here so it's documented why it's not available - ]), - ), - start_new_row: tNullable(t.boolean), + // The following are deprecated + z.literal("PRIMARY"), + z.literal("SECONDARY"), + z.literal("SUCCESS"), + z.literal("DANGER"), + // z.literal("LINK"), // Role buttons don't use link buttons, but adding this here so it's documented why it's not available + ]).nullable().default(null), + start_new_row: z.boolean().default(false), }); -export type TRoleButtonOption = t.TypeOf<typeof RoleButtonOption>; +export type TRoleButtonOption = z.infer<typeof zRoleButtonOption>; -const RoleButtonsConfigItem = t.type({ - name: t.string, - message: t.union([ - t.type({ - channel_id: t.string, - message_id: t.string, +const zRoleButtonsConfigItem = z.strictObject({ + // Typed as "never" because you are not expected to supply this directly. + // The transform instead picks it up from the property key and the output type is a string. + name: z.never().optional().transform((_, ctx) => { + const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); + if (! ruleName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Role buttons must have names", + }); + return z.NEVER; + } + return ruleName; + }), + message: z.union([ + z.strictObject({ + channel_id: zSnowflake, + message_id: zSnowflake, }), - t.type({ - channel_id: t.string, - content: tMessageContent, + z.strictObject({ + channel_id: zSnowflake, + content: zMessageContent, }), ]), - options: t.array(RoleButtonOption), - exclusive: tNullable(t.boolean), -}); -export type TRoleButtonsConfigItem = t.TypeOf<typeof RoleButtonsConfigItem>; + options: z.array(zRoleButtonOption).max(25), + exclusive: z.boolean().default(false), +}) + .refine((parsed) => { + try { + createButtonComponents(parsed); + } catch (err) { + if (err instanceof TooManyComponentsError) { + return false; + } + throw err; + } + return true; + }, { + message: "Too many options; can only have max 5 buttons per row on max 5 rows." + }); +export type TRoleButtonsConfigItem = z.infer<typeof zRoleButtonsConfigItem>; -export const ConfigSchema = t.type({ - buttons: t.record(t.string, RoleButtonsConfigItem), - can_reset: t.boolean, -}); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; +export const zRoleButtonsConfig = z.strictObject({ + buttons: zBoundedRecord( + z.record(zBoundedCharacters(1, 16), zRoleButtonsConfigItem), + 0, + 100, + ), + can_reset: z.boolean(), +}) + .refine((parsed) => { + const seenMessages = new Set(); + for (const button of Object.values(parsed.buttons)) { + if (button.message) { + if ("message_id" in button.message) { + if (seenMessages.has(button.message.message_id)) { + return false; + } + seenMessages.add(button.message.message_id); + } + } + } + return true; + }, { + message: "Can't target the same message with two sets of role buttons", + }); export interface RoleButtonsPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zRoleButtonsConfig>; state: { roleButtons: GuildRoleButtons; }; diff --git a/backend/src/plugins/RoleManager/RoleManagerPlugin.ts b/backend/src/plugins/RoleManager/RoleManagerPlugin.ts index 878b2cd4..34427d98 100644 --- a/backend/src/plugins/RoleManager/RoleManagerPlugin.ts +++ b/backend/src/plugins/RoleManager/RoleManagerPlugin.ts @@ -1,5 +1,5 @@ import { GuildRoleQueue } from "../../data/GuildRoleQueue"; -import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils"; +import { mapToPublicFn } from "../../pluginUtils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { addPriorityRole } from "./functions/addPriorityRole"; @@ -7,14 +7,14 @@ import { addRole } from "./functions/addRole"; import { removePriorityRole } from "./functions/removePriorityRole"; import { removeRole } from "./functions/removeRole"; import { runRoleAssignmentLoop } from "./functions/runRoleAssignmentLoop"; -import { ConfigSchema, RoleManagerPluginType } from "./types"; +import { RoleManagerPluginType, zRoleManagerConfig } from "./types"; export const RoleManagerPlugin = zeppelinGuildPlugin<RoleManagerPluginType>()({ name: "role_manager", showInDocs: false, dependencies: () => [LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zRoleManagerConfig.parse(input), public: { addRole: mapToPublicFn(addRole), diff --git a/backend/src/plugins/RoleManager/types.ts b/backend/src/plugins/RoleManager/types.ts index 54f5033a..51ba3080 100644 --- a/backend/src/plugins/RoleManager/types.ts +++ b/backend/src/plugins/RoleManager/types.ts @@ -1,12 +1,11 @@ -import * as t from "io-ts"; import { BasePluginType } from "knub"; +import z from "zod"; import { GuildRoleQueue } from "../../data/GuildRoleQueue"; -export const ConfigSchema = t.type({}); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; +export const zRoleManagerConfig = z.strictObject({}); export interface RoleManagerPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zRoleManagerConfig>; state: { roleQueue: GuildRoleQueue; roleAssignmentLoopRunning: boolean; diff --git a/backend/src/plugins/Roles/RolesPlugin.ts b/backend/src/plugins/Roles/RolesPlugin.ts index 6c810940..0e181cfa 100644 --- a/backend/src/plugins/Roles/RolesPlugin.ts +++ b/backend/src/plugins/Roles/RolesPlugin.ts @@ -1,6 +1,5 @@ import { PluginOptions } from "knub"; import { GuildLogs } from "../../data/GuildLogs"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { trimPluginDescription } from "../../utils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin"; @@ -9,13 +8,13 @@ import { AddRoleCmd } from "./commands/AddRoleCmd"; import { MassAddRoleCmd } from "./commands/MassAddRoleCmd"; import { MassRemoveRoleCmd } from "./commands/MassRemoveRoleCmd"; import { RemoveRoleCmd } from "./commands/RemoveRoleCmd"; -import { ConfigSchema, RolesPluginType } from "./types"; +import { RolesPluginType, zRolesConfig } from "./types"; const defaultOptions: PluginOptions<RolesPluginType> = { config: { can_assign: false, can_mass_assign: false, - assignable_roles: ["558037973581430785"], + assignable_roles: [], }, overrides: [ { @@ -41,11 +40,11 @@ export const RolesPlugin = zeppelinGuildPlugin<RolesPluginType>()({ description: trimPluginDescription(` Enables authorised users to add and remove whitelisted roles with a command. `), - configSchema: ConfigSchema, + configSchema: zRolesConfig, }, dependencies: () => [LogsPlugin, RoleManagerPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zRolesConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/Roles/types.ts b/backend/src/plugins/Roles/types.ts index 346dc562..caf55b76 100644 --- a/backend/src/plugins/Roles/types.ts +++ b/backend/src/plugins/Roles/types.ts @@ -1,16 +1,15 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildLogs } from "../../data/GuildLogs"; -export const ConfigSchema = t.type({ - can_assign: t.boolean, - can_mass_assign: t.boolean, - assignable_roles: t.array(t.string), +export const zRolesConfig = z.strictObject({ + can_assign: z.boolean(), + can_mass_assign: z.boolean(), + assignable_roles: z.array(z.string()).max(100), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface RolesPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zRolesConfig>; state: { logs: GuildLogs; }; diff --git a/backend/src/plugins/SelfGrantableRoles/SelfGrantableRolesPlugin.ts b/backend/src/plugins/SelfGrantableRoles/SelfGrantableRolesPlugin.ts index f16cff92..6f0d666b 100644 --- a/backend/src/plugins/SelfGrantableRoles/SelfGrantableRolesPlugin.ts +++ b/backend/src/plugins/SelfGrantableRoles/SelfGrantableRolesPlugin.ts @@ -1,11 +1,10 @@ import { CooldownManager, PluginOptions } from "knub"; import { trimPluginDescription } from "../../utils"; -import { parseIoTsSchema } from "../../validatorUtils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { RoleAddCmd } from "./commands/RoleAddCmd"; import { RoleHelpCmd } from "./commands/RoleHelpCmd"; import { RoleRemoveCmd } from "./commands/RoleRemoveCmd"; -import { ConfigSchema, SelfGrantableRolesPluginType, defaultSelfGrantableRoleEntry } from "./types"; +import { SelfGrantableRolesPluginType, zSelfGrantableRolesConfig } from "./types"; const defaultOptions: PluginOptions<SelfGrantableRolesPluginType> = { config: { @@ -66,25 +65,10 @@ export const SelfGrantableRolesPlugin = zeppelinGuildPlugin<SelfGrantableRolesPl can_use: true ~~~ `), - configSchema: ConfigSchema, + configSchema: zSelfGrantableRolesConfig, }, - configParser: (input) => { - const entries = (input as any).entries; - for (const [key, entry] of Object.entries<any>(entries)) { - // Apply default entry config - entries[key] = { ...defaultSelfGrantableRoleEntry, ...entry }; - - // Normalize alias names - if (entry.roles) { - for (const [roleId, aliases] of Object.entries<string[]>(entry.roles)) { - entry.roles[roleId] = aliases.map((a) => a.toLowerCase()); - } - } - } - - return parseIoTsSchema(ConfigSchema, input); - }, + configParser: (input) => zSelfGrantableRolesConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/SelfGrantableRoles/types.ts b/backend/src/plugins/SelfGrantableRoles/types.ts index 08d665df..a1aed241 100644 --- a/backend/src/plugins/SelfGrantableRoles/types.ts +++ b/backend/src/plugins/SelfGrantableRoles/types.ts @@ -1,31 +1,29 @@ -import * as t from "io-ts"; import { BasePluginType, CooldownManager, guildPluginMessageCommand } from "knub"; +import z from "zod"; +import { zBoundedCharacters, zBoundedRecord } from "../../utils"; -const RoleMap = t.record(t.string, t.array(t.string)); +const zRoleMap = z.record( + zBoundedCharacters(1, 100), + z.array(zBoundedCharacters(1, 2000)) + .max(100) + .transform((parsed) => parsed.map(v => v.toLowerCase())), +); -const SelfGrantableRoleEntry = t.type({ - roles: RoleMap, - can_use: t.boolean, - can_ignore_cooldown: t.boolean, - max_roles: t.number, +const zSelfGrantableRoleEntry = z.strictObject({ + roles: zBoundedRecord(zRoleMap, 0, 100), + can_use: z.boolean().default(false), + can_ignore_cooldown: z.boolean().default(false), + max_roles: z.number().default(0), }); -const PartialRoleEntry = t.partial(SelfGrantableRoleEntry.props); -export type TSelfGrantableRoleEntry = t.TypeOf<typeof SelfGrantableRoleEntry>; +export type TSelfGrantableRoleEntry = z.infer<typeof zSelfGrantableRoleEntry>; -export const ConfigSchema = t.type({ - entries: t.record(t.string, SelfGrantableRoleEntry), - mention_roles: t.boolean, +export const zSelfGrantableRolesConfig = z.strictObject({ + entries: zBoundedRecord(z.record(zBoundedCharacters(0, 255), zSelfGrantableRoleEntry), 0, 100), + mention_roles: z.boolean(), }); -type TConfigSchema = t.TypeOf<typeof ConfigSchema>; - -export const defaultSelfGrantableRoleEntry: t.TypeOf<typeof PartialRoleEntry> = { - can_use: false, - can_ignore_cooldown: false, - max_roles: 0, -}; export interface SelfGrantableRolesPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zSelfGrantableRolesConfig>; state: { cooldowns: CooldownManager; }; diff --git a/backend/src/plugins/Slowmode/SlowmodePlugin.ts b/backend/src/plugins/Slowmode/SlowmodePlugin.ts index 4f6c0996..174f38a9 100644 --- a/backend/src/plugins/Slowmode/SlowmodePlugin.ts +++ b/backend/src/plugins/Slowmode/SlowmodePlugin.ts @@ -2,7 +2,6 @@ import { PluginOptions } from "knub"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildSlowmodes } from "../../data/GuildSlowmodes"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { SECONDS } from "../../utils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; @@ -11,7 +10,7 @@ import { SlowmodeDisableCmd } from "./commands/SlowmodeDisableCmd"; import { SlowmodeGetCmd } from "./commands/SlowmodeGetCmd"; import { SlowmodeListCmd } from "./commands/SlowmodeListCmd"; import { SlowmodeSetCmd } from "./commands/SlowmodeSetCmd"; -import { ConfigSchema, SlowmodePluginType } from "./types"; +import { SlowmodePluginType, zSlowmodeConfig } from "./types"; import { clearExpiredSlowmodes } from "./util/clearExpiredSlowmodes"; import { onMessageCreate } from "./util/onMessageCreate"; @@ -41,7 +40,7 @@ export const SlowmodePlugin = zeppelinGuildPlugin<SlowmodePluginType>()({ showInDocs: true, info: { prettyName: "Slowmode", - configSchema: ConfigSchema, + configSchema: zSlowmodeConfig, }, // prettier-ignore @@ -49,7 +48,7 @@ export const SlowmodePlugin = zeppelinGuildPlugin<SlowmodePluginType>()({ LogsPlugin, ], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zSlowmodeConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/Slowmode/types.ts b/backend/src/plugins/Slowmode/types.ts index 089a59ef..c4fe44d5 100644 --- a/backend/src/plugins/Slowmode/types.ts +++ b/backend/src/plugins/Slowmode/types.ts @@ -1,20 +1,19 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildSlowmodes } from "../../data/GuildSlowmodes"; import { SlowmodeChannel } from "../../data/entities/SlowmodeChannel"; -export const ConfigSchema = t.type({ - use_native_slowmode: t.boolean, - - can_manage: t.boolean, - is_affected: t.boolean, +export const zSlowmodeConfig = z.strictObject({ + use_native_slowmode: z.boolean(), + + can_manage: z.boolean(), + is_affected: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface SlowmodePluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zSlowmodeConfig>; state: { slowmodes: GuildSlowmodes; savedMessages: GuildSavedMessages; diff --git a/backend/src/plugins/Spam/SpamPlugin.ts b/backend/src/plugins/Spam/SpamPlugin.ts index e1a7efc0..54ec540c 100644 --- a/backend/src/plugins/Spam/SpamPlugin.ts +++ b/backend/src/plugins/Spam/SpamPlugin.ts @@ -3,12 +3,11 @@ import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildMutes } from "../../data/GuildMutes"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { trimPluginDescription } from "../../utils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { SpamVoiceStateUpdateEvt } from "./events/SpamVoiceEvt"; -import { ConfigSchema, SpamPluginType } from "./types"; +import { SpamPluginType, zSpamConfig } from "./types"; import { clearOldRecentActions } from "./util/clearOldRecentActions"; import { onMessageCreate } from "./util/onMessageCreate"; @@ -53,11 +52,11 @@ export const SpamPlugin = zeppelinGuildPlugin<SpamPluginType>()({ For more advanced spam filtering, check out the Automod plugin! `), legacy: true, - configSchema: ConfigSchema, + configSchema: zSpamConfig, }, dependencies: () => [LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zSpamConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/Spam/types.ts b/backend/src/plugins/Spam/types.ts index 1e561477..74ea872f 100644 --- a/backend/src/plugins/Spam/types.ts +++ b/backend/src/plugins/Spam/types.ts @@ -1,35 +1,40 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener } from "knub"; +import z from "zod"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildMutes } from "../../data/GuildMutes"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -import { tNullable } from "../../utils"; +import { zSnowflake } from "../../utils"; -const BaseSingleSpamConfig = t.type({ - interval: t.number, - count: t.number, - mute: tNullable(t.boolean), - mute_time: tNullable(t.number), - remove_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])), - restore_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])), - clean: tNullable(t.boolean), +const zBaseSingleSpamConfig = z.strictObject({ + interval: z.number(), + count: z.number(), + mute: z.boolean().default(false), + mute_time: z.number().nullable().default(null), + remove_roles_on_mute: z.union([ + z.boolean(), + z.array(zSnowflake), + ]).default(false), + restore_roles_on_mute: z.union([ + z.boolean(), + z.array(zSnowflake), + ]).default(false), + clean: z.boolean().default(false), }); -export type TBaseSingleSpamConfig = t.TypeOf<typeof BaseSingleSpamConfig>; +export type TBaseSingleSpamConfig = z.infer<typeof zBaseSingleSpamConfig>; -export const ConfigSchema = t.type({ - max_censor: tNullable(BaseSingleSpamConfig), - max_messages: tNullable(BaseSingleSpamConfig), - max_mentions: tNullable(BaseSingleSpamConfig), - max_links: tNullable(BaseSingleSpamConfig), - max_attachments: tNullable(BaseSingleSpamConfig), - max_emojis: tNullable(BaseSingleSpamConfig), - max_newlines: tNullable(BaseSingleSpamConfig), - max_duplicates: tNullable(BaseSingleSpamConfig), - max_characters: tNullable(BaseSingleSpamConfig), - max_voice_moves: tNullable(BaseSingleSpamConfig), +export const zSpamConfig = z.strictObject({ + max_censor: zBaseSingleSpamConfig.nullable(), + max_messages: zBaseSingleSpamConfig.nullable(), + max_mentions: zBaseSingleSpamConfig.nullable(), + max_links: zBaseSingleSpamConfig.nullable(), + max_attachments: zBaseSingleSpamConfig.nullable(), + max_emojis: zBaseSingleSpamConfig.nullable(), + max_newlines: zBaseSingleSpamConfig.nullable(), + max_duplicates: zBaseSingleSpamConfig.nullable(), + max_characters: zBaseSingleSpamConfig.nullable(), + max_voice_moves: zBaseSingleSpamConfig.nullable(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export enum RecentActionType { Message = 1, @@ -53,7 +58,7 @@ interface IRecentAction<T> { } export interface SpamPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zSpamConfig>; state: { logs: GuildLogs; archives: GuildArchives; diff --git a/backend/src/plugins/Starboard/StarboardPlugin.ts b/backend/src/plugins/Starboard/StarboardPlugin.ts index 050b9037..4ca3c441 100644 --- a/backend/src/plugins/Starboard/StarboardPlugin.ts +++ b/backend/src/plugins/Starboard/StarboardPlugin.ts @@ -3,12 +3,11 @@ import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildStarboardMessages } from "../../data/GuildStarboardMessages"; import { GuildStarboardReactions } from "../../data/GuildStarboardReactions"; import { trimPluginDescription } from "../../utils"; -import { parseIoTsSchema } from "../../validatorUtils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { MigratePinsCmd } from "./commands/MigratePinsCmd"; import { StarboardReactionAddEvt } from "./events/StarboardReactionAddEvt"; import { StarboardReactionRemoveAllEvt, StarboardReactionRemoveEvt } from "./events/StarboardReactionRemoveEvts"; -import { ConfigSchema, StarboardPluginType, defaultStarboardOpts } from "./types"; +import { StarboardPluginType, zStarboardConfig } from "./types"; import { onMessageDelete } from "./util/onMessageDelete"; const defaultOptions: PluginOptions<StarboardPluginType> = { @@ -120,19 +119,10 @@ export const StarboardPlugin = zeppelinGuildPlugin<StarboardPluginType>()({ enabled: true ~~~ `), - configSchema: ConfigSchema, + configSchema: zStarboardConfig, }, - configParser(input) { - const boards = (input as any).boards; - if (boards) { - for (const [name, opts] of Object.entries(boards)) { - boards[name] = Object.assign({}, defaultStarboardOpts, opts); - } - } - - return parseIoTsSchema(ConfigSchema, input); - }, + configParser: (input) => zStarboardConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/Starboard/types.ts b/backend/src/plugins/Starboard/types.ts index 223501e3..7dc2d8f5 100644 --- a/backend/src/plugins/Starboard/types.ts +++ b/backend/src/plugins/Starboard/types.ts @@ -1,39 +1,33 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildStarboardMessages } from "../../data/GuildStarboardMessages"; import { GuildStarboardReactions } from "../../data/GuildStarboardReactions"; -import { tDeepPartial, tNullable } from "../../utils"; +import { zBoundedRecord, zSnowflake } from "../../utils"; -const StarboardOpts = t.type({ - channel_id: t.string, - stars_required: t.number, - star_emoji: tNullable(t.array(t.string)), - allow_selfstars: tNullable(t.boolean), - copy_full_embed: tNullable(t.boolean), - enabled: tNullable(t.boolean), - show_star_count: t.boolean, - color: tNullable(t.number), +const zStarboardOpts = z.strictObject({ + channel_id: zSnowflake, + stars_required: z.number(), + star_emoji: z.array(z.string()).default(["⭐"]), + allow_selfstars: z.boolean().default(false), + copy_full_embed: z.boolean().default(false), + enabled: z.boolean().default(true), + show_star_count: z.boolean().default(true), + color: z.number().nullable().default(null), }); -export type TStarboardOpts = t.TypeOf<typeof StarboardOpts>; +export type TStarboardOpts = z.infer<typeof zStarboardOpts>; -export const ConfigSchema = t.type({ - boards: t.record(t.string, StarboardOpts), - can_migrate: t.boolean, +export const zStarboardConfig = z.strictObject({ + boards: zBoundedRecord( + z.record(z.string(), zStarboardOpts), + 0, + 100, + ), + can_migrate: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; - -export const PartialConfigSchema = tDeepPartial(ConfigSchema); - -export const defaultStarboardOpts: Partial<TStarboardOpts> = { - star_emoji: ["⭐"], - enabled: true, - show_star_count: true, - color: null, -}; export interface StarboardPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zStarboardConfig>; state: { savedMessages: GuildSavedMessages; diff --git a/backend/src/plugins/Starboard/util/preprocessStaticConfig.ts b/backend/src/plugins/Starboard/util/preprocessStaticConfig.ts deleted file mode 100644 index 57258759..00000000 --- a/backend/src/plugins/Starboard/util/preprocessStaticConfig.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as t from "io-ts"; -import { defaultStarboardOpts, PartialConfigSchema } from "../types"; - -export function preprocessStaticConfig(config: t.TypeOf<typeof PartialConfigSchema>) { - if (config.boards) { - for (const [name, opts] of Object.entries(config.boards)) { - config.boards[name] = Object.assign({}, defaultStarboardOpts, opts); - } - } - - return config; -} diff --git a/backend/src/plugins/Tags/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts index 3e0a5bb4..c68f90bf 100644 --- a/backend/src/plugins/Tags/TagsPlugin.ts +++ b/backend/src/plugins/Tags/TagsPlugin.ts @@ -2,7 +2,6 @@ import { Snowflake } from "discord.js"; import humanizeDuration from "humanize-duration"; import { PluginOptions } from "knub"; import moment from "moment-timezone"; -import { parseIoTsSchema, StrictValidationError } from "src/validatorUtils"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; @@ -19,7 +18,7 @@ import { TagListCmd } from "./commands/TagListCmd"; import { TagSourceCmd } from "./commands/TagSourceCmd"; import { generateTemplateMarkdown } from "./docs"; import { TemplateFunctions } from "./templateFunctions"; -import { ConfigSchema, TagsPluginType } from "./types"; +import { TagsPluginType, zTagsConfig } from "./types"; import { findTagByName } from "./util/findTagByName"; import { onMessageCreate } from "./util/onMessageCreate"; import { onMessageDelete } from "./util/onMessageDelete"; @@ -71,7 +70,7 @@ export const TagsPlugin = zeppelinGuildPlugin<TagsPluginType>()({ ${generateTemplateMarkdown(TemplateFunctions)} `), - configSchema: ConfigSchema, + configSchema: zTagsConfig, }, dependencies: () => [LogsPlugin], @@ -96,28 +95,7 @@ export const TagsPlugin = zeppelinGuildPlugin<TagsPluginType>()({ findTagByName: mapToPublicFn(findTagByName), }, - configParser(_input) { - const input = _input as any; - - if (input.delete_with_command && input.auto_delete_command) { - throw new StrictValidationError([ - `Cannot have both (global) delete_with_command and global_delete_invoke enabled`, - ]); - } - - // Check each category for conflicting options - if (input.categories) { - for (const [name, cat] of Object.entries(input.categories)) { - if ((cat as any).delete_with_command && (cat as any).auto_delete_command) { - throw new StrictValidationError([ - `Cannot have both (category specific) delete_with_command and category_delete_invoke enabled at <categories/${name}>`, - ]); - } - } - } - - return parseIoTsSchema(ConfigSchema, input); - }, + configParser: (input) => zTagsConfig.parse(input), beforeLoad(pluginData) { const { state, guild } = pluginData; diff --git a/backend/src/plugins/Tags/types.ts b/backend/src/plugins/Tags/types.ts index 3a7eb2b3..441906fe 100644 --- a/backend/src/plugins/Tags/types.ts +++ b/backend/src/plugins/Tags/types.ts @@ -1,52 +1,63 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildTags } from "../../data/GuildTags"; -import { tEmbed, tNullable } from "../../utils"; +import { zEmbedInput } from "../../utils"; -export const Tag = t.union([t.string, tEmbed]); -export type TTag = t.TypeOf<typeof Tag>; +export const zTag = z.union([z.string(), zEmbedInput]); +export type TTag = z.infer<typeof zTag>; -export const TagCategory = t.type({ - prefix: tNullable(t.string), - delete_with_command: tNullable(t.boolean), +export const zTagCategory = z.strictObject({ + prefix: z.string().nullable().default(null), + delete_with_command: z.boolean().default(false), - user_tag_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag - user_category_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag category - global_tag_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per tag - allow_mentions: tNullable(t.boolean), // Per user, per category - global_category_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per category - auto_delete_command: tNullable(t.boolean), // Any tag, per tag category + user_tag_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Per user, per tag + user_category_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Per user, per tag category + global_tag_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Any user, per tag + allow_mentions: z.boolean().nullable().default(null), + global_category_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Any user, per category + auto_delete_command: z.boolean().nullable().default(null), - tags: t.record(t.string, Tag), + tags: z.record(z.string(), zTag), - can_use: tNullable(t.boolean), -}); -export type TTagCategory = t.TypeOf<typeof TagCategory>; + can_use: z.boolean().nullable().default(null), +}) + .refine( + (parsed) => ! (parsed.auto_delete_command && parsed.delete_with_command), + { + message: "Cannot have both (category specific) delete_with_command and auto_delete_command enabled", + }, + ); +export type TTagCategory = z.infer<typeof zTagCategory>; -export const ConfigSchema = t.type({ - prefix: t.string, - delete_with_command: t.boolean, +export const zTagsConfig = z.strictObject({ + prefix: z.string(), + delete_with_command: z.boolean(), - user_tag_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag - global_tag_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per tag - user_cooldown: tNullable(t.union([t.string, t.number])), // Per user - allow_mentions: t.boolean, // Per user - global_cooldown: tNullable(t.union([t.string, t.number])), // Any tag use - auto_delete_command: t.boolean, // Any tag + user_tag_cooldown: z.union([z.string(), z.number()]).nullable(), // Per user, per tag + global_tag_cooldown: z.union([z.string(), z.number()]).nullable(), // Any user, per tag + user_cooldown: z.union([z.string(), z.number()]).nullable(), // Per user + allow_mentions: z.boolean(), // Per user + global_cooldown: z.union([z.string(), z.number()]).nullable(), // Any tag use + auto_delete_command: z.boolean(), // Any tag - categories: t.record(t.string, TagCategory), + categories: z.record(z.string(), zTagCategory), - can_create: t.boolean, - can_use: t.boolean, - can_list: t.boolean, -}); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; + can_create: z.boolean(), + can_use: z.boolean(), + can_list: z.boolean(), +}) +.refine( + (parsed) => ! (parsed.auto_delete_command && parsed.delete_with_command), + { + message: "Cannot have both (category specific) delete_with_command and auto_delete_command enabled", + }, +); export interface TagsPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zTagsConfig>; state: { archives: GuildArchives; tags: GuildTags; diff --git a/backend/src/plugins/Tags/util/findTagByName.ts b/backend/src/plugins/Tags/util/findTagByName.ts index 1b877f33..98fafc3f 100644 --- a/backend/src/plugins/Tags/util/findTagByName.ts +++ b/backend/src/plugins/Tags/util/findTagByName.ts @@ -1,12 +1,11 @@ -import * as t from "io-ts"; import { ExtendedMatchParams, GuildPluginData } from "knub"; -import { Tag, TagsPluginType } from "../types"; +import { TTag, TagsPluginType } from "../types"; export async function findTagByName( pluginData: GuildPluginData<TagsPluginType>, name: string, matchParams: ExtendedMatchParams = {}, -): Promise<t.TypeOf<typeof Tag> | null> { +): Promise<TTag | null> { const config = await pluginData.config.getMatchingConfig(matchParams); // Tag from a hardcoded category diff --git a/backend/src/plugins/Tags/util/onMessageCreate.ts b/backend/src/plugins/Tags/util/onMessageCreate.ts index 12c6cf8d..f5b0b04a 100644 --- a/backend/src/plugins/Tags/util/onMessageCreate.ts +++ b/backend/src/plugins/Tags/util/onMessageCreate.ts @@ -2,9 +2,8 @@ import { Snowflake, TextChannel } from "discord.js"; import { GuildPluginData } from "knub"; import { erisAllowedMentionsToDjsMentionOptions } from "src/utils/erisAllowedMentionsToDjsMentionOptions"; import { SavedMessage } from "../../../data/entities/SavedMessage"; -import { convertDelayStringToMS, resolveMember, tStrictMessageContent } from "../../../utils"; +import { convertDelayStringToMS, resolveMember, zStrictMessageContent } from "../../../utils"; import { messageIsEmpty } from "../../../utils/messageIsEmpty"; -import { validate } from "../../../validatorUtils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { TagsPluginType } from "../types"; import { matchAndRenderTagFromString } from "./matchAndRenderTagFromString"; @@ -85,10 +84,10 @@ export async function onMessageCreate(pluginData: GuildPluginData<TagsPluginType pluginData.cooldowns.setCooldown(cd[0], cd[1]); } - const validationError = await validate(tStrictMessageContent, tagResult.renderedContent); - if (validationError) { + const validated = zStrictMessageContent.safeParse(tagResult.renderedContent); + if (! validated.success) { pluginData.getPlugin(LogsPlugin).logBotAlert({ - body: `Rendering tag ${tagResult.tagName} resulted in an invalid message: ${validationError.message}`, + body: `Rendering tag ${tagResult.tagName} resulted in an invalid message: ${validated.error.message}`, }); return; } diff --git a/backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts b/backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts index f4d5ded0..885604a2 100644 --- a/backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts +++ b/backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts @@ -1,6 +1,6 @@ import { PluginOptions } from "knub"; import { GuildMemberTimezones } from "../../data/GuildMemberTimezones"; -import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils"; +import { mapToPublicFn } from "../../pluginUtils"; import { trimPluginDescription } from "../../utils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { ResetTimezoneCmd } from "./commands/ResetTimezoneCmd"; @@ -12,7 +12,7 @@ import { getGuildTz } from "./functions/getGuildTz"; import { getMemberTz } from "./functions/getMemberTz"; import { inGuildTz } from "./functions/inGuildTz"; import { inMemberTz } from "./functions/inMemberTz"; -import { ConfigSchema, TimeAndDatePluginType } from "./types"; +import { TimeAndDatePluginType, zTimeAndDateConfig } from "./types"; const defaultOptions: PluginOptions<TimeAndDatePluginType> = { config: { @@ -39,10 +39,10 @@ export const TimeAndDatePlugin = zeppelinGuildPlugin<TimeAndDatePluginType>()({ description: trimPluginDescription(` Allows controlling the displayed time/date formats and timezones `), - configSchema: ConfigSchema, + configSchema: zTimeAndDateConfig, }, - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zTimeAndDateConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/TimeAndDate/types.ts b/backend/src/plugins/TimeAndDate/types.ts index 102d620b..7a942518 100644 --- a/backend/src/plugins/TimeAndDate/types.ts +++ b/backend/src/plugins/TimeAndDate/types.ts @@ -1,19 +1,21 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginMessageCommand } from "knub"; +import { U } from "ts-toolbelt"; +import z from "zod"; import { GuildMemberTimezones } from "../../data/GuildMemberTimezones"; -import { tNullable, tPartialDictionary } from "../../utils"; -import { tValidTimezone } from "../../utils/tValidTimezone"; +import { keys } from "../../utils"; +import { zValidTimezone } from "../../utils/zValidTimezone"; import { defaultDateFormats } from "./defaultDateFormats"; -export const ConfigSchema = t.type({ - timezone: tValidTimezone, - date_formats: tNullable(tPartialDictionary(t.keyof(defaultDateFormats), t.string)), - can_set_timezone: t.boolean, +const zDateFormatKeys = z.enum(keys(defaultDateFormats) as U.ListOf<keyof typeof defaultDateFormats>); + +export const zTimeAndDateConfig = z.strictObject({ + timezone: zValidTimezone(z.string()), + date_formats: z.record(zDateFormatKeys, z.string()).nullable(), + can_set_timezone: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface TimeAndDatePluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zTimeAndDateConfig>; state: { memberTimezones: GuildMemberTimezones; }; diff --git a/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts b/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts index 3bdb310c..e64d3c98 100644 --- a/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts +++ b/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts @@ -1,16 +1,14 @@ -import * as t from "io-ts"; import { Queue } from "../../Queue"; import { UsernameHistory } from "../../data/UsernameHistory"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { MessageCreateUpdateUsernameEvt, VoiceChannelJoinUpdateUsernameEvt } from "./events/UpdateUsernameEvts"; -import { UsernameSaverPluginType } from "./types"; +import { UsernameSaverPluginType, zUsernameSaverConfig } from "./types"; export const UsernameSaverPlugin = zeppelinGuildPlugin<UsernameSaverPluginType>()({ name: "username_saver", showInDocs: false, - configParser: makeIoTsConfigParser(t.type({})), + configParser: (input) => zUsernameSaverConfig.parse(input), // prettier-ignore events: [ diff --git a/backend/src/plugins/UsernameSaver/types.ts b/backend/src/plugins/UsernameSaver/types.ts index d3c5518f..16d6e342 100644 --- a/backend/src/plugins/UsernameSaver/types.ts +++ b/backend/src/plugins/UsernameSaver/types.ts @@ -1,8 +1,12 @@ import { BasePluginType, guildPluginEventListener } from "knub"; +import z from "zod"; import { Queue } from "../../Queue"; import { UsernameHistory } from "../../data/UsernameHistory"; +export const zUsernameSaverConfig = z.strictObject({}); + export interface UsernameSaverPluginType extends BasePluginType { + config: z.infer<typeof zUsernameSaverConfig>; state: { usernameHistory: UsernameHistory; updateQueue: Queue; diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index 68f6987a..efb892ec 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -5,7 +5,7 @@ import { GuildCases } from "../../data/GuildCases"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { Supporters } from "../../data/Supporters"; -import { makeIoTsConfigParser, sendSuccessMessage } from "../../pluginUtils"; +import { sendSuccessMessage } from "../../pluginUtils"; import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { ModActionsPlugin } from "../ModActions/ModActionsPlugin"; @@ -42,7 +42,7 @@ import { getUserInfoEmbed } from "./functions/getUserInfoEmbed"; import { hasPermission } from "./functions/hasPermission"; import { activeReloads } from "./guildReloads"; import { refreshMembersIfNeeded } from "./refreshMembers"; -import { ConfigSchema, UtilityPluginType } from "./types"; +import { UtilityPluginType, zUtilityConfig } from "./types"; const defaultOptions: PluginOptions<UtilityPluginType> = { config: { @@ -117,11 +117,11 @@ export const UtilityPlugin = zeppelinGuildPlugin<UtilityPluginType>()({ showInDocs: true, info: { prettyName: "Utility", - configSchema: ConfigSchema, + configSchema: zUtilityConfig, }, dependencies: () => [TimeAndDatePlugin, ModActionsPlugin, LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zUtilityConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index 19710b58..dbaacf27 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -14,10 +14,9 @@ import { ArgsFromSignatureOrArray, GuildPluginData } from "knub"; import moment from "moment-timezone"; import { RegExpRunner, allowTimeout } from "../../RegExpRunner"; import { getBaseUrl, sendErrorMessage } from "../../pluginUtils"; -import { MINUTES, multiSorter, renderUserUsername, sorter, trimLines } from "../../utils"; +import { InvalidRegexError, MINUTES, inputPatternToRegExp, multiSorter, renderUserUsername, sorter, trimLines } from "../../utils"; import { asyncFilter } from "../../utils/async"; import { hasDiscordPermissions } from "../../utils/hasDiscordPermissions"; -import { InvalidRegexError, inputPatternToRegExp } from "../../validatorUtils"; import { banSearchSignature } from "./commands/BanSearchCmd"; import { searchCmdSignature } from "./commands/SearchCmd"; import { getUserInfoEmbed } from "./functions/getUserInfoEmbed"; diff --git a/backend/src/plugins/Utility/types.ts b/backend/src/plugins/Utility/types.ts index aaac036a..77b3d8ac 100644 --- a/backend/src/plugins/Utility/types.ts +++ b/backend/src/plugins/Utility/types.ts @@ -1,5 +1,5 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; +import z from "zod"; import { RegExpRunner } from "../../RegExpRunner"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildCases } from "../../data/GuildCases"; @@ -7,39 +7,38 @@ import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { Supporters } from "../../data/Supporters"; -export const ConfigSchema = t.type({ - can_roles: t.boolean, - can_level: t.boolean, - can_search: t.boolean, - can_clean: t.boolean, - can_info: t.boolean, - can_server: t.boolean, - can_inviteinfo: t.boolean, - can_channelinfo: t.boolean, - can_messageinfo: t.boolean, - can_userinfo: t.boolean, - can_roleinfo: t.boolean, - can_emojiinfo: t.boolean, - can_snowflake: t.boolean, - can_reload_guild: t.boolean, - can_nickname: t.boolean, - can_ping: t.boolean, - can_source: t.boolean, - can_vcmove: t.boolean, - can_vckick: t.boolean, - can_help: t.boolean, - can_about: t.boolean, - can_context: t.boolean, - can_jumbo: t.boolean, - jumbo_size: t.Integer, - can_avatar: t.boolean, - info_on_single_result: t.boolean, - autojoin_threads: t.boolean, +export const zUtilityConfig = z.strictObject({ + can_roles: z.boolean(), + can_level: z.boolean(), + can_search: z.boolean(), + can_clean: z.boolean(), + can_info: z.boolean(), + can_server: z.boolean(), + can_inviteinfo: z.boolean(), + can_channelinfo: z.boolean(), + can_messageinfo: z.boolean(), + can_userinfo: z.boolean(), + can_roleinfo: z.boolean(), + can_emojiinfo: z.boolean(), + can_snowflake: z.boolean(), + can_reload_guild: z.boolean(), + can_nickname: z.boolean(), + can_ping: z.boolean(), + can_source: z.boolean(), + can_vcmove: z.boolean(), + can_vckick: z.boolean(), + can_help: z.boolean(), + can_about: z.boolean(), + can_context: z.boolean(), + can_jumbo: z.boolean(), + jumbo_size: z.number(), + can_avatar: z.boolean(), + info_on_single_result: z.boolean(), + autojoin_threads: z.boolean(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface UtilityPluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zUtilityConfig>; state: { logs: GuildLogs; cases: GuildCases; diff --git a/backend/src/plugins/WelcomeMessage/WelcomeMessagePlugin.ts b/backend/src/plugins/WelcomeMessage/WelcomeMessagePlugin.ts index e007b968..ba8f9de3 100644 --- a/backend/src/plugins/WelcomeMessage/WelcomeMessagePlugin.ts +++ b/backend/src/plugins/WelcomeMessage/WelcomeMessagePlugin.ts @@ -1,10 +1,9 @@ import { PluginOptions } from "knub"; import { GuildLogs } from "../../data/GuildLogs"; -import { makeIoTsConfigParser } from "../../pluginUtils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { SendWelcomeMessageEvt } from "./events/SendWelcomeMessageEvt"; -import { ConfigSchema, WelcomeMessagePluginType } from "./types"; +import { WelcomeMessagePluginType, zWelcomeMessageConfig } from "./types"; const defaultOptions: PluginOptions<WelcomeMessagePluginType> = { config: { @@ -19,11 +18,11 @@ export const WelcomeMessagePlugin = zeppelinGuildPlugin<WelcomeMessagePluginType showInDocs: true, info: { prettyName: "Welcome message", - configSchema: ConfigSchema, + configSchema: zWelcomeMessageConfig, }, dependencies: () => [LogsPlugin], - configParser: makeIoTsConfigParser(ConfigSchema), + configParser: (input) => zWelcomeMessageConfig.parse(input), defaultOptions, // prettier-ignore diff --git a/backend/src/plugins/WelcomeMessage/types.ts b/backend/src/plugins/WelcomeMessage/types.ts index 682cfe39..25e9afb1 100644 --- a/backend/src/plugins/WelcomeMessage/types.ts +++ b/backend/src/plugins/WelcomeMessage/types.ts @@ -1,17 +1,15 @@ -import * as t from "io-ts"; import { BasePluginType, guildPluginEventListener } from "knub"; +import z from "zod"; import { GuildLogs } from "../../data/GuildLogs"; -import { tNullable } from "../../utils"; -export const ConfigSchema = t.type({ - send_dm: t.boolean, - send_to_channel: tNullable(t.string), - message: tNullable(t.string), +export const zWelcomeMessageConfig = z.strictObject({ + send_dm: z.boolean(), + send_to_channel: z.string().nullable(), + message: z.string().nullable(), }); -export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export interface WelcomeMessagePluginType extends BasePluginType { - config: TConfigSchema; + config: z.infer<typeof zWelcomeMessageConfig>; state: { logs: GuildLogs; sentWelcomeMessages: Set<string>; diff --git a/backend/src/plugins/ZeppelinPluginBlueprint.ts b/backend/src/plugins/ZeppelinPluginBlueprint.ts index 165c67b5..aa266abc 100644 --- a/backend/src/plugins/ZeppelinPluginBlueprint.ts +++ b/backend/src/plugins/ZeppelinPluginBlueprint.ts @@ -1,4 +1,3 @@ -import * as t from "io-ts"; import { BasePluginType, globalPlugin, @@ -9,6 +8,7 @@ import { GuildPluginData, } from "knub"; import { TMarkdown } from "../types"; +import { ZodTypeAny } from "zod"; /** * GUILD PLUGINS @@ -23,7 +23,7 @@ export interface ZeppelinGuildPluginBlueprint<TPluginData extends GuildPluginDat usageGuide?: TMarkdown; configurationGuide?: TMarkdown; legacy?: boolean | string; - configSchema?: t.Type<any>; + configSchema?: ZodTypeAny; }; } diff --git a/backend/src/types.ts b/backend/src/types.ts index 45974aee..409c4dc6 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -1,5 +1,6 @@ -import * as t from "io-ts"; import { BaseConfig, Knub } from "knub"; +import z from "zod"; +import { zSnowflake } from "./utils"; export interface ZeppelinGuildConfig extends BaseConfig { success_emoji?: string; @@ -10,31 +11,19 @@ export interface ZeppelinGuildConfig extends BaseConfig { date_formats?: any; } -export const ZeppelinGuildConfigSchema = t.type({ +export const zZeppelinGuildConfig = z.strictObject({ // From BaseConfig - prefix: t.string, - levels: t.record(t.string, t.number), - plugins: t.record(t.string, t.unknown), + prefix: z.string().optional(), + levels: z.record(zSnowflake, z.number()).optional(), + plugins: z.record(z.string(), z.unknown()).optional(), // From ZeppelinGuildConfig - success_emoji: t.string, - error_emoji: t.string, + success_emoji: z.string().optional(), + error_emoji: z.string().optional(), // Deprecated - timezone: t.string, - date_formats: t.unknown, -}); -export const PartialZeppelinGuildConfigSchema = t.partial(ZeppelinGuildConfigSchema.props); - -export interface ZeppelinGlobalConfig extends BaseConfig { - url: string; - owners?: string[]; -} - -export const ZeppelinGlobalConfigSchema = t.type({ - url: t.string, - owners: t.array(t.string), - plugins: t.record(t.string, t.unknown), + timezone: z.string().optional(), + date_formats: z.unknown().optional(), }); export type TZeppelinKnub = Knub; diff --git a/backend/src/utils.test.ts b/backend/src/utils.test.ts index f5c9f935..461be47b 100644 --- a/backend/src/utils.test.ts +++ b/backend/src/utils.test.ts @@ -1,6 +1,6 @@ import test from "ava"; -import * as ioTs from "io-ts"; -import { convertDelayStringToMS, convertMSToDelayString, getUrlsInString, tAllowedMentions } from "./utils"; +import z from "zod"; +import { convertDelayStringToMS, convertMSToDelayString, getUrlsInString, zAllowedMentions } from "./utils"; import { ErisAllowedMentionFormat } from "./utils/erisAllowedMentionsToDjsMentionOptions"; type AssertEquals<TActual, TExpected> = TActual extends TExpected ? true : false; @@ -50,7 +50,7 @@ test("delay strings: reverse conversion (conservative)", (t) => { }); test("tAllowedMentions matches Eris's AllowedMentions", (t) => { - type TAllowedMentions = ioTs.TypeOf<typeof tAllowedMentions>; + type TAllowedMentions = z.infer<typeof zAllowedMentions>; // eslint-disable-next-line @typescript-eslint/no-unused-vars const typeTest: AssertEquals<TAllowedMentions, ErisAllowedMentionFormat> = true; t.pass(); diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 1bb4d6ce..641a2573 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -21,31 +21,26 @@ import { PartialChannelData, PartialMessage, RoleResolvable, - Snowflake, Sticker, TextBasedChannel, User, } from "discord.js"; import emojiRegex from "emoji-regex"; -import { either } from "fp-ts/lib/Either"; -import { unsafeCoerce } from "fp-ts/lib/function"; import fs from "fs"; import https from "https"; import humanizeDuration from "humanize-duration"; -import * as t from "io-ts"; import { isEqual } from "lodash"; -import moment from "moment-timezone"; import { performance } from "perf_hooks"; import tlds from "tlds"; import tmp from "tmp"; import { URL } from "url"; -import { z, ZodError } from "zod"; +import { z, ZodEffects, ZodError, ZodRecord, ZodString } from "zod"; import { ISavedMessageAttachmentData, SavedMessage } from "./data/entities/SavedMessage"; import { getProfiler } from "./profiler"; import { SimpleCache } from "./SimpleCache"; import { sendDM } from "./utils/sendDM"; +import { Brand } from "./utils/typeUtils"; import { waitForButtonConfirm } from "./utils/waitForInteraction"; -import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils"; const fsp = fs.promises; @@ -91,71 +86,9 @@ export function isDiscordAPIError(err: Error | string): err is DiscordAPIError { return err instanceof DiscordAPIError; } -export function tNullable<T extends t.Type<any, any>>(type: T) { - return t.union([type, t.undefined, t.null], `Nullable<${type.name}>`); -} - -export const tNormalizedNullOrUndefined = new t.Type<undefined, null | undefined>( - "tNormalizedNullOrUndefined", - (v): v is undefined => typeof v === "undefined", - (v, c) => (v == null ? t.success(undefined) : t.failure(v, c, "Value must be null or undefined")), - () => undefined, -); - -/** - * Similar to `tNullable`, but normalizes both `null` and `undefined` to `undefined`. - * This allows adding optional config options that can be "removed" by setting the value to `null`. - */ -export function tNormalizedNullOptional<T extends t.Type<any, any>>(type: T) { - return t.union( - [type, tNormalizedNullOrUndefined], - `Optional<${type.name}>`, // Simplified name for errors and config schema views - ); -} - -export type TDeepPartial<T> = T extends t.InterfaceType<any> - ? TDeepPartialProps<T["props"]> - : T extends t.DictionaryType<any, any> - ? t.DictionaryType<T["domain"], TDeepPartial<T["codomain"]>> - : T extends t.UnionType<any[]> - ? t.UnionType<Array<TDeepPartial<T["types"][number]>>> - : T extends t.IntersectionType<any> - ? t.IntersectionType<Array<TDeepPartial<T["types"][number]>>> - : T extends t.ArrayType<any> - ? t.ArrayType<TDeepPartial<T["type"]>> - : T; - -// Based on t.PartialC -export interface TDeepPartialProps<P extends t.Props> - extends t.PartialType< - P, - { - [K in keyof P]?: TDeepPartial<t.TypeOf<P[K]>>; - }, - { - [K in keyof P]?: TDeepPartial<t.OutputOf<P[K]>>; - } - > {} - -export function tDeepPartial<T>(type: T): TDeepPartial<T> { - if (type instanceof t.InterfaceType || type instanceof t.PartialType) { - const newProps = {}; - for (const [key, prop] of Object.entries(type.props)) { - newProps[key] = tDeepPartial(prop); - } - return t.partial(newProps) as TDeepPartial<T>; - } else if (type instanceof t.DictionaryType) { - return t.record(type.domain, tDeepPartial(type.codomain)) as TDeepPartial<T>; - } else if (type instanceof t.UnionType) { - return t.union(type.types.map((unionType) => tDeepPartial(unionType))) as TDeepPartial<T>; - } else if (type instanceof t.IntersectionType) { - const types = type.types.map((intersectionType) => tDeepPartial(intersectionType)); - return t.intersection(types as [t.Mixed, t.Mixed]) as unknown as TDeepPartial<T>; - } else if (type instanceof t.ArrayType) { - return t.array(tDeepPartial(type.type)) as TDeepPartial<T>; - } else { - return type as TDeepPartial<T>; - } +// null | undefined -> undefined +export function zNullishToUndefined<T extends z.ZodTypeAny>(type: T): ZodEffects<T, NonNullable<z.output<T>> | undefined> { + return type.transform(v => v ?? undefined); } export function getScalarDifference<T extends object>( @@ -207,29 +140,6 @@ export function differenceToString(diff: Map<string, { was: any; is: any }>): st // https://stackoverflow.com/a/49262929/316944 export type Not<T, E> = T & Exclude<T, E>; -// io-ts partial dictionary type -// From https://github.com/gcanti/io-ts/issues/429#issuecomment-655394345 -export interface PartialDictionaryC<D extends t.Mixed, C extends t.Mixed> - extends t.DictionaryType< - D, - C, - { - [K in t.TypeOf<D>]?: t.TypeOf<C>; - }, - { - [K in t.OutputOf<D>]?: t.OutputOf<C>; - }, - unknown - > {} - -export const tPartialDictionary = <D extends t.Mixed, C extends t.Mixed>( - domain: D, - codomain: C, - name?: string, -): PartialDictionaryC<D, C> => { - return unsafeCoerce(t.record(t.union([domain, t.undefined]), codomain, name)); -}; - export function nonNullish<V>(v: V): v is NonNullable<V> { return v != null; } @@ -240,70 +150,60 @@ export type GroupDMInvite = Invite & { type: typeof ChannelType.GroupDM; }; -/** - * Mirrors EmbedOptions from Eris - */ -export const tEmbed = t.type({ - title: tNullable(t.string), - description: tNullable(t.string), - url: tNullable(t.string), - timestamp: tNullable(t.string), - color: tNullable(t.number), - footer: tNullable( - t.type({ - text: t.string, - icon_url: tNullable(t.string), - proxy_icon_url: tNullable(t.string), - }), - ), - image: tNullable( - t.type({ - url: tNullable(t.string), - proxy_url: tNullable(t.string), - width: tNullable(t.number), - height: tNullable(t.number), - }), - ), - thumbnail: tNullable( - t.type({ - url: tNullable(t.string), - proxy_url: tNullable(t.string), - width: tNullable(t.number), - height: tNullable(t.number), - }), - ), - video: tNullable( - t.type({ - url: tNullable(t.string), - width: tNullable(t.number), - height: tNullable(t.number), - }), - ), - provider: tNullable( - t.type({ - name: t.string, - url: tNullable(t.string), - }), - ), - fields: tNullable( - t.array( - t.type({ - name: tNullable(t.string), - value: tNullable(t.string), - inline: tNullable(t.boolean), - }), - ), - ), - author: tNullable( - t.type({ - name: t.string, - url: tNullable(t.string), - width: tNullable(t.number), - height: tNullable(t.number), - }), - ), +function isBoundedString(str: unknown, min: number, max: number): str is string { + if (typeof str !== "string") { + return false; + } + return (str.length >= min && str.length <= max); +} + +export function zBoundedCharacters(min: number, max: number) { + return z.string().refine(str => { + const len = [...str].length; // Unicode aware character split + return (len >= min && len <= max); + }, { + message: `String must be between ${min} and ${max} characters long`, + }); +} + +export const zSnowflake = z.string().refine(str => isSnowflake(str), { + message: "Invalid snowflake ID", }); +const regexWithFlags = /^\/(.*?)\/([i]*)$/; + +export class InvalidRegexError extends Error {} + +/** + * This function supports two input syntaxes for regexes: /<pattern>/<flags> and just <pattern> + */ +export function inputPatternToRegExp(pattern: string) { + const advancedSyntaxMatch = pattern.match(regexWithFlags); + const [finalPattern, flags] = advancedSyntaxMatch ? [advancedSyntaxMatch[1], advancedSyntaxMatch[2]] : [pattern, ""]; + try { + return new RegExp(finalPattern, flags); + } catch (e) { + throw new InvalidRegexError(e.message); + } +} + +export function zRegex<T extends ZodString>(zStr: T) { + return zStr.transform((str, ctx) => { + try { + return inputPatternToRegExp(str); + } catch (err) { + if (err instanceof InvalidRegexError) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid regex" + }); + return z.NEVER; + } + throw err; + } + }); +} + export const zEmbedInput = z.object({ title: z.string().optional(), description: z.string().optional(), @@ -387,15 +287,7 @@ export type StrictMessageContent = { embeds?: APIEmbed[]; }; -export const tStrictMessageContent = t.type({ - content: tNullable(t.string), - tts: tNullable(t.boolean), - disableEveryone: tNullable(t.boolean), - embed: tNullable(tEmbed), - embeds: tNullable(t.array(tEmbed)), -}); - -export const tMessageContent = t.union([t.string, tStrictMessageContent]); +export const zMessageContent = z.union([zBoundedCharacters(0, 4000), zStrictMessageContent]); export function validateAndParseMessageContent(input: unknown): StrictMessageContent { if (input == null) { @@ -454,11 +346,11 @@ function dropNullValuesRecursively(obj: any) { /** * Mirrors AllowedMentions from Eris */ -export const tAllowedMentions = t.type({ - everyone: tNormalizedNullOptional(t.boolean), - users: tNormalizedNullOptional(t.union([t.boolean, t.array(t.string)])), - roles: tNormalizedNullOptional(t.union([t.boolean, t.array(t.string)])), - repliedUser: tNormalizedNullOptional(t.boolean), +export const zAllowedMentions = z.strictObject({ + everyone: zNullishToUndefined(z.boolean().nullable().optional()), + users: zNullishToUndefined(z.union([z.boolean(), z.array(z.string())]).nullable().optional()), + roles: zNullishToUndefined(z.union([z.boolean(), z.array(z.string())]).nullable().optional()), + replied_user: zNullishToUndefined(z.boolean().nullable().optional()), }); export function dropPropertiesByName(obj, propName) { @@ -472,39 +364,18 @@ export function dropPropertiesByName(obj, propName) { } } -export const tAlphanumeric = new t.Type<string, string>( - "tAlphanumeric", - (s): s is string => typeof s === "string", - (from, to) => - either.chain(t.string.validate(from, to), (s) => { - return s.match(/\W/) ? t.failure(from, to, "String must be alphanumeric") : t.success(s); - }), - (s) => s, -); +export function zBoundedRecord<TRecord extends ZodRecord<any, any>>(record: TRecord, minKeys: number, maxKeys: number): ZodEffects<TRecord> { + return record.refine(data => { + const len = Object.keys(data).length; + return (len >= minKeys && len <= maxKeys); + }, { + message: `Object must have ${minKeys}-${maxKeys} keys`, + }); +} -export const tDateTime = new t.Type<string, string>( - "tDateTime", - (s): s is string => typeof s === "string", - (from, to) => - either.chain(t.string.validate(from, to), (s) => { - const parsed = - s.length === 10 ? moment.utc(s, "YYYY-MM-DD") : s.length === 19 ? moment.utc(s, "YYYY-MM-DD HH:mm:ss") : null; - - return parsed && parsed.isValid() ? t.success(s) : t.failure(from, to, "Invalid datetime"); - }), - (s) => s, -); - -export const tDelayString = new t.Type<string, string>( - "tDelayString", - (s): s is string => typeof s === "string", - (from, to) => - either.chain(t.string.validate(from, to), (s) => { - const ms = convertDelayStringToMS(s); - return ms === null ? t.failure(from, to, "Invalid delay string") : t.success(s); - }), - (s) => s, -); +export const zDelayString = z.string().max(32).refine(str => convertDelayStringToMS(str) !== null, { + message: "Invalid delay string", +}); // To avoid running into issues with the JS max date vaLue, we cap maximum delay strings *far* below that. // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#The_ECMAScript_epoch_and_timestamps @@ -609,9 +480,11 @@ export function stripObjectToScalars(obj, includedNested: string[] = []) { export const snowflakeRegex = /[1-9][0-9]{5,19}/; +export type Snowflake = Brand<string, "Snowflake">; + const isSnowflakeRegex = new RegExp(`^${snowflakeRegex.source}$`); -export function isSnowflake(v: string): boolean { - return isSnowflakeRegex.test(v); +export function isSnowflake(v: unknown): v is Snowflake { + return typeof v === "string" && isSnowflakeRegex.test(v); } export function sleep(ms: number): Promise<void> { @@ -1474,8 +1347,7 @@ export function messageLink(guildIdOrMessage: string | Message | null, channelId } export function isValidEmbed(embed: any): boolean { - const result = decodeAndValidateStrict(tEmbed, embed); - return !(result instanceof StrictValidationError); + return zEmbedInput.safeParse(embed).success; } const formatter = new Intl.NumberFormat("en-US"); @@ -1613,3 +1485,19 @@ export function renderUsername(username: string, discriminator: string): string export function renderUserUsername(user: User | UnknownUser): string { return renderUsername(user.username, user.discriminator); } + +type Entries<T> = Array<{ + [Key in keyof T]-?: [Key, T[Key]]; +}[keyof T]>; + +export function entries<T extends object>(object: T) { + return Object.entries(object) as Entries<T>; +} + +export function keys<T extends object>(object: T) { + return Object.keys(object) as Array<keyof T>; +} + +export function values<T extends object>(object: T) { + return Object.values(object) as Array<T[keyof T]>; +} diff --git a/backend/src/utils/iotsUtils.ts b/backend/src/utils/iotsUtils.ts deleted file mode 100644 index a3636c84..00000000 --- a/backend/src/utils/iotsUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as t from "io-ts"; - -interface BoundedStringBrand { - readonly BoundedString: unique symbol; -} - -export function asBoundedString(str: string) { - return str as t.Branded<string, BoundedStringBrand>; -} - -export function tBoundedString(min: number, max: number) { - return t.brand( - t.string, - (str): str is t.Branded<string, BoundedStringBrand> => (str.length >= min && str.length <= max), - "BoundedString", - ); -} diff --git a/backend/src/utils/tColor.ts b/backend/src/utils/tColor.ts deleted file mode 100644 index f240f7f4..00000000 --- a/backend/src/utils/tColor.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { either } from "fp-ts/lib/Either"; -import * as t from "io-ts"; -import { intToRgb } from "./intToRgb"; -import { parseColor } from "./parseColor"; -import { rgbToInt } from "./rgbToInt"; - -export const tColor = new t.Type<number, string>( - "tColor", - (s): s is number => typeof s === "number", - (from, to) => - either.chain(t.string.validate(from, to), (input) => { - const parsedColor = parseColor(input); - return parsedColor == null ? t.failure(from, to, "Invalid color") : t.success(rgbToInt(parsedColor)); - }), - (s) => intToRgb(s).join(","), -); diff --git a/backend/src/utils/tValidTimezone.ts b/backend/src/utils/tValidTimezone.ts deleted file mode 100644 index 35fc97c5..00000000 --- a/backend/src/utils/tValidTimezone.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { either } from "fp-ts/lib/Either"; -import * as t from "io-ts"; -import { isValidTimezone } from "./isValidTimezone"; - -export const tValidTimezone = new t.Type<string, string>( - "tValidTimezone", - (s): s is string => typeof s === "string", - (from, to) => - either.chain(t.string.validate(from, to), (input) => { - return isValidTimezone(input) ? t.success(input) : t.failure(from, to, `Invalid timezone: ${input}`); - }), - (s) => s, -); diff --git a/backend/src/utils/typeUtils.ts b/backend/src/utils/typeUtils.ts index 7c8b4cbd..98301581 100644 --- a/backend/src/utils/typeUtils.ts +++ b/backend/src/utils/typeUtils.ts @@ -14,3 +14,11 @@ export type Awaitable<T = unknown> = T | Promise<T>; export type DeepMutable<T> = { -readonly [P in keyof T]: DeepMutable<T[P]>; }; + +// From https://stackoverflow.com/a/70262876/316944 +export declare abstract class As<Tag extends keyof never> { + private static readonly $as$: unique symbol; + private [As.$as$]: Record<Tag, true>; +} + +export type Brand<T, B extends keyof never> = T & As<B>; diff --git a/backend/src/utils/zColor.ts b/backend/src/utils/zColor.ts new file mode 100644 index 00000000..1bc3ae92 --- /dev/null +++ b/backend/src/utils/zColor.ts @@ -0,0 +1,15 @@ +import z from "zod"; +import { parseColor } from "./parseColor"; +import { rgbToInt } from "./rgbToInt"; + +export const zColor = z.string().transform((val, ctx) => { + const parsedColor = parseColor(val); + if (parsedColor == null) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid color", + }); + return z.NEVER; + } + return rgbToInt(parsedColor); +}); diff --git a/backend/src/utils/zValidTimezone.ts b/backend/src/utils/zValidTimezone.ts new file mode 100644 index 00000000..b83c47c1 --- /dev/null +++ b/backend/src/utils/zValidTimezone.ts @@ -0,0 +1,8 @@ +import { ZodString } from "zod"; +import { isValidTimezone } from "./isValidTimezone"; + +export function zValidTimezone<Z extends ZodString>(z: Z) { + return z.refine((val) => isValidTimezone(val), { + message: "Invalid timezone", + }); +} diff --git a/backend/src/validation.test.ts b/backend/src/validation.test.ts deleted file mode 100644 index d41d6c4a..00000000 --- a/backend/src/validation.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import test from "ava"; -import * as t from "io-ts"; -import { tDeepPartial } from "./utils"; -import * as validatorUtils from "./validatorUtils"; - -test("tDeepPartial works", (ava) => { - const originalSchema = t.type({ - listOfThings: t.record( - t.string, - t.type({ - enabled: t.boolean, - someValue: t.number, - }), - ), - }); - - const deepPartialSchema = tDeepPartial(originalSchema); - - const partialValidValue = { - listOfThings: { - myThing: { - someValue: 5, - }, - }, - }; - - const partialErrorValue = { - listOfThings: { - myThing: { - someValue: "test", - }, - }, - }; - - const result1 = validatorUtils.validate(deepPartialSchema, partialValidValue); - ava.is(result1, null); - - const result2 = validatorUtils.validate(deepPartialSchema, partialErrorValue); - ava.not(result2, null); -}); diff --git a/backend/src/validatorUtils.ts b/backend/src/validatorUtils.ts deleted file mode 100644 index 31139596..00000000 --- a/backend/src/validatorUtils.ts +++ /dev/null @@ -1,140 +0,0 @@ -import deepDiff from "deep-diff"; -import { either, fold, isLeft } from "fp-ts/lib/Either"; -import { pipe } from "fp-ts/lib/pipeable"; -import * as t from "io-ts"; -import { noop } from "./utils"; - -const regexWithFlags = /^\/(.*?)\/([i]*)$/; - -export class InvalidRegexError extends Error {} - -/** - * This function supports two input syntaxes for regexes: /<pattern>/<flags> and just <pattern> - */ -export function inputPatternToRegExp(pattern: string) { - const advancedSyntaxMatch = pattern.match(regexWithFlags); - const [finalPattern, flags] = advancedSyntaxMatch ? [advancedSyntaxMatch[1], advancedSyntaxMatch[2]] : [pattern, ""]; - try { - return new RegExp(finalPattern, flags); - } catch (e) { - throw new InvalidRegexError(e.message); - } -} - -export const TRegex = new t.Type<RegExp, string>( - "TRegex", - (s): s is RegExp => s instanceof RegExp, - (from, to) => - either.chain(t.string.validate(from, to), (s) => { - try { - return t.success(inputPatternToRegExp(s)); - } catch (err) { - if (err instanceof InvalidRegexError) { - return t.failure(s, [], err.message); - } - - throw err; - } - }), - (s) => `/${s.source}/${s.flags}`, -); - -// From io-ts/lib/PathReporter -function stringify(v) { - if (typeof v === "function") { - return t.getFunctionName(v); - } - if (typeof v === "number" && !isFinite(v)) { - if (isNaN(v)) { - return "NaN"; - } - return v > 0 ? "Infinity" : "-Infinity"; - } - return JSON.stringify(v); -} - -export class StrictValidationError extends Error { - private readonly errors; - - constructor(errors: string[]) { - errors = Array.from(new Set(errors)); - super(errors.join("\n")); - this.errors = errors; - } - getErrors() { - return this.errors; - } -} - -const report = fold((errors: any): StrictValidationError | void => { - const errorStrings = errors.map((err) => { - const context = err.context.map((c) => c.key).filter((k) => k && !k.startsWith("{")); - while (context.length > 0 && !isNaN(context[context.length - 1])) context.splice(-1); - - const value = stringify(err.value); - return value === undefined - ? `<${context.join("/")}> is required` - : `Invalid value supplied to <${context.join("/")}>${err.message ? `: ${err.message}` : ""}`; - }); - - return new StrictValidationError(errorStrings); -}, noop); - -export function validate(schema: t.Type<any>, value: any): StrictValidationError | null { - const validationResult = schema.decode(value); - return ( - pipe( - validationResult, - fold( - () => report(validationResult), - () => null, - ), - ) || null - ); -} - -export function parseIoTsSchema<T extends t.Type<any>>(schema: T, value: unknown): t.TypeOf<T> { - const decodeResult = schema.decode(value); - if (isLeft(decodeResult)) { - throw report(decodeResult); - } - return decodeResult.right; -} - -/** - * Decodes and validates the given value against the given schema while also disallowing extra properties - * See: https://github.com/gcanti/io-ts/issues/322 - */ -export function decodeAndValidateStrict<T extends t.HasProps>( - schema: T, - value: any, - debug = false, -): StrictValidationError | any { - const validationResult = t.exact(schema).decode(value); - return pipe( - validationResult, - fold( - () => report(validationResult), - (result) => { - // Make sure there are no extra properties - if (debug) { - // tslint:disable-next-line:no-console - console.log( - "JSON.stringify() check:", - JSON.stringify(value) === JSON.stringify(result) - ? "they are the same, no excess" - : "they are not the same, might have excess", - result, - ); - } - if (JSON.stringify(value) !== JSON.stringify(result)) { - const diff = deepDiff(result, value); - const errors = diff.filter((d) => d.kind === "N").map((d) => `Unknown property <${d.path.join(".")}>`); - if (errors.length) return new StrictValidationError(errors); - } - - return result; - }, - ), - ); -} diff --git a/package-lock.json b/package-lock.json index 6fe63dc2..d853c8db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "lint-staged": "^9.4.2", "prettier": "^2.8.4", "prettier-plugin-organize-imports": "^3.2.2", + "ts-toolbelt": "^9.6.0", "tsc-watch": "^6.0.4", "typescript": "^5.0.4" } @@ -3483,6 +3484,12 @@ "node": ">=8.0" } }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "dev": true + }, "node_modules/tsc-watch": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/tsc-watch/-/tsc-watch-6.0.4.tgz", diff --git a/package.json b/package.json index 0cec2093..0309d59c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "lint-staged": "^9.4.2", "prettier": "^2.8.4", "prettier-plugin-organize-imports": "^3.2.2", + "ts-toolbelt": "^9.6.0", "tsc-watch": "^6.0.4", "typescript": "^5.0.4" }, From 3db90907058b4a14ef52fcf8a862b93b66b4e819 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:03:45 +0000 Subject: [PATCH 26/51] fix: circular dependency in automod types --- backend/src/plugins/Automod/actions/ban.ts | 2 +- backend/src/plugins/Automod/actions/kick.ts | 2 +- backend/src/plugins/Automod/actions/mute.ts | 2 +- backend/src/plugins/Automod/actions/warn.ts | 2 +- backend/src/plugins/Automod/constants.ts | 6 ++++++ backend/src/plugins/Automod/types.ts | 5 ----- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/backend/src/plugins/Automod/actions/ban.ts b/backend/src/plugins/Automod/actions/ban.ts index dcc9563f..22cc8167 100644 --- a/backend/src/plugins/Automod/actions/ban.ts +++ b/backend/src/plugins/Automod/actions/ban.ts @@ -4,7 +4,7 @@ import { CaseArgs } from "../../Cases/types"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; -import { zNotify } from "../types"; +import { zNotify } from "../constants"; const configSchema = z.strictObject({ reason: zBoundedCharacters(0, 4000).nullable().default(null), diff --git a/backend/src/plugins/Automod/actions/kick.ts b/backend/src/plugins/Automod/actions/kick.ts index 5afba467..412309cd 100644 --- a/backend/src/plugins/Automod/actions/kick.ts +++ b/backend/src/plugins/Automod/actions/kick.ts @@ -4,7 +4,7 @@ import { CaseArgs } from "../../Cases/types"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; -import { zNotify } from "../types"; +import { zNotify } from "../constants"; export const KickAction = automodAction({ configSchema: z.strictObject({ diff --git a/backend/src/plugins/Automod/actions/mute.ts b/backend/src/plugins/Automod/actions/mute.ts index 4a2a42ad..c2f8e186 100644 --- a/backend/src/plugins/Automod/actions/mute.ts +++ b/backend/src/plugins/Automod/actions/mute.ts @@ -6,7 +6,7 @@ import { LogsPlugin } from "../../Logs/LogsPlugin"; import { MutesPlugin } from "../../Mutes/MutesPlugin"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; -import { zNotify } from "../types"; +import { zNotify } from "../constants"; export const MuteAction = automodAction({ configSchema: z.strictObject({ diff --git a/backend/src/plugins/Automod/actions/warn.ts b/backend/src/plugins/Automod/actions/warn.ts index e8c63340..7ccb3898 100644 --- a/backend/src/plugins/Automod/actions/warn.ts +++ b/backend/src/plugins/Automod/actions/warn.ts @@ -4,7 +4,7 @@ import { CaseArgs } from "../../Cases/types"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; -import { zNotify } from "../types"; +import { zNotify } from "../constants"; export const WarnAction = automodAction({ configSchema: z.strictObject({ diff --git a/backend/src/plugins/Automod/constants.ts b/backend/src/plugins/Automod/constants.ts index 53ff75ad..cc37f402 100644 --- a/backend/src/plugins/Automod/constants.ts +++ b/backend/src/plugins/Automod/constants.ts @@ -1,3 +1,4 @@ +import z from "zod"; import { MINUTES, SECONDS } from "../../utils"; export const RECENT_SPAM_EXPIRY_TIME = 10 * SECONDS; @@ -18,3 +19,8 @@ export enum RecentActionType { MemberLeave, ThreadCreate, } + +export const zNotify = z.union([ + z.literal("dm"), + z.literal("channel"), +]); diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index 59a09a33..251ef643 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -65,11 +65,6 @@ const zRule = z.strictObject({ }); export type TRule = z.infer<typeof zRule>; -export const zNotify = z.union([ - z.literal("dm"), - z.literal("channel"), -]); - export const zAutomodConfig = z.strictObject({ rules: zBoundedRecord( z.record(z.string().max(100), zRule), From ac8926cdb80a52874ba71ad8abb1e398db3d6966 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:05:01 +0000 Subject: [PATCH 27/51] chore: fix inconsistent import paths --- backend/src/data/GuildArchives.ts | 2 +- backend/src/plugins/Automod/actions/alert.ts | 2 +- backend/src/plugins/ContextMenus/actions/mute.ts | 4 ++-- backend/src/plugins/Logs/logFunctions/logCensor.ts | 2 +- backend/src/plugins/ModActions/functions/clearTempban.ts | 4 ++-- backend/src/plugins/Tags/docs.ts | 2 +- backend/src/plugins/Tags/util/onMessageCreate.ts | 2 +- backend/src/utils/templateSafeObjects.ts | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/src/data/GuildArchives.ts b/backend/src/data/GuildArchives.ts index de1e9d07..0451dfe0 100644 --- a/backend/src/data/GuildArchives.ts +++ b/backend/src/data/GuildArchives.ts @@ -1,6 +1,5 @@ import { Guild, Snowflake } from "discord.js"; import moment from "moment-timezone"; -import { isDefaultSticker } from "src/utils/isDefaultSticker"; import { Repository } from "typeorm"; import { TemplateSafeValueContainer, renderTemplate } from "../templateFormatter"; import { renderUsername, trimLines } from "../utils"; @@ -10,6 +9,7 @@ import { BaseGuildRepository } from "./BaseGuildRepository"; import { dataSource } from "./dataSource"; import { ArchiveEntry } from "./entities/ArchiveEntry"; import { SavedMessage } from "./entities/SavedMessage"; +import { isDefaultSticker } from "../utils/isDefaultSticker"; const DEFAULT_EXPIRY_DAYS = 30; diff --git a/backend/src/plugins/Automod/actions/alert.ts b/backend/src/plugins/Automod/actions/alert.ts index 1a767a0e..c5fcd4b3 100644 --- a/backend/src/plugins/Automod/actions/alert.ts +++ b/backend/src/plugins/Automod/actions/alert.ts @@ -1,5 +1,4 @@ import { Snowflake } from "discord.js"; -import { erisAllowedMentionsToDjsMentionOptions } from "src/utils/erisAllowedMentionsToDjsMentionOptions"; import z from "zod"; import { LogType } from "../../../data/LogType"; import { @@ -19,6 +18,7 @@ import { zNullishToUndefined, zSnowflake } from "../../../utils"; +import { erisAllowedMentionsToDjsMentionOptions } from "../../../utils/erisAllowedMentionsToDjsMentionOptions"; import { messageIsEmpty } from "../../../utils/messageIsEmpty"; import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { InternalPosterPlugin } from "../../InternalPoster/InternalPosterPlugin"; diff --git a/backend/src/plugins/ContextMenus/actions/mute.ts b/backend/src/plugins/ContextMenus/actions/mute.ts index 556ee248..c6eedbc6 100644 --- a/backend/src/plugins/ContextMenus/actions/mute.ts +++ b/backend/src/plugins/ContextMenus/actions/mute.ts @@ -1,14 +1,14 @@ import { ContextMenuCommandInteraction } from "discord.js"; import humanizeDuration from "humanize-duration"; import { GuildPluginData } from "knub"; -import { canActOn } from "src/pluginUtils"; -import { ModActionsPlugin } from "src/plugins/ModActions/ModActionsPlugin"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; import { convertDelayStringToMS } from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { MutesPlugin } from "../../Mutes/MutesPlugin"; import { ContextMenuPluginType } from "../types"; +import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; +import { canActOn } from "../../../pluginUtils"; export async function muteAction( pluginData: GuildPluginData<ContextMenuPluginType>, diff --git a/backend/src/plugins/Logs/logFunctions/logCensor.ts b/backend/src/plugins/Logs/logFunctions/logCensor.ts index cec6e441..c1853baf 100644 --- a/backend/src/plugins/Logs/logFunctions/logCensor.ts +++ b/backend/src/plugins/Logs/logFunctions/logCensor.ts @@ -1,11 +1,11 @@ import { GuildTextBasedChannel, User } from "discord.js"; import { GuildPluginData } from "knub"; import { deactivateMentions, disableCodeBlocks } from "knub/helpers"; -import { resolveChannelIds } from "src/utils/resolveChannelIds"; import { LogType } from "../../../data/LogType"; import { SavedMessage } from "../../../data/entities/SavedMessage"; import { createTypedTemplateSafeValueContainer } from "../../../templateFormatter"; import { UnknownUser } from "../../../utils"; +import { resolveChannelIds } from "../../../utils/resolveChannelIds"; import { channelToTemplateSafeChannel, savedMessageToTemplateSafeSavedMessage, diff --git a/backend/src/plugins/ModActions/functions/clearTempban.ts b/backend/src/plugins/ModActions/functions/clearTempban.ts index 25807df8..5173ca7e 100644 --- a/backend/src/plugins/ModActions/functions/clearTempban.ts +++ b/backend/src/plugins/ModActions/functions/clearTempban.ts @@ -2,8 +2,6 @@ import { Snowflake } from "discord.js"; import humanizeDuration from "humanize-duration"; import { GuildPluginData } from "knub"; import moment from "moment-timezone"; -import { LogType } from "src/data/LogType"; -import { logger } from "src/logger"; import { CaseTypes } from "../../../data/CaseTypes"; import { Tempban } from "../../../data/entities/Tempban"; import { resolveUser } from "../../../utils"; @@ -13,6 +11,8 @@ import { IgnoredEventType, ModActionsPluginType } from "../types"; import { formatReasonWithAttachments } from "./formatReasonWithAttachments"; import { ignoreEvent } from "./ignoreEvent"; import { isBanned } from "./isBanned"; +import { LogType } from "../../../data/LogType"; +import { logger } from "../../../logger"; export async function clearTempban(pluginData: GuildPluginData<ModActionsPluginType>, tempban: Tempban) { if (!(await isBanned(pluginData, tempban.user_id))) { diff --git a/backend/src/plugins/Tags/docs.ts b/backend/src/plugins/Tags/docs.ts index bc1608bb..61f4dfcb 100644 --- a/backend/src/plugins/Tags/docs.ts +++ b/backend/src/plugins/Tags/docs.ts @@ -1,4 +1,4 @@ -import { trimPluginDescription } from "src/utils"; +import { trimPluginDescription } from "../../utils"; import { TemplateFunction } from "./types"; export function generateTemplateMarkdown(definitions: TemplateFunction[]): string { diff --git a/backend/src/plugins/Tags/util/onMessageCreate.ts b/backend/src/plugins/Tags/util/onMessageCreate.ts index f5b0b04a..aa7c8490 100644 --- a/backend/src/plugins/Tags/util/onMessageCreate.ts +++ b/backend/src/plugins/Tags/util/onMessageCreate.ts @@ -1,12 +1,12 @@ import { Snowflake, TextChannel } from "discord.js"; import { GuildPluginData } from "knub"; -import { erisAllowedMentionsToDjsMentionOptions } from "src/utils/erisAllowedMentionsToDjsMentionOptions"; import { SavedMessage } from "../../../data/entities/SavedMessage"; import { convertDelayStringToMS, resolveMember, zStrictMessageContent } from "../../../utils"; import { messageIsEmpty } from "../../../utils/messageIsEmpty"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { TagsPluginType } from "../types"; import { matchAndRenderTagFromString } from "./matchAndRenderTagFromString"; +import { erisAllowedMentionsToDjsMentionOptions } from "../../../utils/erisAllowedMentionsToDjsMentionOptions"; export async function onMessageCreate(pluginData: GuildPluginData<TagsPluginType>, msg: SavedMessage) { if (msg.is_bot) return; diff --git a/backend/src/utils/templateSafeObjects.ts b/backend/src/utils/templateSafeObjects.ts index 67517f19..3395cd6f 100644 --- a/backend/src/utils/templateSafeObjects.ts +++ b/backend/src/utils/templateSafeObjects.ts @@ -13,7 +13,6 @@ import { User, } from "discord.js"; import { GuildPluginData } from "knub"; -import { UnknownUser, renderUserUsername } from "src/utils"; import { Case } from "../data/entities/Case"; import { ISavedMessageAttachmentData, @@ -27,6 +26,7 @@ import { TypedTemplateSafeValueContainer, ingestDataIntoTemplateSafeValueContainer, } from "../templateFormatter"; +import { UnknownUser, renderUserUsername } from "../utils"; type InputProps<T> = Omit< { From cbec80981daacd5e03ba88ac7c6e2357ff8fcffb Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:36:10 +0000 Subject: [PATCH 28/51] feat: improve ZodIssue rendering in config validation --- backend/src/configValidator.ts | 10 +++++++--- backend/src/utils/formatZodIssue.ts | 6 ++++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 backend/src/utils/formatZodIssue.ts diff --git a/backend/src/configValidator.ts b/backend/src/configValidator.ts index 96e08ba3..eb115d46 100644 --- a/backend/src/configValidator.ts +++ b/backend/src/configValidator.ts @@ -1,9 +1,10 @@ -import { PluginConfigManager } from "knub"; +import { ConfigValidationError, PluginConfigManager } from "knub"; import moment from "moment-timezone"; import { ZodError } from "zod"; import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin"; import { guildPlugins } from "./plugins/availablePlugins"; import { ZeppelinGuildConfig, zZeppelinGuildConfig } from "./types"; +import { formatZodIssue } from "./utils/formatZodIssue"; const pluginNameToPlugin = new Map<string, ZeppelinPlugin>(); for (const plugin of guildPlugins) { @@ -13,7 +14,7 @@ for (const plugin of guildPlugins) { export async function validateGuildConfig(config: any): Promise<string | null> { const validationResult = zZeppelinGuildConfig.safeParse(config); if (!validationResult.success) { - return validationResult.error.issues.join("\n"); + return validationResult.error.issues.map(formatZodIssue).join("\n"); } const guildConfig = config as ZeppelinGuildConfig; @@ -44,7 +45,10 @@ export async function validateGuildConfig(config: any): Promise<string | null> { await configManager.init(); } catch (err) { if (err instanceof ZodError) { - return `${pluginName}: ${err.issues.join("\n")}`; + return `${pluginName}: ${err.issues.map(formatZodIssue).join("\n")}`; + } + if (err instanceof ConfigValidationError) { + return `${pluginName}: ${err.message}`; } throw err; diff --git a/backend/src/utils/formatZodIssue.ts b/backend/src/utils/formatZodIssue.ts new file mode 100644 index 00000000..2b5c3d8e --- /dev/null +++ b/backend/src/utils/formatZodIssue.ts @@ -0,0 +1,6 @@ +import { ZodIssue } from "zod"; + +export function formatZodIssue(issue: ZodIssue): string { + const path = issue.path.join("/"); + return `${path}: ${issue.message}`; +} From 82d720d308a2998b731354200b384c317244a30e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:37:39 +0000 Subject: [PATCH 29/51] refactor: change LogType to a plain object instead of an enum --- backend/src/data/Configs.ts | 6 + backend/src/data/GuildLogs.ts | 10 +- backend/src/data/LogType.ts | 176 ++++++++---------- backend/src/plugins/Logs/types.ts | 19 +- backend/src/plugins/Logs/util/isLogIgnored.ts | 2 +- 5 files changed, 97 insertions(+), 116 deletions(-) diff --git a/backend/src/data/Configs.ts b/backend/src/data/Configs.ts index 1b9ade22..bc702b5e 100644 --- a/backend/src/data/Configs.ts +++ b/backend/src/data/Configs.ts @@ -27,6 +27,12 @@ export class Configs extends BaseRepository { this.configs = dataSource.getRepository(Config); } + getActive() { + return this.configs.find({ + where: { is_active: true }, + }); + } + getActiveByKey(key) { return this.configs.findOne({ where: { diff --git a/backend/src/data/GuildLogs.ts b/backend/src/data/GuildLogs.ts index b324984e..5f7dc4ed 100644 --- a/backend/src/data/GuildLogs.ts +++ b/backend/src/data/GuildLogs.ts @@ -5,7 +5,7 @@ import { LogType } from "./LogType"; const guildInstances: Map<string, GuildLogs> = new Map(); interface IIgnoredLog { - type: LogType; + type: keyof typeof LogType; ignoreId: any; } @@ -27,7 +27,7 @@ export class GuildLogs extends events.EventEmitter { guildInstances.set(guildId, this); } - log(type: LogType, data: any, ignoreId?: string) { + log(type: keyof typeof LogType, data: any, ignoreId?: string) { if (ignoreId && this.isLogIgnored(type, ignoreId)) { this.clearIgnoredLog(type, ignoreId); return; @@ -36,7 +36,7 @@ export class GuildLogs extends events.EventEmitter { this.emit("log", { type, data }); } - ignoreLog(type: LogType, ignoreId: any, timeout?: number) { + ignoreLog(type: keyof typeof LogType, ignoreId: any, timeout?: number) { this.ignoredLogs.push({ type, ignoreId }); // Clear after expiry (15sec by default) @@ -45,11 +45,11 @@ export class GuildLogs extends events.EventEmitter { }, timeout || 1000 * 15); } - isLogIgnored(type: LogType, ignoreId: any) { + isLogIgnored(type: keyof typeof LogType, ignoreId: any) { return this.ignoredLogs.some((info) => type === info.type && ignoreId === info.ignoreId); } - clearIgnoredLog(type: LogType, ignoreId: any) { + clearIgnoredLog(type: keyof typeof LogType, ignoreId: any) { this.ignoredLogs.splice( this.ignoredLogs.findIndex((info) => type === info.type && ignoreId === info.ignoreId), 1, diff --git a/backend/src/data/LogType.ts b/backend/src/data/LogType.ts index 90c81173..1aba45ec 100644 --- a/backend/src/data/LogType.ts +++ b/backend/src/data/LogType.ts @@ -1,102 +1,74 @@ -export enum LogType { - MEMBER_WARN = 1, - MEMBER_MUTE, - MEMBER_UNMUTE, - MEMBER_MUTE_EXPIRED, - MEMBER_KICK, - MEMBER_BAN, - MEMBER_UNBAN, - MEMBER_FORCEBAN, - MEMBER_SOFTBAN, - MEMBER_JOIN, - MEMBER_LEAVE, - MEMBER_ROLE_ADD, - MEMBER_ROLE_REMOVE, - MEMBER_NICK_CHANGE, - MEMBER_USERNAME_CHANGE, - MEMBER_RESTORE, - - CHANNEL_CREATE, - CHANNEL_DELETE, - CHANNEL_UPDATE, - - THREAD_CREATE, - THREAD_DELETE, - THREAD_UPDATE, - - ROLE_CREATE, - ROLE_DELETE, - ROLE_UPDATE, - - MESSAGE_EDIT, - MESSAGE_DELETE, - MESSAGE_DELETE_BULK, - MESSAGE_DELETE_BARE, - - VOICE_CHANNEL_JOIN, - VOICE_CHANNEL_LEAVE, - VOICE_CHANNEL_MOVE, - - STAGE_INSTANCE_CREATE, - STAGE_INSTANCE_DELETE, - STAGE_INSTANCE_UPDATE, - - EMOJI_CREATE, - EMOJI_DELETE, - EMOJI_UPDATE, - - STICKER_CREATE, - STICKER_DELETE, - STICKER_UPDATE, - - COMMAND, - - MESSAGE_SPAM_DETECTED, - CENSOR, - CLEAN, - - CASE_CREATE, - - MASSUNBAN, - MASSBAN, - MASSMUTE, - - MEMBER_TIMED_MUTE, - MEMBER_TIMED_UNMUTE, - MEMBER_TIMED_BAN, - MEMBER_TIMED_UNBAN, - - MEMBER_JOIN_WITH_PRIOR_RECORDS, - OTHER_SPAM_DETECTED, - - MEMBER_ROLE_CHANGES, - VOICE_CHANNEL_FORCE_MOVE, - VOICE_CHANNEL_FORCE_DISCONNECT, - - CASE_UPDATE, - - MEMBER_MUTE_REJOIN, - - SCHEDULED_MESSAGE, - POSTED_SCHEDULED_MESSAGE, - - BOT_ALERT, - AUTOMOD_ACTION, - - SCHEDULED_REPEATED_MESSAGE, - REPEATED_MESSAGE, - - MESSAGE_DELETE_AUTO, - - SET_ANTIRAID_USER, - SET_ANTIRAID_AUTO, - - MASS_ASSIGN_ROLES, - MASS_UNASSIGN_ROLES, - - MEMBER_NOTE, - - CASE_DELETE, - - DM_FAILED, -} +export const LogType = { + MEMBER_WARN: "MEMBER_WARN", + MEMBER_MUTE: "MEMBER_MUTE", + MEMBER_UNMUTE: "MEMBER_UNMUTE", + MEMBER_MUTE_EXPIRED: "MEMBER_MUTE_EXPIRED", + MEMBER_KICK: "MEMBER_KICK", + MEMBER_BAN: "MEMBER_BAN", + MEMBER_UNBAN: "MEMBER_UNBAN", + MEMBER_FORCEBAN: "MEMBER_FORCEBAN", + MEMBER_SOFTBAN: "MEMBER_SOFTBAN", + MEMBER_JOIN: "MEMBER_JOIN", + MEMBER_LEAVE: "MEMBER_LEAVE", + MEMBER_ROLE_ADD: "MEMBER_ROLE_ADD", + MEMBER_ROLE_REMOVE: "MEMBER_ROLE_REMOVE", + MEMBER_NICK_CHANGE: "MEMBER_NICK_CHANGE", + MEMBER_USERNAME_CHANGE: "MEMBER_USERNAME_CHANGE", + MEMBER_RESTORE: "MEMBER_RESTORE", + CHANNEL_CREATE: "CHANNEL_CREATE", + CHANNEL_DELETE: "CHANNEL_DELETE", + CHANNEL_UPDATE: "CHANNEL_UPDATE", + THREAD_CREATE: "THREAD_CREATE", + THREAD_DELETE: "THREAD_DELETE", + THREAD_UPDATE: "THREAD_UPDATE", + ROLE_CREATE: "ROLE_CREATE", + ROLE_DELETE: "ROLE_DELETE", + ROLE_UPDATE: "ROLE_UPDATE", + MESSAGE_EDIT: "MESSAGE_EDIT", + MESSAGE_DELETE: "MESSAGE_DELETE", + MESSAGE_DELETE_BULK: "MESSAGE_DELETE_BULK", + MESSAGE_DELETE_BARE: "MESSAGE_DELETE_BARE", + VOICE_CHANNEL_JOIN: "VOICE_CHANNEL_JOIN", + VOICE_CHANNEL_LEAVE: "VOICE_CHANNEL_LEAVE", + VOICE_CHANNEL_MOVE: "VOICE_CHANNEL_MOVE", + STAGE_INSTANCE_CREATE: "STAGE_INSTANCE_CREATE", + STAGE_INSTANCE_DELETE: "STAGE_INSTANCE_DELETE", + STAGE_INSTANCE_UPDATE: "STAGE_INSTANCE_UPDATE", + EMOJI_CREATE: "EMOJI_CREATE", + EMOJI_DELETE: "EMOJI_DELETE", + EMOJI_UPDATE: "EMOJI_UPDATE", + STICKER_CREATE: "STICKER_CREATE", + STICKER_DELETE: "STICKER_DELETE", + STICKER_UPDATE: "STICKER_UPDATE", + COMMAND: "COMMAND", + MESSAGE_SPAM_DETECTED: "MESSAGE_SPAM_DETECTED", + CENSOR: "CENSOR", + CLEAN: "CLEAN", + CASE_CREATE: "CASE_CREATE", + MASSUNBAN: "MASSUNBAN", + MASSBAN: "MASSBAN", + MASSMUTE: "MASSMUTE", + MEMBER_TIMED_MUTE: "MEMBER_TIMED_MUTE", + MEMBER_TIMED_UNMUTE: "MEMBER_TIMED_UNMUTE", + MEMBER_TIMED_BAN: "MEMBER_TIMED_BAN", + MEMBER_TIMED_UNBAN: "MEMBER_TIMED_UNBAN", + MEMBER_JOIN_WITH_PRIOR_RECORDS: "MEMBER_JOIN_WITH_PRIOR_RECORDS", + OTHER_SPAM_DETECTED: "OTHER_SPAM_DETECTED", + MEMBER_ROLE_CHANGES: "MEMBER_ROLE_CHANGES", + VOICE_CHANNEL_FORCE_MOVE: "VOICE_CHANNEL_FORCE_MOVE", + VOICE_CHANNEL_FORCE_DISCONNECT: "VOICE_CHANNEL_FORCE_DISCONNECT", + CASE_UPDATE: "CASE_UPDATE", + MEMBER_MUTE_REJOIN: "MEMBER_MUTE_REJOIN", + SCHEDULED_MESSAGE: "SCHEDULED_MESSAGE", + POSTED_SCHEDULED_MESSAGE: "POSTED_SCHEDULED_MESSAGE", + BOT_ALERT: "BOT_ALERT", + AUTOMOD_ACTION: "AUTOMOD_ACTION", + SCHEDULED_REPEATED_MESSAGE: "SCHEDULED_REPEATED_MESSAGE", + REPEATED_MESSAGE: "REPEATED_MESSAGE", + MESSAGE_DELETE_AUTO: "MESSAGE_DELETE_AUTO", + SET_ANTIRAID_USER: "SET_ANTIRAID_USER", + SET_ANTIRAID_AUTO: "SET_ANTIRAID_AUTO", + MEMBER_NOTE: "MEMBER_NOTE", + CASE_DELETE: "CASE_DELETE", + DM_FAILED: "DM_FAILED", +} as const; diff --git a/backend/src/plugins/Logs/types.ts b/backend/src/plugins/Logs/types.ts index 51f67b87..2960c2ae 100644 --- a/backend/src/plugins/Logs/types.ts +++ b/backend/src/plugins/Logs/types.ts @@ -1,12 +1,12 @@ import { BasePluginType, CooldownManager, guildPluginEventListener } from "knub"; -import { z } from "zod"; +import { ZodString, z } from "zod"; import { RegExpRunner } from "../../RegExpRunner"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildCases } from "../../data/GuildCases"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { LogType } from "../../data/LogType"; -import { zBoundedCharacters, zMessageContent, zRegex, zSnowflake } from "../../utils"; +import { keys, zBoundedCharacters, zMessageContent, zRegex, zSnowflake } from "../../utils"; import { MessageBuffer } from "../../utils/MessageBuffer"; import { TemplateSafeCase, @@ -26,10 +26,13 @@ const DEFAULT_BATCH_TIME = 1000; const MIN_BATCH_TIME = 250; const MAX_BATCH_TIME = 5000; -export const zLogFormats = z.record( - zBoundedCharacters(1, 255), - zMessageContent, -); +type ZLogFormatsHelper = { + -readonly [K in keyof typeof LogType]: typeof zMessageContent; +}; +export const zLogFormats = z.strictObject(keys(LogType).reduce((map, logType) => { + map[logType] = zMessageContent; + return map; +}, {} as ZLogFormatsHelper)); export type TLogFormats = z.infer<typeof zLogFormats>; const zLogChannel = z.strictObject({ @@ -44,7 +47,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.partial().default({}), timestamp_format: z.string().nullable().default(null), include_embed_timestamp: z.boolean().nullable().default(null), }); @@ -55,7 +58,7 @@ export type TLogChannelMap = z.infer<typeof zLogChannelMap>; export const zLogsConfig = z.strictObject({ channels: zLogChannelMap, - format: z.intersection(zLogFormats, z.strictObject({ + format: zLogFormats.merge(z.strictObject({ // Legacy/deprecated, use timestamp_format below instead timestamp: zBoundedCharacters(0, 64).nullable(), })), diff --git a/backend/src/plugins/Logs/util/isLogIgnored.ts b/backend/src/plugins/Logs/util/isLogIgnored.ts index eb23b927..71255f50 100644 --- a/backend/src/plugins/Logs/util/isLogIgnored.ts +++ b/backend/src/plugins/Logs/util/isLogIgnored.ts @@ -2,6 +2,6 @@ import { GuildPluginData } from "knub"; import { LogType } from "../../../data/LogType"; import { LogsPluginType } from "../types"; -export function isLogIgnored(pluginData: GuildPluginData<LogsPluginType>, type: LogType, ignoreId: string) { +export function isLogIgnored(pluginData: GuildPluginData<LogsPluginType>, type: keyof typeof LogType, ignoreId: string) { return pluginData.state.guildLogs.isLogIgnored(type, ignoreId); } From 5a5be89573337f701e1115a2e7c8497608f917ad Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:38:48 +0000 Subject: [PATCH 30/51] fix: counter types --- backend/src/plugins/Counters/types.ts | 57 ++++++++++++++++----------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/backend/src/plugins/Counters/types.ts b/backend/src/plugins/Counters/types.ts index 2b8cce16..783ec666 100644 --- a/backend/src/plugins/Counters/types.ts +++ b/backend/src/plugins/Counters/types.ts @@ -10,7 +10,7 @@ const MAX_COUNTERS = 5; const MAX_TRIGGERS_PER_COUNTER = 5; export const zTrigger = z.strictObject({ - // Dummy type because name gets replaced by the property key in zTriggerInput + // 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( @@ -20,34 +20,45 @@ export const zTrigger = z.strictObject({ reverse_condition: zBoundedCharacters(1, 64).refine( (str) => parseCounterConditionString(str) !== null, { message: "Invalid counter trigger reverse condition" }, - ), -}); - -const zTriggerInput = z.union([zBoundedCharacters(0, 100), zTrigger]) + ).optional(), +}) .transform((val, ctx) => { const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); - if (typeof val === "string") { - const parsedCondition = parseCounterConditionString(val); - if (!parsedCondition) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Invalid counter trigger condition", - }); - return z.NEVER; - } - return { - name: ruleName, - pretty_name: null, - condition: buildCounterConditionString(parsedCondition[0], parsedCondition[1]), - reverse_condition: buildCounterConditionString(getReverseCounterComparisonOp(parsedCondition[0]), parsedCondition[1]), - }; + + 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 - 2]).trim(); + const parsedCondition = parseCounterConditionString(val); + if (!parsedCondition) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid counter trigger condition", + }); + return z.NEVER; + } + return { + name: ruleName, + pretty_name: null, + condition: buildCounterConditionString(parsedCondition[0], parsedCondition[1]), + reverse_condition: buildCounterConditionString(getReverseCounterComparisonOp(parsedCondition[0]), parsedCondition[1]), + }; + }); + +const zTriggerInput = z.union([zTrigger, zTriggerFromString]); + export const zCounter = z.strictObject({ // Typed as "never" because you are not expected to supply this directly. // The transform instead picks it up from the property key and the output type is a string. @@ -78,9 +89,9 @@ export const zCounter = z.strictObject({ amount: z.number(), every: zDelayString, }).nullable().default(null), - can_view: z.boolean(), - can_edit: z.boolean(), - can_reset_all: z.boolean(), + can_view: z.boolean().default(false), + can_edit: z.boolean().default(false), + can_reset_all: z.boolean().default(false), }); export const zCountersConfig = z.strictObject({ From e4b098b5639aaf579fcb41b1dc0ab2954cb090fe Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:39:27 +0000 Subject: [PATCH 31/51] fix: automod types --- backend/src/plugins/Automod/actions/changePerms.ts | 6 +++--- .../Automod/functions/createMessageSpamTrigger.ts | 2 +- backend/src/plugins/Automod/triggers/matchLinks.ts | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/plugins/Automod/actions/changePerms.ts b/backend/src/plugins/Automod/actions/changePerms.ts index b5bc9686..33b0c283 100644 --- a/backend/src/plugins/Automod/actions/changePerms.ts +++ b/backend/src/plugins/Automod/actions/changePerms.ts @@ -2,7 +2,7 @@ import { PermissionsBitField, PermissionsString } from "discord.js"; import { U } from "ts-toolbelt"; import z from "zod"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; -import { isValidSnowflake, keys, noop, zSnowflake } from "../../../utils"; +import { isValidSnowflake, keys, noop, zBoundedCharacters, zSnowflake } from "../../../utils"; import { guildToTemplateSafeGuild, savedMessageToTemplateSafeSavedMessage, @@ -66,8 +66,8 @@ const allPermissionNames = [...permissionNames, ...legacyPermissionNames] as con export const ChangePermsAction = automodAction({ configSchema: z.strictObject({ - target: zSnowflake, - channel: zSnowflake.nullable().default(null), + target: zBoundedCharacters(1, 255), + channel: zBoundedCharacters(1, 255).nullable().default(null), perms: z.record( z.enum(allPermissionNames), z.boolean().nullable(), diff --git a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts index 19364eee..562ade6b 100644 --- a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts +++ b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts @@ -16,7 +16,7 @@ interface TMessageSpamMatchResultType { const configSchema = z.strictObject({ amount: z.number().int(), within: zDelayString, - per_channel: z.boolean().optional(), + per_channel: z.boolean().nullable().default(false), }); export function createMessageSpamTrigger(spamType: RecentActionType, prettyName: string) { diff --git a/backend/src/plugins/Automod/triggers/matchLinks.ts b/backend/src/plugins/Automod/triggers/matchLinks.ts index 128b3cd1..fe3280fc 100644 --- a/backend/src/plugins/Automod/triggers/matchLinks.ts +++ b/backend/src/plugins/Automod/triggers/matchLinks.ts @@ -21,18 +21,18 @@ const regexCache = new WeakMap<any, RegExp[]>(); const quickLinkCheck = /^https?:\/\//i; const configSchema = z.strictObject({ - include_domains: z.array(z.string().max(255)).max(255).optional(), - exclude_domains: z.array(z.string().max(255)).max(255).optional(), + include_domains: z.array(z.string().max(255)).max(700).optional(), + exclude_domains: z.array(z.string().max(255)).max(700).optional(), include_subdomains: z.boolean().default(true), - include_words: z.array(z.string().max(2000)).max(512).optional(), - exclude_words: z.array(z.string().max(2000)).max(512).optional(), + include_words: z.array(z.string().max(2000)).max(700).optional(), + exclude_words: z.array(z.string().max(2000)).max(700).optional(), include_regex: z.array(zRegex(z.string().max(2000))).max(512).optional(), exclude_regex: z.array(zRegex(z.string().max(2000))).max(512).optional(), phisherman: z.strictObject({ include_suspected: z.boolean().optional(), include_verified: z.boolean().optional(), }).optional(), - only_real_links: z.boolean(), + only_real_links: z.boolean().default(true), match_messages: z.boolean().default(true), match_embeds: z.boolean().default(true), match_visible_names: z.boolean().default(false), From 61b5f3f0d39d0b0f59ba599310cf97e4e068c0ea Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:44:00 +0000 Subject: [PATCH 32/51] feat: add cli command to validate active configs --- .gitignore | 2 ++ backend/package.json | 1 + backend/src/data/db.ts | 6 ++++ backend/src/validateActiveConfigs.ts | 46 ++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 backend/src/validateActiveConfigs.ts diff --git a/.gitignore b/.gitignore index 890e9c97..5b912745 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,5 @@ npm-audit.txt *.debug.js .vscode/ + +config-errors.txt diff --git a/backend/package.json b/backend/package.json index fbbe2ddc..b23dd8b6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,6 +24,7 @@ "migrate-rollback": "npm run typeorm -- migration:revert -d dist/backend/src/data/dataSource.js", "migrate-rollback-prod": "cross-env NODE_ENV=production npm run migrate", "migrate-rollback-dev": "cross-env NODE_ENV=development npm run build && npm run migrate", + "validate-active-configs": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps dist/backend/src/validateActiveConfigs.js > ../config-errors.txt", "test": "npm run build && npm run run-tests", "run-tests": "ava", "test-watch": "tsc-watch --onSuccess \"npx ava\"" diff --git a/backend/src/data/db.ts b/backend/src/data/db.ts index a4f9b88b..f8d8f3bf 100644 --- a/backend/src/data/db.ts +++ b/backend/src/data/db.ts @@ -15,3 +15,9 @@ export function connect() { return connectionPromise; } + +export function disconnect() { + if (connectionPromise) { + connectionPromise.then(() => dataSource.destroy()); + } +} diff --git a/backend/src/validateActiveConfigs.ts b/backend/src/validateActiveConfigs.ts new file mode 100644 index 00000000..c9e3414b --- /dev/null +++ b/backend/src/validateActiveConfigs.ts @@ -0,0 +1,46 @@ +import { YAMLException } from "js-yaml"; +import { validateGuildConfig } from "./configValidator"; +import { Configs } from "./data/Configs"; +import { connect, disconnect } from "./data/db"; +import { loadYamlSafely } from "./utils/loadYamlSafely"; +import { ObjectAliasError } from "./utils/validateNoObjectAliases"; + +function writeError(key: string, error: string) { + const indented = error.split("\n").map(s => " ".repeat(64) + s).join("\n"); + const prefix = `Invalid config ${key}:`; + const prefixed = prefix + indented.slice(prefix.length); + console.log(prefixed + "\n\n"); +} + +connect().then(async () => { + const configs = new Configs(); + const activeConfigs = await configs.getActive(); + for (const config of activeConfigs) { + if (config.key === "global") { + continue; + } + + let parsed: unknown; + try { + parsed = loadYamlSafely(config.config); + } catch (err) { + if (err instanceof ObjectAliasError) { + writeError(config.key, err.message); + continue; + } + if (err instanceof YAMLException) { + writeError(config.key, `invalid YAML: ${err.message}`); + continue; + } + throw err; + } + + const errors = await validateGuildConfig(parsed); + if (errors) { + writeError(config.key, errors); + } + } + + await disconnect(); + process.exit(0); +}); From 1b76af357999b4926d04e104aa4318a2da0b2056 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:45:07 +0000 Subject: [PATCH 33/51] fix: cases types --- backend/src/plugins/Cases/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/Cases/types.ts b/backend/src/plugins/Cases/types.ts index e25b7ed0..bcc50eed 100644 --- a/backend/src/plugins/Cases/types.ts +++ b/backend/src/plugins/Cases/types.ts @@ -16,7 +16,7 @@ export const zCasesConfig = z.strictObject({ show_relative_times: z.boolean(), relative_time_cutoff: zDelayString.default("1w"), case_colors: z.record(z.enum(caseKeys), zColor).nullable(), - case_icons: z.record(z.enum(caseKeys), zBoundedCharacters(0, 32)).nullable(), + case_icons: z.record(z.enum(caseKeys), zBoundedCharacters(0, 100)).nullable(), }); export interface CasesPluginType extends BasePluginType { From a562182e7ca8024ff58a8181d6e6404a893edf61 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:45:38 +0000 Subject: [PATCH 34/51] chore: remove unused log types from presetup-configurator --- presetup-configurator/src/LogChannels.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/presetup-configurator/src/LogChannels.tsx b/presetup-configurator/src/LogChannels.tsx index 40c0ca0f..bdc2c5b6 100644 --- a/presetup-configurator/src/LogChannels.tsx +++ b/presetup-configurator/src/LogChannels.tsx @@ -67,8 +67,6 @@ const LOG_TYPES = { MESSAGE_DELETE_AUTO: "Message deleted (auto)", SET_ANTIRAID_USER: "Set antiraid (user)", SET_ANTIRAID_AUTO: "Set antiraid (auto)", - MASS_ASSIGN_ROLES: "Mass-assigned roles", - MASS_UNASSIGN_ROLES: "Mass-unassigned roles", MEMBER_NOTE: "Member noted", CASE_DELETE: "Case deleted", DM_FAILED: "Failed to DM member", From be87fe6c43389fab07c2eb364b610f998fc6e0f2 Mon Sep 17 00:00:00 2001 From: Almeida <github@almeidx.dev> Date: Tue, 23 Jan 2024 16:36:45 +0000 Subject: [PATCH 35/51] fix case note on timeout expiration for manual timeouts --- backend/src/plugins/ModActions/events/AuditLogEvents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/ModActions/events/AuditLogEvents.ts b/backend/src/plugins/ModActions/events/AuditLogEvents.ts index a8a7f4ef..347d594e 100644 --- a/backend/src/plugins/ModActions/events/AuditLogEvents.ts +++ b/backend/src/plugins/ModActions/events/AuditLogEvents.ts @@ -42,7 +42,7 @@ export const AuditLogEvents = modActionsEvt({ caseId: existingCaseId, modId: auditLogEntry.executor?.id || "0", body: auditLogEntry.reason || "", - noteDetails: [`Timeout set to expire on <t:${moment.utc(muteChange.new as string).valueOf()}>`], + noteDetails: [`Timeout set to expire on <t:${Math.ceil(moment.utc(muteChange.new as string).valueOf() / 1_000)}>`], }); } else { await casesPlugin.createCase({ From 681b19b69c5b5ff7c9d636bac7e1d8cd036aa413 Mon Sep 17 00:00:00 2001 From: Almeida <github@almeidx.dev> Date: Tue, 23 Jan 2024 16:47:58 +0000 Subject: [PATCH 36/51] fmt --- backend/src/plugins/ModActions/events/AuditLogEvents.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/plugins/ModActions/events/AuditLogEvents.ts b/backend/src/plugins/ModActions/events/AuditLogEvents.ts index 347d594e..a66328b9 100644 --- a/backend/src/plugins/ModActions/events/AuditLogEvents.ts +++ b/backend/src/plugins/ModActions/events/AuditLogEvents.ts @@ -42,7 +42,9 @@ export const AuditLogEvents = modActionsEvt({ caseId: existingCaseId, modId: auditLogEntry.executor?.id || "0", body: auditLogEntry.reason || "", - noteDetails: [`Timeout set to expire on <t:${Math.ceil(moment.utc(muteChange.new as string).valueOf() / 1_000)}>`], + noteDetails: [ + `Timeout set to expire on <t:${Math.ceil(moment.utc(muteChange.new as string).valueOf() / 1_000)}>`, + ], }); } else { await casesPlugin.createCase({ From 6110b8190c4152735acbb8bf64bcbed4ec152195 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 12:45:52 +0200 Subject: [PATCH 37/51] chore: use @zeppelinbot scope over @zeppelin for package names --- backend/package.json | 2 +- dashboard/package.json | 2 +- package.json | 2 +- shared/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/package.json b/backend/package.json index b23dd8b6..0ac26264 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,5 +1,5 @@ { - "name": "@zeppelin/backend", + "name": "@zeppelinbot/backend", "version": "0.0.1", "description": "", "private": true, diff --git a/dashboard/package.json b/dashboard/package.json index 633f3d2c..269e9d6d 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,5 +1,5 @@ { - "name": "@zeppelin/dashboard", + "name": "@zeppelinbot/dashboard", "version": "1.0.0", "description": "", "private": true, diff --git a/package.json b/package.json index 0309d59c..4cae320e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@zeppelin/zeppelin", + "name": "@zeppelinbot/zeppelin", "version": "0.0.1", "description": "", "private": true, diff --git a/shared/package.json b/shared/package.json index 134cab28..83a2dd16 100644 --- a/shared/package.json +++ b/shared/package.json @@ -1,5 +1,5 @@ { - "name": "@zeppelin/shared", + "name": "@zeppelinbot/shared", "version": "0.0.1", "description": "", "private": true, From b0a9cf1bcd4b28ad3a5184bbb8ba0b1ab2a11325 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 12:46:48 +0200 Subject: [PATCH 38/51] fix: tweaks to config types --- backend/src/plugins/Automod/actions/alert.ts | 2 +- backend/src/plugins/Automod/actions/changePerms.ts | 6 +++--- backend/src/plugins/Automod/triggers/matchWords.ts | 2 +- backend/src/plugins/Automod/types.ts | 2 +- backend/src/utils.ts | 2 ++ 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/src/plugins/Automod/actions/alert.ts b/backend/src/plugins/Automod/actions/alert.ts index c5fcd4b3..65456218 100644 --- a/backend/src/plugins/Automod/actions/alert.ts +++ b/backend/src/plugins/Automod/actions/alert.ts @@ -27,7 +27,7 @@ import { automodAction } from "../helpers"; const configSchema = z.object({ channel: zSnowflake, - text: zBoundedCharacters(1, 4000), + text: zBoundedCharacters(0, 4000), allowed_mentions: zNullishToUndefined(zAllowedMentions.nullable().default(null)), }); diff --git a/backend/src/plugins/Automod/actions/changePerms.ts b/backend/src/plugins/Automod/actions/changePerms.ts index 33b0c283..92fdbb84 100644 --- a/backend/src/plugins/Automod/actions/changePerms.ts +++ b/backend/src/plugins/Automod/actions/changePerms.ts @@ -2,7 +2,7 @@ import { PermissionsBitField, PermissionsString } from "discord.js"; import { U } from "ts-toolbelt"; import z from "zod"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; -import { isValidSnowflake, keys, noop, zBoundedCharacters, zSnowflake } from "../../../utils"; +import { isValidSnowflake, keys, noop, zBoundedCharacters } from "../../../utils"; import { guildToTemplateSafeGuild, savedMessageToTemplateSafeSavedMessage, @@ -66,8 +66,8 @@ const allPermissionNames = [...permissionNames, ...legacyPermissionNames] as con export const ChangePermsAction = automodAction({ configSchema: z.strictObject({ - target: zBoundedCharacters(1, 255), - channel: zBoundedCharacters(1, 255).nullable().default(null), + target: zBoundedCharacters(1, 2000), + channel: zBoundedCharacters(1, 2000).nullable().default(null), perms: z.record( z.enum(allPermissionNames), z.boolean().nullable(), diff --git a/backend/src/plugins/Automod/triggers/matchWords.ts b/backend/src/plugins/Automod/triggers/matchWords.ts index e8c275e1..95556289 100644 --- a/backend/src/plugins/Automod/triggers/matchWords.ts +++ b/backend/src/plugins/Automod/triggers/matchWords.ts @@ -14,7 +14,7 @@ interface MatchResultType { const regexCache = new WeakMap<any, RegExp[]>(); const configSchema = z.strictObject({ - words: z.array(z.string().max(2000)).max(512), + words: z.array(z.string().max(2000)).max(1024), case_sensitive: z.boolean().default(false), only_full_words: z.boolean().default(true), normalize: z.boolean().default(false), diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index 251ef643..086060a0 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -69,7 +69,7 @@ export const zAutomodConfig = z.strictObject({ rules: zBoundedRecord( z.record(z.string().max(100), zRule), 0, - 100, + 255, ), antiraid_levels: z.array(z.string().max(100)).max(10), can_set_antiraid: z.boolean(), diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 641a2573..e7160e6b 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -289,6 +289,8 @@ export type StrictMessageContent = { export const zMessageContent = z.union([zBoundedCharacters(0, 4000), zStrictMessageContent]); +export type MessageContent = string | StrictMessageContent; + export function validateAndParseMessageContent(input: unknown): StrictMessageContent { if (input == null) { return {}; From b83f38809694f20be5a03549567cd4f4d3f79fe4 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 12:47:30 +0200 Subject: [PATCH 39/51] feat: add cli command to export configs as json schema --- .gitignore | 1 + backend/package-lock.json | 18 ++++++++++++++---- backend/package.json | 4 +++- backend/src/exportSchemas.ts | 24 ++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 backend/src/exportSchemas.ts diff --git a/.gitignore b/.gitignore index 5b912745..a312a7f9 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,4 @@ npm-audit.txt .vscode/ config-errors.txt +/config-schema.json diff --git a/backend/package-lock.json b/backend/package-lock.json index 1f2f5992..12d2473a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -75,7 +75,8 @@ "@types/uuid": "^9.0.2", "ava": "^5.3.1", "rimraf": "^2.6.2", - "source-map-support": "^0.5.16" + "source-map-support": "^0.5.16", + "zod-to-json-schema": "^3.22.3" } }, "node_modules/@assemblyscript/loader": { @@ -10192,12 +10193,21 @@ } }, "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.22.3.tgz", + "integrity": "sha512-9isG8SqRe07p+Aio2ruBZmLm2Q6Sq4EqmXOiNpDxp+7f0LV6Q/LX65fs5Nn+FV/CzfF3NLBoksXbS2jNYIfpKw==", + "dev": true, + "peerDependencies": { + "zod": "^3.22.4" + } } } } diff --git a/backend/package.json b/backend/package.json index 0ac26264..8a0c0e8f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,6 +25,7 @@ "migrate-rollback-prod": "cross-env NODE_ENV=production npm run migrate", "migrate-rollback-dev": "cross-env NODE_ENV=development npm run build && npm run migrate", "validate-active-configs": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps dist/backend/src/validateActiveConfigs.js > ../config-errors.txt", + "export-config-json-schema": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps dist/backend/src/exportSchemas.js > ../config-schema.json", "test": "npm run build && npm run run-tests", "run-tests": "ava", "test-watch": "tsc-watch --onSuccess \"npx ava\"" @@ -97,7 +98,8 @@ "@types/uuid": "^9.0.2", "ava": "^5.3.1", "rimraf": "^2.6.2", - "source-map-support": "^0.5.16" + "source-map-support": "^0.5.16", + "zod-to-json-schema": "^3.22.3" }, "ava": { "files": [ diff --git a/backend/src/exportSchemas.ts b/backend/src/exportSchemas.ts new file mode 100644 index 00000000..9fa5c3a5 --- /dev/null +++ b/backend/src/exportSchemas.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; +import { guildPlugins } from "./plugins/availablePlugins"; +import zodToJsonSchema from "zod-to-json-schema"; +import { zZeppelinGuildConfig } from "./types"; + +const pluginSchemaMap = guildPlugins.reduce((map, plugin) => { + if (! plugin.info) { + return map; + } + map[plugin.name] = plugin.info.configSchema; + return map; +}, {}); + +const fullSchema = zZeppelinGuildConfig + .omit({ plugins: true }) + .merge(z.strictObject({ + plugins: z.strictObject(pluginSchemaMap).partial(), + })); + +const jsonSchema = zodToJsonSchema(fullSchema); + +console.log(JSON.stringify(jsonSchema, null, 2)); + +process.exit(0); From 7ba318a6d9b6eebcfd587bbab427241b8dc91fa5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 12:50:09 +0200 Subject: [PATCH 40/51] chore: centralize common TS config options --- backend/tsconfig.json | 19 ++----------------- dashboard/tsconfig.json | 9 +-------- shared/tsconfig.json | 9 +-------- tsconfig.json | 19 +++++++++++++++++++ 4 files changed, 23 insertions(+), 33 deletions(-) create mode 100644 tsconfig.json diff --git a/backend/tsconfig.json b/backend/tsconfig.json index bec249ad..2000565b 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,28 +1,13 @@ { + "extends": "../tsconfig.json", "compilerOptions": { "moduleResolution": "NodeNext", "module": "NodeNext", - "noImplicitAny": false, - "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "target": "esnext", - "lib": ["es2023"], "baseUrl": ".", - "resolveJsonModule": true, - "esModuleInterop": true, "outDir": "./dist", "paths": { "@shared/*": ["../shared/src/*"] - }, - "sourceMap": true, - "alwaysStrict": true, - "noImplicitThis": true, - "skipLibCheck": true, - "strict": true, - "strictPropertyInitialization": false, - "useUnknownInCatchVariables": false, - "allowJs": true + } }, "include": ["src/**/*.ts"] } diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json index 1bd1fd32..34507dfc 100644 --- a/dashboard/tsconfig.json +++ b/dashboard/tsconfig.json @@ -1,18 +1,11 @@ { + "extends": "../tsconfig.json", "compilerOptions": { "moduleResolution": "node", "module": "esnext", "target": "es2018", - "sourceMap": true, - "noImplicitAny": false, - "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "strict": false, "lib": ["esnext", "dom"], "baseUrl": ".", - "resolveJsonModule": true, - "esModuleInterop": true, "allowJs": true, "paths": { "@shared/*": ["../shared/src/*"] diff --git a/shared/tsconfig.json b/shared/tsconfig.json index c5c16316..cea9087a 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -1,16 +1,9 @@ { + "extends": "../tsconfig.json", "compilerOptions": { "moduleResolution": "NodeNext", "module": "NodeNext", - "noImplicitAny": false, - "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "target": "es2022", - "lib": ["es2022"], "baseUrl": "src", - "resolveJsonModule": true, - "esModuleInterop": true, "outDir": "./dist" }, "include": ["src/**/*.ts"] diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..3701310e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "noImplicitAny": false, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "target": "esnext", + "lib": ["es2023"], + "resolveJsonModule": true, + "esModuleInterop": true, + "sourceMap": true, + "alwaysStrict": true, + "noImplicitThis": true, + "skipLibCheck": true, + "strict": true, + "strictPropertyInitialization": false, + "useUnknownInCatchVariables": false + } +} From 49866d375acf14f0ebe9d5c177f3c7c7526e28ff Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 13:55:26 +0200 Subject: [PATCH 41/51] feat: zod config schema formatting --- backend/src/api/docs.ts | 107 ++++++++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 21 deletions(-) diff --git a/backend/src/api/docs.ts b/backend/src/api/docs.ts index dcb75c49..5b9de52b 100644 --- a/backend/src/api/docs.ts +++ b/backend/src/api/docs.ts @@ -1,34 +1,99 @@ import express from "express"; +import z from "zod"; import { guildPlugins } from "../plugins/availablePlugins"; import { indentLines } from "../utils"; import { notFound } from "./responses"; -function formatConfigSchema(schema) { - if (schema._tag === "InterfaceType" || schema._tag === "PartialType") { +function isZodObject(schema: z.ZodTypeAny): schema is z.ZodObject<any> { + return schema._def.typeName === "ZodObject"; +} + +function isZodRecord(schema: z.ZodTypeAny): schema is z.ZodRecord<any> { + return schema._def.typeName === "ZodRecord"; +} + +function isZodEffects(schema: z.ZodTypeAny): schema is z.ZodEffects<any, any> { + return schema._def.typeName === "ZodEffects"; +} + +function isZodOptional(schema: z.ZodTypeAny): schema is z.ZodOptional<any> { + return schema._def.typeName === "ZodOptional"; +} + +function isZodArray(schema: z.ZodTypeAny): schema is z.ZodArray<any> { + return schema._def.typeName === "ZodArray"; +} + +function isZodUnion(schema: z.ZodTypeAny): schema is z.ZodUnion<any> { + return schema._def.typeName === "ZodUnion"; +} + +function isZodNullable(schema: z.ZodTypeAny): schema is z.ZodNullable<any> { + return schema._def.typeName === "ZodNullable"; +} + +function isZodDefault(schema: z.ZodTypeAny): schema is z.ZodDefault<any> { + return schema._def.typeName === "ZodDefault"; +} + +function isZodLiteral(schema: z.ZodTypeAny): schema is z.ZodLiteral<any> { + return schema._def.typeName === "ZodLiteral"; +} + +function isZodIntersection(schema: z.ZodTypeAny): schema is z.ZodIntersection<any, any> { + return schema._def.typeName === "ZodIntersection"; +} + +function formatZodConfigSchema(schema: z.ZodTypeAny) { + if (isZodObject(schema)) { return ( `{\n` + - Object.entries(schema.props) - .map(([k, value]) => indentLines(`${k}: ${formatConfigSchema(value)}`, 2)) + Object.entries(schema._def.shape()) + .map(([k, value]) => indentLines(`${k}: ${formatZodConfigSchema(value as z.ZodTypeAny)}`, 2)) .join("\n") + "\n}" ); - } else if (schema._tag === "DictionaryType") { - return "{\n" + indentLines(`[string]: ${formatConfigSchema(schema.codomain)}`, 2) + "\n}"; - } else if (schema._tag === "ArrayType") { - return `Array<${formatConfigSchema(schema.type)}>`; - } else if (schema._tag === "UnionType") { - if (schema.name.startsWith("Nullable<")) { - return `Nullable<${formatConfigSchema(schema.types[0])}>`; - } else if (schema.name.startsWith("Optional<")) { - return `Optional<${formatConfigSchema(schema.types[0])}>`; - } else { - return schema.types.map((t) => formatConfigSchema(t)).join(" | "); - } - } else if (schema._tag === "IntersectionType") { - return schema.types.map((t) => formatConfigSchema(t)).join(" & "); - } else { - return schema.name; } + if (isZodRecord(schema)) { + return "{\n" + indentLines(`[string]: ${formatZodConfigSchema(schema._def.valueType)}`, 2) + "\n}"; + } + if (isZodEffects(schema)) { + return formatZodConfigSchema(schema._def.schema); + } + if (isZodOptional(schema)) { + return `Optional<${formatZodConfigSchema(schema._def.innerType)}>`; + } + if (isZodArray(schema)) { + return `Array<${formatZodConfigSchema(schema._def.type)}>`; + } + if (isZodUnion(schema)) { + return schema._def.options.map((t) => formatZodConfigSchema(t)).join(" | "); + } + if (isZodNullable(schema)) { + return `Nullable<${formatZodConfigSchema(schema._def.innerType)}>`; + } + if (isZodDefault(schema)) { + return formatZodConfigSchema(schema._def.innerType); + } + if (isZodLiteral(schema)) { + return schema._def.value; + } + if (isZodIntersection(schema)) { + return [formatZodConfigSchema(schema._def.left), formatZodConfigSchema(schema._def.right)].join(" & "); + } + if (schema._def.typeName === "ZodString") { + return "string"; + } + if (schema._def.typeName === "ZodNumber") { + return "number"; + } + if (schema._def.typeName === "ZodBoolean") { + return "boolean"; + } + if (schema._def.typeName === "ZodNever") { + return "never"; + } + return "unknown"; } export function initDocs(app: express.Express) { @@ -67,7 +132,7 @@ export function initDocs(app: express.Express) { })); const defaultOptions = plugin.defaultOptions || {}; - const configSchema = plugin.info?.configSchema && formatConfigSchema(plugin.info.configSchema); + const configSchema = plugin.info?.configSchema && formatZodConfigSchema(plugin.info.configSchema); res.json({ name, From adab5dd591af7df85619fa7adc3a95d07a12abf8 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:04:49 +0200 Subject: [PATCH 42/51] fix: revert dashboard tsconfig There are a lot of errors with the stricter settings that the backend uses and it's not worth it fixing the dashboard now when we're rewriting it in the near future. --- dashboard/tsconfig.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json index 34507dfc..1bd1fd32 100644 --- a/dashboard/tsconfig.json +++ b/dashboard/tsconfig.json @@ -1,11 +1,18 @@ { - "extends": "../tsconfig.json", "compilerOptions": { "moduleResolution": "node", "module": "esnext", "target": "es2018", + "sourceMap": true, + "noImplicitAny": false, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strict": false, "lib": ["esnext", "dom"], "baseUrl": ".", + "resolveJsonModule": true, + "esModuleInterop": true, "allowJs": true, "paths": { "@shared/*": ["../shared/src/*"] From 77ab2718e7a60cd5bb82e3eb9be86aa7566ad0ab Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:21:32 +0200 Subject: [PATCH 43/51] chore: resolve lint errors --- backend/src/index.ts | 2 +- .../src/plugins/ContextMenus/actions/userInfo.ts | 2 +- backend/src/plugins/Logs/types.ts | 2 +- backend/src/plugins/Utility/UtilityPlugin.ts | 4 ++-- backend/src/plugins/Utility/commands/InfoCmd.ts | 13 ++++++------- .../src/plugins/Utility/commands/MessageInfoCmd.ts | 1 - backend/src/plugins/Utility/commands/RoleInfoCmd.ts | 2 +- .../src/plugins/Utility/commands/ServerInfoCmd.ts | 2 +- .../plugins/Utility/commands/SnowflakeInfoCmd.ts | 4 ++-- backend/src/plugins/Utility/commands/UserInfoCmd.ts | 2 +- .../Utility/functions/getChannelInfoEmbed.ts | 1 - .../Utility/functions/getMessageInfoEmbed.ts | 4 ---- .../plugins/Utility/functions/getRoleInfoEmbed.ts | 1 - .../plugins/Utility/functions/getServerInfoEmbed.ts | 1 - .../Utility/functions/getSnowflakeInfoEmbed.ts | 4 ---- .../plugins/Utility/functions/getUserInfoEmbed.ts | 4 ---- backend/src/utils.ts | 7 ------- 17 files changed, 16 insertions(+), 40 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 2fe16c11..6dad14b1 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -203,7 +203,7 @@ if (env.DEBUG) { } logger.info("Connecting to database"); -connect().then(async (connection) => { +connect().then(async () => { const client = new Client({ partials: [Partials.User, Partials.Channel, Partials.GuildMember, Partials.Message, Partials.Reaction], diff --git a/backend/src/plugins/ContextMenus/actions/userInfo.ts b/backend/src/plugins/ContextMenus/actions/userInfo.ts index e445e1c8..a2d44d7a 100644 --- a/backend/src/plugins/ContextMenus/actions/userInfo.ts +++ b/backend/src/plugins/ContextMenus/actions/userInfo.ts @@ -16,7 +16,7 @@ export async function userInfoAction( const utility = pluginData.getPlugin(UtilityPlugin); if (userCfg.can_use && (await utility.hasPermission(executingMember, interaction.channelId, "can_userinfo"))) { - const embed = await utility.userInfo(interaction.targetId, interaction.user.id); + const embed = await utility.userInfo(interaction.targetId); if (!embed) { await interaction.followUp({ content: "Cannot info: internal error" }); return; diff --git a/backend/src/plugins/Logs/types.ts b/backend/src/plugins/Logs/types.ts index 2960c2ae..6b214c99 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 { ZodString, z } from "zod"; +import { z } from "zod"; import { RegExpRunner } from "../../RegExpRunner"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildCases } from "../../data/GuildCases"; diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index efb892ec..5f44118e 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -169,8 +169,8 @@ export const UtilityPlugin = zeppelinGuildPlugin<UtilityPluginType>()({ }, userInfo(pluginData) { - return (userId: Snowflake, requestMemberId?: Snowflake) => { - return getUserInfoEmbed(pluginData, userId, false, requestMemberId); + return (userId: Snowflake) => { + return getUserInfoEmbed(pluginData, userId, false); }; }, diff --git a/backend/src/plugins/Utility/commands/InfoCmd.ts b/backend/src/plugins/Utility/commands/InfoCmd.ts index 63d0ae81..a430898e 100644 --- a/backend/src/plugins/Utility/commands/InfoCmd.ts +++ b/backend/src/plugins/Utility/commands/InfoCmd.ts @@ -42,7 +42,7 @@ export const InfoCmd = utilityCmd({ const channelId = getChannelId(value); const channel = channelId && pluginData.guild.channels.cache.get(channelId as Snowflake); if (channel) { - const embed = await getChannelInfoEmbed(pluginData, channelId!, message.author.id); + const embed = await getChannelInfoEmbed(pluginData, channelId!); if (embed) { message.channel.send({ embeds: [embed] }); return; @@ -54,7 +54,7 @@ export const InfoCmd = utilityCmd({ if (userCfg.can_server) { const guild = await pluginData.client.guilds.fetch(value as Snowflake).catch(noop); if (guild) { - const embed = await getServerInfoEmbed(pluginData, value, message.author.id); + const embed = await getServerInfoEmbed(pluginData, value); if (embed) { message.channel.send({ embeds: [embed] }); return; @@ -66,7 +66,7 @@ export const InfoCmd = utilityCmd({ if (userCfg.can_userinfo) { const user = await resolveUser(pluginData.client, value); if (user && userCfg.can_userinfo) { - const embed = await getUserInfoEmbed(pluginData, user.id, Boolean(args.compact), message.author.id); + const embed = await getUserInfoEmbed(pluginData, user.id, Boolean(args.compact)); if (embed) { message.channel.send({ embeds: [embed] }); return; @@ -83,7 +83,6 @@ export const InfoCmd = utilityCmd({ pluginData, messageTarget.channel.id, messageTarget.messageId, - message.author.id, ); if (embed) { message.channel.send({ embeds: [embed] }); @@ -112,7 +111,7 @@ export const InfoCmd = utilityCmd({ if (userCfg.can_server) { const serverPreview = await getGuildPreview(pluginData.client, value).catch(() => null); if (serverPreview) { - const embed = await getServerInfoEmbed(pluginData, value, message.author.id); + const embed = await getServerInfoEmbed(pluginData, value); if (embed) { message.channel.send({ embeds: [embed] }); return; @@ -125,7 +124,7 @@ export const InfoCmd = utilityCmd({ const roleId = getRoleId(value); const role = roleId && pluginData.guild.roles.cache.get(roleId as Snowflake); if (role) { - const embed = await getRoleInfoEmbed(pluginData, role, message.author.id); + const embed = await getRoleInfoEmbed(pluginData, role); message.channel.send({ embeds: [embed] }); return; } @@ -145,7 +144,7 @@ export const InfoCmd = utilityCmd({ // 9. Arbitrary ID if (isValidSnowflake(value) && userCfg.can_snowflake) { - const embed = await getSnowflakeInfoEmbed(pluginData, value, true, message.author.id); + const embed = await getSnowflakeInfoEmbed(value, true); message.channel.send({ embeds: [embed] }); return; } diff --git a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts index 82096250..72df1f6c 100644 --- a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts +++ b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts @@ -24,7 +24,6 @@ export const MessageInfoCmd = utilityCmd({ pluginData, args.message.channel.id, args.message.messageId, - message.author.id, ); if (!embed) { sendErrorMessage(pluginData, message.channel, "Unknown message"); diff --git a/backend/src/plugins/Utility/commands/RoleInfoCmd.ts b/backend/src/plugins/Utility/commands/RoleInfoCmd.ts index b04b49a7..6f540b4b 100644 --- a/backend/src/plugins/Utility/commands/RoleInfoCmd.ts +++ b/backend/src/plugins/Utility/commands/RoleInfoCmd.ts @@ -13,7 +13,7 @@ export const RoleInfoCmd = utilityCmd({ }, async run({ message, args, pluginData }) { - const embed = await getRoleInfoEmbed(pluginData, args.role, message.author.id); + const embed = await getRoleInfoEmbed(pluginData, args.role); message.channel.send({ embeds: [embed] }); }, }); diff --git a/backend/src/plugins/Utility/commands/ServerInfoCmd.ts b/backend/src/plugins/Utility/commands/ServerInfoCmd.ts index 30fb7efe..9d1d4ed6 100644 --- a/backend/src/plugins/Utility/commands/ServerInfoCmd.ts +++ b/backend/src/plugins/Utility/commands/ServerInfoCmd.ts @@ -15,7 +15,7 @@ export const ServerInfoCmd = utilityCmd({ async run({ message, pluginData, args }) { const serverId = args.serverId || pluginData.guild.id; - const serverInfoEmbed = await getServerInfoEmbed(pluginData, serverId, message.author.id); + const serverInfoEmbed = await getServerInfoEmbed(pluginData, serverId); if (!serverInfoEmbed) { sendErrorMessage(pluginData, message.channel, "Could not find information for that server"); return; diff --git a/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts b/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts index 59f43893..bf0e859f 100644 --- a/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts +++ b/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts @@ -12,8 +12,8 @@ export const SnowflakeInfoCmd = utilityCmd({ id: ct.anyId(), }, - async run({ message, args, pluginData }) { - const embed = await getSnowflakeInfoEmbed(pluginData, args.id, false, message.author.id); + async run({ message, args }) { + const embed = await getSnowflakeInfoEmbed(args.id, false); message.channel.send({ embeds: [embed] }); }, }); diff --git a/backend/src/plugins/Utility/commands/UserInfoCmd.ts b/backend/src/plugins/Utility/commands/UserInfoCmd.ts index c7ce8b48..d796bf36 100644 --- a/backend/src/plugins/Utility/commands/UserInfoCmd.ts +++ b/backend/src/plugins/Utility/commands/UserInfoCmd.ts @@ -17,7 +17,7 @@ export const UserInfoCmd = utilityCmd({ async run({ message, args, pluginData }) { const userId = args.user?.id || message.author.id; - const embed = await getUserInfoEmbed(pluginData, userId, args.compact, message.author.id); + const embed = await getUserInfoEmbed(pluginData, userId, args.compact); if (!embed) { sendErrorMessage(pluginData, message.channel, "User not found"); return; diff --git a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts index ee006f5f..33a04b8e 100644 --- a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts @@ -22,7 +22,6 @@ const FORUM_CHANNEL_ICON = export async function getChannelInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, channelId: string, - requestMemberId?: string, ): Promise<APIEmbed | null> { const channel = pluginData.guild.channels.cache.get(channelId as Snowflake); if (!channel) { diff --git a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts index 1568a3e3..4b615cf0 100644 --- a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts @@ -9,7 +9,6 @@ import { trimEmptyLines, trimLines, } from "../../../utils"; -import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; import { UtilityPluginType } from "../types"; const MESSAGE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/740685652152025088/message.png"; @@ -18,7 +17,6 @@ export async function getMessageInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, channelId: string, messageId: string, - requestMemberId?: string, ): Promise<APIEmbed | null> { const message = await (pluginData.guild.channels.resolve(channelId as Snowflake) as TextChannel).messages .fetch(messageId as Snowflake) @@ -27,8 +25,6 @@ export async function getMessageInfoEmbed( return null; } - const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); - const embed: EmbedWith<"fields" | "author"> = { fields: [], author: { diff --git a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts index fa3188ec..700546ca 100644 --- a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts @@ -9,7 +9,6 @@ const MENTION_ICON = "https://cdn.discordapp.com/attachments/705009450855039042/ export async function getRoleInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, role: Role, - requestMemberId?: string, ): Promise<APIEmbed> { const embed: EmbedWith<"fields" | "author" | "color"> = { fields: [], diff --git a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts index d0202db7..bbd1f78f 100644 --- a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts @@ -25,7 +25,6 @@ const prettifyFeature = (feature: string): string => export async function getServerInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, serverId: string, - requestMemberId?: string, ): Promise<APIEmbed | null> { const thisServer = serverId === pluginData.guild.id ? pluginData.guild : null; const [restGuild, guildPreview] = await Promise.all([ diff --git a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts index ef676fd1..7009241c 100644 --- a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts @@ -1,16 +1,12 @@ import { APIEmbed } from "discord.js"; -import { GuildPluginData } from "knub"; import { EmbedWith, preEmbedPadding } from "../../../utils"; import { snowflakeToTimestamp } from "../../../utils/snowflakeToTimestamp"; -import { UtilityPluginType } from "../types"; const SNOWFLAKE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/742020790471491668/snowflake.png"; export async function getSnowflakeInfoEmbed( - pluginData: GuildPluginData<UtilityPluginType>, snowflake: string, showUnknownWarning = false, - requestMemberId?: string, ): Promise<APIEmbed> { const embed: EmbedWith<"fields" | "author"> = { fields: [], diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts index ef8d2320..b568a833 100644 --- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts @@ -13,7 +13,6 @@ import { trimLines, UnknownUser, } from "../../../utils"; -import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; import { UtilityPluginType } from "../types"; const MAX_ROLES_TO_DISPLAY = 15; @@ -27,7 +26,6 @@ export async function getUserInfoEmbed( pluginData: GuildPluginData<UtilityPluginType>, userId: string, compact = false, - requestMemberId?: string, ): Promise<APIEmbed | null> { const user = await resolveUser(pluginData.client, userId); if (!user || user instanceof UnknownUser) { @@ -40,8 +38,6 @@ export async function getUserInfoEmbed( fields: [], }; - const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); - embed.author = { name: `${user.bot ? "Bot" : "User"}: ${renderUsername(user.username, user.discriminator)}`, }; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index db08dc49..97f51301 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -150,13 +150,6 @@ export type GroupDMInvite = Invite & { type: typeof ChannelType.GroupDM; }; -function isBoundedString(str: unknown, min: number, max: number): str is string { - if (typeof str !== "string") { - return false; - } - return (str.length >= min && str.length <= max); -} - export function zBoundedCharacters(min: number, max: number) { return z.string().refine(str => { const len = [...str].length; // Unicode aware character split From 873bf7eb9962a210c4e62b837f65e429378be388 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:23:13 +0200 Subject: [PATCH 44/51] chore: run prettier --- backend/src/data/GuildArchives.ts | 2 +- backend/src/exportSchemas.ts | 12 +- backend/src/pluginUtils.ts | 8 +- backend/src/plugins/AutoDelete/types.ts | 2 +- backend/src/plugins/Automod/AutomodPlugin.ts | 4 +- backend/src/plugins/Automod/actions/alert.ts | 2 +- backend/src/plugins/Automod/actions/ban.ts | 11 +- .../plugins/Automod/actions/changePerms.ts | 5 +- backend/src/plugins/Automod/actions/kick.ts | 2 +- backend/src/plugins/Automod/actions/mute.ts | 21 ++- backend/src/plugins/Automod/actions/reply.ts | 2 +- backend/src/plugins/Automod/actions/warn.ts | 2 +- backend/src/plugins/Automod/constants.ts | 5 +- .../functions/createMessageSpamTrigger.ts | 2 +- backend/src/plugins/Automod/helpers.ts | 2 +- .../plugins/Automod/triggers/antiraidLevel.ts | 2 +- .../plugins/Automod/triggers/anyMessage.ts | 2 +- .../Automod/triggers/matchAttachmentType.ts | 48 +++--- .../plugins/Automod/triggers/matchLinks.ts | 20 ++- .../plugins/Automod/triggers/matchMimeType.ts | 46 +++--- .../src/plugins/Automod/triggers/roleAdded.ts | 5 +- .../plugins/Automod/triggers/roleRemoved.ts | 5 +- backend/src/plugins/Automod/types.ts | 70 ++++---- .../src/plugins/ContextMenus/actions/mute.ts | 4 +- .../src/plugins/Counters/CountersPlugin.ts | 5 +- backend/src/plugins/Counters/types.ts | 133 ++++++++------- .../actions/setChannelPermissionOverrides.ts | 18 +- backend/src/plugins/CustomEvents/types.ts | 6 +- .../GuildAccessMonitorPlugin.ts | 2 +- .../GuildInfoSaver/GuildInfoSaverPlugin.ts | 2 +- backend/src/plugins/Logs/LogsPlugin.ts | 2 +- .../Logs/logFunctions/logMessageDelete.ts | 4 +- backend/src/plugins/Logs/types.ts | 20 ++- backend/src/plugins/Logs/util/isLogIgnored.ts | 6 +- .../ModActions/functions/clearTempban.ts | 4 +- backend/src/plugins/ReactionRoles/types.ts | 5 +- backend/src/plugins/RoleButtons/types.ts | 154 ++++++++++-------- .../src/plugins/SelfGrantableRoles/types.ts | 5 +- backend/src/plugins/Slowmode/types.ts | 2 +- backend/src/plugins/Spam/types.ts | 10 +- backend/src/plugins/Starboard/types.ts | 6 +- .../src/plugins/Tags/commands/TagEvalCmd.ts | 8 +- backend/src/plugins/Tags/types.ts | 70 ++++---- .../src/plugins/Tags/util/onMessageCreate.ts | 4 +- .../plugins/Tags/util/renderTagFromString.ts | 8 +- .../src/plugins/Utility/commands/InfoCmd.ts | 6 +- .../Utility/commands/MessageInfoCmd.ts | 6 +- .../Utility/functions/getRoleInfoEmbed.ts | 5 +- .../functions/getSnowflakeInfoEmbed.ts | 5 +- backend/src/plugins/Utility/search.ts | 10 +- .../src/plugins/ZeppelinPluginBlueprint.ts | 2 +- backend/src/utils.ts | 77 ++++++--- backend/src/utils/formatZodIssue.ts | 4 +- backend/src/validateActiveConfigs.ts | 5 +- 54 files changed, 462 insertions(+), 416 deletions(-) diff --git a/backend/src/data/GuildArchives.ts b/backend/src/data/GuildArchives.ts index 0451dfe0..4d74ccef 100644 --- a/backend/src/data/GuildArchives.ts +++ b/backend/src/data/GuildArchives.ts @@ -4,12 +4,12 @@ import { Repository } from "typeorm"; import { TemplateSafeValueContainer, renderTemplate } from "../templateFormatter"; import { renderUsername, trimLines } from "../utils"; import { decrypt, encrypt } from "../utils/crypt"; +import { isDefaultSticker } from "../utils/isDefaultSticker"; import { channelToTemplateSafeChannel, guildToTemplateSafeGuild } from "../utils/templateSafeObjects"; import { BaseGuildRepository } from "./BaseGuildRepository"; import { dataSource } from "./dataSource"; import { ArchiveEntry } from "./entities/ArchiveEntry"; import { SavedMessage } from "./entities/SavedMessage"; -import { isDefaultSticker } from "../utils/isDefaultSticker"; const DEFAULT_EXPIRY_DAYS = 30; diff --git a/backend/src/exportSchemas.ts b/backend/src/exportSchemas.ts index 9fa5c3a5..54f55291 100644 --- a/backend/src/exportSchemas.ts +++ b/backend/src/exportSchemas.ts @@ -1,21 +1,21 @@ import { z } from "zod"; -import { guildPlugins } from "./plugins/availablePlugins"; import zodToJsonSchema from "zod-to-json-schema"; +import { guildPlugins } from "./plugins/availablePlugins"; import { zZeppelinGuildConfig } from "./types"; const pluginSchemaMap = guildPlugins.reduce((map, plugin) => { - if (! plugin.info) { + if (!plugin.info) { return map; } map[plugin.name] = plugin.info.configSchema; return map; }, {}); -const fullSchema = zZeppelinGuildConfig - .omit({ plugins: true }) - .merge(z.strictObject({ +const fullSchema = zZeppelinGuildConfig.omit({ plugins: true }).merge( + z.strictObject({ plugins: z.strictObject(pluginSchemaMap).partial(), - })); + }), +); const jsonSchema = zodToJsonSchema(fullSchema); diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index a2f744d9..31cbd77c 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -10,13 +10,7 @@ import { PermissionsBitField, TextBasedChannel, } from "discord.js"; -import { - AnyPluginData, - CommandContext, - ExtendedMatchParams, - GuildPluginData, - helpers -} from "knub"; +import { AnyPluginData, CommandContext, ExtendedMatchParams, GuildPluginData, helpers } from "knub"; import { logger } from "./logger"; import { isStaff } from "./staff"; import { TZeppelinKnub } from "./types"; diff --git a/backend/src/plugins/AutoDelete/types.ts b/backend/src/plugins/AutoDelete/types.ts index 69100438..be6ff7ee 100644 --- a/backend/src/plugins/AutoDelete/types.ts +++ b/backend/src/plugins/AutoDelete/types.ts @@ -1,10 +1,10 @@ import { BasePluginType } from "knub"; +import z from "zod"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { SavedMessage } from "../../data/entities/SavedMessage"; import { MINUTES, zDelayString } from "../../utils"; import Timeout = NodeJS.Timeout; -import z from "zod"; export const MAX_DELAY = 5 * MINUTES; diff --git a/backend/src/plugins/Automod/AutomodPlugin.ts b/backend/src/plugins/Automod/AutomodPlugin.ts index 9634c25d..29fcf9e6 100644 --- a/backend/src/plugins/Automod/AutomodPlugin.ts +++ b/backend/src/plugins/Automod/AutomodPlugin.ts @@ -1,9 +1,9 @@ import { CooldownManager } from "knub"; +import { Queue } from "../../Queue"; import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels"; import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; -import { Queue } from "../../Queue"; import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { MINUTES, SECONDS } from "../../utils"; import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap"; @@ -19,9 +19,9 @@ import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { AntiraidClearCmd } from "./commands/AntiraidClearCmd"; import { SetAntiraidCmd } from "./commands/SetAntiraidCmd"; import { ViewAntiraidCmd } from "./commands/ViewAntiraidCmd"; -import { runAutomodOnCounterTrigger } from "./events/runAutomodOnCounterTrigger"; import { RunAutomodOnJoinEvt, RunAutomodOnLeaveEvt } from "./events/RunAutomodOnJoinLeaveEvt"; import { RunAutomodOnMemberUpdate } from "./events/RunAutomodOnMemberUpdate"; +import { runAutomodOnCounterTrigger } from "./events/runAutomodOnCounterTrigger"; import { runAutomodOnMessage } from "./events/runAutomodOnMessage"; import { runAutomodOnModAction } from "./events/runAutomodOnModAction"; import { diff --git a/backend/src/plugins/Automod/actions/alert.ts b/backend/src/plugins/Automod/actions/alert.ts index 65456218..d21fb4d5 100644 --- a/backend/src/plugins/Automod/actions/alert.ts +++ b/backend/src/plugins/Automod/actions/alert.ts @@ -16,7 +16,7 @@ import { zAllowedMentions, zBoundedCharacters, zNullishToUndefined, - zSnowflake + zSnowflake, } from "../../../utils"; import { erisAllowedMentionsToDjsMentionOptions } from "../../../utils/erisAllowedMentionsToDjsMentionOptions"; import { messageIsEmpty } from "../../../utils/messageIsEmpty"; diff --git a/backend/src/plugins/Automod/actions/ban.ts b/backend/src/plugins/Automod/actions/ban.ts index 22cc8167..5bf5b3a2 100644 --- a/backend/src/plugins/Automod/actions/ban.ts +++ b/backend/src/plugins/Automod/actions/ban.ts @@ -1,10 +1,17 @@ import z from "zod"; -import { convertDelayStringToMS, nonNullish, unique, zBoundedCharacters, zDelayString, zSnowflake } from "../../../utils"; +import { + convertDelayStringToMS, + nonNullish, + unique, + zBoundedCharacters, + zDelayString, + zSnowflake, +} from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; +import { zNotify } from "../constants"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; -import { zNotify } from "../constants"; const configSchema = z.strictObject({ reason: zBoundedCharacters(0, 4000).nullable().default(null), diff --git a/backend/src/plugins/Automod/actions/changePerms.ts b/backend/src/plugins/Automod/actions/changePerms.ts index 92fdbb84..7b7eba37 100644 --- a/backend/src/plugins/Automod/actions/changePerms.ts +++ b/backend/src/plugins/Automod/actions/changePerms.ts @@ -68,10 +68,7 @@ export const ChangePermsAction = automodAction({ configSchema: z.strictObject({ target: zBoundedCharacters(1, 2000), channel: zBoundedCharacters(1, 2000).nullable().default(null), - perms: z.record( - z.enum(allPermissionNames), - z.boolean().nullable(), - ), + perms: z.record(z.enum(allPermissionNames), z.boolean().nullable()), }), async apply({ pluginData, contexts, actionConfig }) { diff --git a/backend/src/plugins/Automod/actions/kick.ts b/backend/src/plugins/Automod/actions/kick.ts index 412309cd..2161e2c0 100644 --- a/backend/src/plugins/Automod/actions/kick.ts +++ b/backend/src/plugins/Automod/actions/kick.ts @@ -2,9 +2,9 @@ import z from "zod"; import { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; +import { zNotify } from "../constants"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; -import { zNotify } from "../constants"; export const KickAction = automodAction({ configSchema: z.strictObject({ diff --git a/backend/src/plugins/Automod/actions/mute.ts b/backend/src/plugins/Automod/actions/mute.ts index c2f8e186..ba932b7f 100644 --- a/backend/src/plugins/Automod/actions/mute.ts +++ b/backend/src/plugins/Automod/actions/mute.ts @@ -1,12 +1,19 @@ import z from "zod"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; -import { convertDelayStringToMS, nonNullish, unique, zBoundedCharacters, zDelayString, zSnowflake } from "../../../utils"; +import { + convertDelayStringToMS, + nonNullish, + unique, + zBoundedCharacters, + zDelayString, + zSnowflake, +} from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { MutesPlugin } from "../../Mutes/MutesPlugin"; +import { zNotify } from "../constants"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; -import { zNotify } from "../constants"; export const MuteAction = automodAction({ configSchema: z.strictObject({ @@ -14,8 +21,14 @@ export const MuteAction = automodAction({ duration: zDelayString.nullable().default(null), notify: zNotify.nullable().default(null), notifyChannel: zSnowflake.nullable().default(null), - remove_roles_on_mute: z.union([z.boolean(), z.array(zSnowflake)]).nullable().default(null), - restore_roles_on_mute: z.union([z.boolean(), z.array(zSnowflake)]).nullable().default(null), + remove_roles_on_mute: z + .union([z.boolean(), z.array(zSnowflake)]) + .nullable() + .default(null), + restore_roles_on_mute: z + .union([z.boolean(), z.array(zSnowflake)]) + .nullable() + .default(null), postInCaseLog: z.boolean().nullable().default(null), hide_case: z.boolean().nullable().default(false), }), diff --git a/backend/src/plugins/Automod/actions/reply.ts b/backend/src/plugins/Automod/actions/reply.ts index 02ff9545..e8b1fdf8 100644 --- a/backend/src/plugins/Automod/actions/reply.ts +++ b/backend/src/plugins/Automod/actions/reply.ts @@ -10,7 +10,7 @@ import { verboseChannelMention, zBoundedCharacters, zDelayString, - zMessageContent + zMessageContent, } from "../../../utils"; import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions"; import { messageIsEmpty } from "../../../utils/messageIsEmpty"; diff --git a/backend/src/plugins/Automod/actions/warn.ts b/backend/src/plugins/Automod/actions/warn.ts index 7ccb3898..d8a7805b 100644 --- a/backend/src/plugins/Automod/actions/warn.ts +++ b/backend/src/plugins/Automod/actions/warn.ts @@ -2,9 +2,9 @@ import z from "zod"; import { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; +import { zNotify } from "../constants"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { automodAction } from "../helpers"; -import { zNotify } from "../constants"; export const WarnAction = automodAction({ configSchema: z.strictObject({ diff --git a/backend/src/plugins/Automod/constants.ts b/backend/src/plugins/Automod/constants.ts index cc37f402..c2baae77 100644 --- a/backend/src/plugins/Automod/constants.ts +++ b/backend/src/plugins/Automod/constants.ts @@ -20,7 +20,4 @@ export enum RecentActionType { ThreadCreate, } -export const zNotify = z.union([ - z.literal("dm"), - z.literal("channel"), -]); +export const zNotify = z.union([z.literal("dm"), z.literal("channel")]); diff --git a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts index 562ade6b..cb7eb288 100644 --- a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts +++ b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts @@ -1,3 +1,4 @@ +import z from "zod"; import { SavedMessage } from "../../../data/entities/SavedMessage"; import { humanizeDurationShort } from "../../../humanizeDurationShort"; import { getBaseUrl } from "../../../pluginUtils"; @@ -7,7 +8,6 @@ import { automodTrigger } from "../helpers"; import { findRecentSpam } from "./findRecentSpam"; import { getMatchingMessageRecentActions } from "./getMatchingMessageRecentActions"; import { getMessageSpamIdentifier } from "./getSpamIdentifier"; -import z from "zod"; interface TMessageSpamMatchResultType { archiveId: string; diff --git a/backend/src/plugins/Automod/helpers.ts b/backend/src/plugins/Automod/helpers.ts index d1c04d61..b4b9f764 100644 --- a/backend/src/plugins/Automod/helpers.ts +++ b/backend/src/plugins/Automod/helpers.ts @@ -1,7 +1,7 @@ import { GuildPluginData } from "knub"; +import z, { ZodTypeAny } from "zod"; import { Awaitable } from "../../utils/typeUtils"; import { AutomodContext, AutomodPluginType } from "./types"; -import z, { ZodTypeAny } from "zod"; interface BaseAutomodTriggerMatchResult { extraContexts?: AutomodContext[]; diff --git a/backend/src/plugins/Automod/triggers/antiraidLevel.ts b/backend/src/plugins/Automod/triggers/antiraidLevel.ts index 1c5f7458..c1467bf8 100644 --- a/backend/src/plugins/Automod/triggers/antiraidLevel.ts +++ b/backend/src/plugins/Automod/triggers/antiraidLevel.ts @@ -1,5 +1,5 @@ -import { automodTrigger } from "../helpers"; import z from "zod"; +import { automodTrigger } from "../helpers"; interface AntiraidLevelTriggerResult {} diff --git a/backend/src/plugins/Automod/triggers/anyMessage.ts b/backend/src/plugins/Automod/triggers/anyMessage.ts index 93ce0abc..71c92019 100644 --- a/backend/src/plugins/Automod/triggers/anyMessage.ts +++ b/backend/src/plugins/Automod/triggers/anyMessage.ts @@ -1,7 +1,7 @@ import { Snowflake } from "discord.js"; +import z from "zod"; import { verboseChannelMention } from "../../../utils"; import { automodTrigger } from "../helpers"; -import z from "zod"; interface AnyMessageResultType {} diff --git a/backend/src/plugins/Automod/triggers/matchAttachmentType.ts b/backend/src/plugins/Automod/triggers/matchAttachmentType.ts index 6dd49580..8c562eb1 100644 --- a/backend/src/plugins/Automod/triggers/matchAttachmentType.ts +++ b/backend/src/plugins/Automod/triggers/matchAttachmentType.ts @@ -1,6 +1,6 @@ import { escapeInlineCode, Snowflake } from "discord.js"; -import z from "zod"; import { extname } from "path"; +import z from "zod"; import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils"; import { automodTrigger } from "../helpers"; @@ -9,28 +9,30 @@ interface MatchResultType { mode: "blacklist" | "whitelist"; } -const configSchema = z.strictObject({ - filetype_blacklist: z.array(z.string().max(32)).max(255).default([]), - blacklist_enabled: z.boolean().default(false), - filetype_whitelist: z.array(z.string().max(32)).max(255).default([]), - whitelist_enabled: z.boolean().default(false), -}).transform((parsed, ctx) => { - if (parsed.blacklist_enabled && parsed.whitelist_enabled) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Cannot have both blacklist and whitelist enabled", - }); - return z.NEVER; - } - if (! parsed.blacklist_enabled && ! parsed.whitelist_enabled) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Must have either blacklist or whitelist enabled", - }); - return z.NEVER; - } - return parsed; -}); +const configSchema = z + .strictObject({ + filetype_blacklist: z.array(z.string().max(32)).max(255).default([]), + blacklist_enabled: z.boolean().default(false), + filetype_whitelist: z.array(z.string().max(32)).max(255).default([]), + whitelist_enabled: z.boolean().default(false), + }) + .transform((parsed, ctx) => { + if (parsed.blacklist_enabled && parsed.whitelist_enabled) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cannot have both blacklist and whitelist enabled", + }); + return z.NEVER; + } + if (!parsed.blacklist_enabled && !parsed.whitelist_enabled) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Must have either blacklist or whitelist enabled", + }); + return z.NEVER; + } + return parsed; + }); export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({ configSchema, diff --git a/backend/src/plugins/Automod/triggers/matchLinks.ts b/backend/src/plugins/Automod/triggers/matchLinks.ts index fe3280fc..c43d8320 100644 --- a/backend/src/plugins/Automod/triggers/matchLinks.ts +++ b/backend/src/plugins/Automod/triggers/matchLinks.ts @@ -26,12 +26,20 @@ const configSchema = z.strictObject({ include_subdomains: z.boolean().default(true), include_words: z.array(z.string().max(2000)).max(700).optional(), exclude_words: z.array(z.string().max(2000)).max(700).optional(), - include_regex: z.array(zRegex(z.string().max(2000))).max(512).optional(), - exclude_regex: z.array(zRegex(z.string().max(2000))).max(512).optional(), - phisherman: z.strictObject({ - include_suspected: z.boolean().optional(), - include_verified: z.boolean().optional(), - }).optional(), + include_regex: z + .array(zRegex(z.string().max(2000))) + .max(512) + .optional(), + exclude_regex: z + .array(zRegex(z.string().max(2000))) + .max(512) + .optional(), + phisherman: z + .strictObject({ + include_suspected: z.boolean().optional(), + include_verified: z.boolean().optional(), + }) + .optional(), only_real_links: z.boolean().default(true), match_messages: z.boolean().default(true), match_embeds: z.boolean().default(true), diff --git a/backend/src/plugins/Automod/triggers/matchMimeType.ts b/backend/src/plugins/Automod/triggers/matchMimeType.ts index 8da9b2e3..86232916 100644 --- a/backend/src/plugins/Automod/triggers/matchMimeType.ts +++ b/backend/src/plugins/Automod/triggers/matchMimeType.ts @@ -8,28 +8,30 @@ interface MatchResultType { mode: "blacklist" | "whitelist"; } -const configSchema = z.strictObject({ - mime_type_blacklist: z.array(z.string().max(255)).max(255).default([]), - blacklist_enabled: z.boolean().default(false), - mime_type_whitelist: z.array(z.string().max(255)).max(255).default([]), - whitelist_enabled: z.boolean().default(false), -}).transform((parsed, ctx) => { - if (parsed.blacklist_enabled && parsed.whitelist_enabled) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Cannot have both blacklist and whitelist enabled", - }); - return z.NEVER; - } - if (! parsed.blacklist_enabled && ! parsed.whitelist_enabled) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Must have either blacklist or whitelist enabled", - }); - return z.NEVER; - } - return parsed; -}); +const configSchema = z + .strictObject({ + mime_type_blacklist: z.array(z.string().max(255)).max(255).default([]), + blacklist_enabled: z.boolean().default(false), + mime_type_whitelist: z.array(z.string().max(255)).max(255).default([]), + whitelist_enabled: z.boolean().default(false), + }) + .transform((parsed, ctx) => { + if (parsed.blacklist_enabled && parsed.whitelist_enabled) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cannot have both blacklist and whitelist enabled", + }); + return z.NEVER; + } + if (!parsed.blacklist_enabled && !parsed.whitelist_enabled) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Must have either blacklist or whitelist enabled", + }); + return z.NEVER; + } + return parsed; + }); export const MatchMimeTypeTrigger = automodTrigger<MatchResultType>()({ configSchema, diff --git a/backend/src/plugins/Automod/triggers/roleAdded.ts b/backend/src/plugins/Automod/triggers/roleAdded.ts index 47027b6b..ce1e0ab5 100644 --- a/backend/src/plugins/Automod/triggers/roleAdded.ts +++ b/backend/src/plugins/Automod/triggers/roleAdded.ts @@ -8,10 +8,7 @@ interface RoleAddedMatchResult { matchedRoleId: string; } -const configSchema = z.union([ - zSnowflake, - z.array(zSnowflake).max(255), -]).default([]); +const configSchema = z.union([zSnowflake, z.array(zSnowflake).max(255)]).default([]); export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({ configSchema, diff --git a/backend/src/plugins/Automod/triggers/roleRemoved.ts b/backend/src/plugins/Automod/triggers/roleRemoved.ts index 26caf227..f11dafeb 100644 --- a/backend/src/plugins/Automod/triggers/roleRemoved.ts +++ b/backend/src/plugins/Automod/triggers/roleRemoved.ts @@ -8,10 +8,7 @@ interface RoleAddedMatchResult { matchedRoleId: string; } -const configSchema = z.union([ - zSnowflake, - z.array(zSnowflake).max(255), -]).default([]); +const configSchema = z.union([zSnowflake, z.array(zSnowflake).max(255)]).default([]); export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({ configSchema, diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index 2cf2df2f..6cf88ad6 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -19,58 +19,62 @@ import { availableTriggers } from "./triggers/availableTriggers"; import Timeout = NodeJS.Timeout; export type ZTriggersMapHelper = { - [TriggerName in keyof typeof availableTriggers]: typeof availableTriggers[TriggerName]["configSchema"]; + [TriggerName in keyof typeof availableTriggers]: (typeof availableTriggers)[TriggerName]["configSchema"]; }; -const zTriggersMap = z.strictObject(entries(availableTriggers).reduce((map, [triggerName, trigger]) => { - map[triggerName] = trigger.configSchema; - return map; -}, {} as ZTriggersMapHelper)).partial(); +const zTriggersMap = z + .strictObject( + entries(availableTriggers).reduce((map, [triggerName, trigger]) => { + map[triggerName] = trigger.configSchema; + return map; + }, {} as ZTriggersMapHelper), + ) + .partial(); type ZActionsMapHelper = { - [ActionName in keyof typeof availableActions]: typeof availableActions[ActionName]["configSchema"]; + [ActionName in keyof typeof availableActions]: (typeof availableActions)[ActionName]["configSchema"]; }; -const zActionsMap = z.strictObject(entries(availableActions).reduce((map, [actionName, action]) => { - // @ts-expect-error TS can't infer this properly but it works fine thanks to our helper - map[actionName] = action.configSchema; - return map; -}, {} as ZActionsMapHelper)).partial(); +const zActionsMap = z + .strictObject( + entries(availableActions).reduce((map, [actionName, action]) => { + // @ts-expect-error TS can't infer this properly but it works fine thanks to our helper + map[actionName] = action.configSchema; + return map; + }, {} as ZActionsMapHelper), + ) + .partial(); const zRule = z.strictObject({ enabled: z.boolean().default(true), // Typed as "never" because you are not expected to supply this directly. // The transform instead picks it up from the property key and the output type is a string. - name: z.never().optional().transform((_, ctx) => { - const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); - if (! ruleName) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Automod rules must have names", - }); - return z.NEVER; - } - return ruleName; - }), + name: z + .never() + .optional() + .transform((_, ctx) => { + const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); + if (!ruleName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Automod rules must have names", + }); + return z.NEVER; + } + return ruleName; + }), presets: z.array(z.string().max(100)).max(25).default([]), affects_bots: z.boolean().default(false), affects_self: z.boolean().default(false), cooldown: zDelayString.nullable().default(null), allow_further_rules: z.boolean().default(false), triggers: z.array(zTriggersMap), - actions: zActionsMap.refine( - (v) => ! (v.clean && v.start_thread), - { - message: "Cannot have both clean and start_thread active at the same time", - } - ), + actions: zActionsMap.refine((v) => !(v.clean && v.start_thread), { + message: "Cannot have both clean and start_thread active at the same time", + }), }); export type TRule = z.infer<typeof zRule>; export const zAutomodConfig = z.strictObject({ - rules: zBoundedRecord( - z.record(z.string().max(100), zRule), - 0, - 255, - ), + rules: zBoundedRecord(z.record(z.string().max(100), zRule), 0, 255), antiraid_levels: z.array(z.string().max(100)).max(10), can_set_antiraid: z.boolean(), can_view_antiraid: z.boolean(), diff --git a/backend/src/plugins/ContextMenus/actions/mute.ts b/backend/src/plugins/ContextMenus/actions/mute.ts index c6eedbc6..16b48a4a 100644 --- a/backend/src/plugins/ContextMenus/actions/mute.ts +++ b/backend/src/plugins/ContextMenus/actions/mute.ts @@ -2,13 +2,13 @@ import { ContextMenuCommandInteraction } from "discord.js"; import humanizeDuration from "humanize-duration"; import { GuildPluginData } from "knub"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; +import { canActOn } from "../../../pluginUtils"; import { convertDelayStringToMS } from "../../../utils"; import { CaseArgs } from "../../Cases/types"; import { LogsPlugin } from "../../Logs/LogsPlugin"; +import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { MutesPlugin } from "../../Mutes/MutesPlugin"; import { ContextMenuPluginType } from "../types"; -import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; -import { canActOn } from "../../../pluginUtils"; export async function muteAction( pluginData: GuildPluginData<ContextMenuPluginType>, diff --git a/backend/src/plugins/Counters/CountersPlugin.ts b/backend/src/plugins/Counters/CountersPlugin.ts index 85be2cf5..1cc1ec25 100644 --- a/backend/src/plugins/Counters/CountersPlugin.ts +++ b/backend/src/plugins/Counters/CountersPlugin.ts @@ -1,10 +1,7 @@ import { EventEmitter } from "events"; import { PluginOptions } from "knub"; import { GuildCounters } from "../../data/GuildCounters"; -import { - CounterTrigger, - parseCounterConditionString -} from "../../data/entities/CounterTrigger"; +import { CounterTrigger, parseCounterConditionString } from "../../data/entities/CounterTrigger"; import { mapToPublicFn } from "../../pluginUtils"; import { MINUTES, convertDelayStringToMS, values } from "../../utils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; diff --git a/backend/src/plugins/Counters/types.ts b/backend/src/plugins/Counters/types.ts index 783ec666..8e70c4ab 100644 --- a/backend/src/plugins/Counters/types.ts +++ b/backend/src/plugins/Counters/types.ts @@ -2,33 +2,45 @@ import { EventEmitter } from "events"; import { BasePluginType } from "knub"; import z from "zod"; import { GuildCounters } from "../../data/GuildCounters"; -import { CounterTrigger, buildCounterConditionString, getReverseCounterComparisonOp, parseCounterConditionString } from "../../data/entities/CounterTrigger"; +import { + CounterTrigger, + buildCounterConditionString, + getReverseCounterComparisonOp, + parseCounterConditionString, +} from "../../data/entities/CounterTrigger"; import { zBoundedCharacters, zBoundedRecord, zDelayString } from "../../utils"; import Timeout = NodeJS.Timeout; const MAX_COUNTERS = 5; 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" }, - ), - reverse_condition: zBoundedCharacters(1, 64).refine( - (str) => parseCounterConditionString(str) !== null, - { message: "Invalid counter trigger reverse condition" }, - ).optional(), -}) +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", + }), + reverse_condition: zBoundedCharacters(1, 64) + .refine((str) => parseCounterConditionString(str) !== null, { + message: "Invalid counter trigger reverse condition", + }) + .optional(), + }) .transform((val, ctx) => { const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); let reverseCondition = val.reverse_condition; - if (! reverseCondition) { + if (!reverseCondition) { const parsedCondition = parseCounterConditionString(val.condition)!; - reverseCondition = buildCounterConditionString(getReverseCounterComparisonOp(parsedCondition[0]), parsedCondition[1]); + reverseCondition = buildCounterConditionString( + getReverseCounterComparisonOp(parsedCondition[0]), + parsedCondition[1], + ); } return { @@ -38,68 +50,65 @@ export const zTrigger = z.strictObject({ }; }); -const zTriggerFromString = zBoundedCharacters(0, 100) - .transform((val, ctx) => { - const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); - const parsedCondition = parseCounterConditionString(val); - if (!parsedCondition) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Invalid counter trigger condition", - }); - return z.NEVER; - } - return { - name: ruleName, - pretty_name: null, - condition: buildCounterConditionString(parsedCondition[0], parsedCondition[1]), - reverse_condition: buildCounterConditionString(getReverseCounterComparisonOp(parsedCondition[0]), parsedCondition[1]), - }; - }); +const zTriggerFromString = zBoundedCharacters(0, 100).transform((val, ctx) => { + const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); + const parsedCondition = parseCounterConditionString(val); + if (!parsedCondition) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid counter trigger condition", + }); + return z.NEVER; + } + return { + name: ruleName, + pretty_name: null, + condition: buildCounterConditionString(parsedCondition[0], parsedCondition[1]), + reverse_condition: buildCounterConditionString( + getReverseCounterComparisonOp(parsedCondition[0]), + parsedCondition[1], + ), + }; +}); const zTriggerInput = z.union([zTrigger, zTriggerFromString]); export const zCounter = z.strictObject({ // Typed as "never" because you are not expected to supply this directly. // The transform instead picks it up from the property key and the output type is a string. - name: z.never().optional().transform((_, ctx) => { - const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); - if (! ruleName) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Counters must have names", - }); - return z.NEVER; - } - return ruleName; - }), + name: z + .never() + .optional() + .transform((_, ctx) => { + const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); + if (!ruleName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Counters must have names", + }); + return z.NEVER; + } + return ruleName; + }), pretty_name: zBoundedCharacters(0, 100).nullable().default(null), per_channel: z.boolean().default(false), per_user: z.boolean().default(false), initial_value: z.number().default(0), - triggers: zBoundedRecord( - z.record( - zBoundedCharacters(0, 100), - zTriggerInput, - ), - 1, - MAX_TRIGGERS_PER_COUNTER, - ), - decay: z.strictObject({ - amount: z.number(), - every: zDelayString, - }).nullable().default(null), + triggers: zBoundedRecord(z.record(zBoundedCharacters(0, 100), zTriggerInput), 1, MAX_TRIGGERS_PER_COUNTER), + decay: z + .strictObject({ + amount: z.number(), + every: zDelayString, + }) + .nullable() + .default(null), can_view: z.boolean().default(false), can_edit: z.boolean().default(false), can_reset_all: z.boolean().default(false), }); export const zCountersConfig = z.strictObject({ - counters: zBoundedRecord( - z.record(zBoundedCharacters(0, 100), zCounter), - 0, - MAX_COUNTERS, - ), + counters: zBoundedRecord(z.record(zBoundedCharacters(0, 100), zCounter), 0, MAX_COUNTERS), can_view: z.boolean(), can_edit: z.boolean(), can_reset_all: z.boolean(), diff --git a/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts b/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts index dbd7b932..b2e44d93 100644 --- a/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts +++ b/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts @@ -9,14 +9,16 @@ import { CustomEventsPluginType, TCustomEvent } from "../types"; export const zSetChannelPermissionOverridesAction = z.strictObject({ type: z.literal("set_channel_permission_overrides"), channel: zSnowflake, - overrides: z.array( - z.strictObject({ - type: z.union([z.literal("member"), z.literal("role")]), - id: zSnowflake, - allow: z.number(), - deny: z.number(), - }), - ).max(15), + overrides: z + .array( + z.strictObject({ + type: z.union([z.literal("member"), z.literal("role")]), + id: zSnowflake, + allow: z.number(), + deny: z.number(), + }), + ) + .max(15), }); export type TSetChannelPermissionOverridesAction = z.infer<typeof zSetChannelPermissionOverridesAction>; diff --git a/backend/src/plugins/CustomEvents/types.ts b/backend/src/plugins/CustomEvents/types.ts index 4572b1cf..6433b6d0 100644 --- a/backend/src/plugins/CustomEvents/types.ts +++ b/backend/src/plugins/CustomEvents/types.ts @@ -36,11 +36,7 @@ export const zCustomEvent = z.strictObject({ export type TCustomEvent = z.infer<typeof zCustomEvent>; export const zCustomEventsConfig = z.strictObject({ - events: zBoundedRecord( - z.record(zBoundedCharacters(0, 100), zCustomEvent), - 0, - 100, - ), + events: zBoundedRecord(z.record(zBoundedCharacters(0, 100), zCustomEvent), 0, 100), }); export interface CustomEventsPluginType extends BasePluginType { diff --git a/backend/src/plugins/GuildAccessMonitor/GuildAccessMonitorPlugin.ts b/backend/src/plugins/GuildAccessMonitor/GuildAccessMonitorPlugin.ts index b8639134..3c267aab 100644 --- a/backend/src/plugins/GuildAccessMonitor/GuildAccessMonitorPlugin.ts +++ b/backend/src/plugins/GuildAccessMonitor/GuildAccessMonitorPlugin.ts @@ -1,10 +1,10 @@ import { Guild } from "discord.js"; import { BasePluginType, GlobalPluginData, globalPluginEventListener } from "knub"; +import z from "zod"; import { AllowedGuilds } from "../../data/AllowedGuilds"; import { Configs } from "../../data/Configs"; import { env } from "../../env"; import { zeppelinGlobalPlugin } from "../ZeppelinPluginBlueprint"; -import z from "zod"; interface GuildAccessMonitorPluginType extends BasePluginType { state: { diff --git a/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts b/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts index a8d71e06..873711b3 100644 --- a/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts +++ b/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts @@ -1,11 +1,11 @@ import { Guild } from "discord.js"; import { guildPluginEventListener } from "knub"; +import z from "zod"; import { AllowedGuilds } from "../../data/AllowedGuilds"; import { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments"; import { MINUTES } from "../../utils"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { GuildInfoSaverPluginType } from "./types"; -import z from "zod"; export const GuildInfoSaverPlugin = zeppelinGuildPlugin<GuildInfoSaverPluginType>()({ name: "guild_info_saver", diff --git a/backend/src/plugins/Logs/LogsPlugin.ts b/backend/src/plugins/Logs/LogsPlugin.ts index 923ac623..c989c682 100644 --- a/backend/src/plugins/Logs/LogsPlugin.ts +++ b/backend/src/plugins/Logs/LogsPlugin.ts @@ -123,7 +123,7 @@ const defaultOptions: PluginOptions<LogsPluginType> = { timestamp: FORMAT_NO_TIMESTAMP, ...DefaultLogMessages, }, - ping_user: true, + ping_user: true, allow_user_mentions: false, timestamp_format: "[<t:]X[>]", include_embed_timestamp: true, diff --git a/backend/src/plugins/Logs/logFunctions/logMessageDelete.ts b/backend/src/plugins/Logs/logFunctions/logMessageDelete.ts index 25c8c340..b56f7410 100644 --- a/backend/src/plugins/Logs/logFunctions/logMessageDelete.ts +++ b/backend/src/plugins/Logs/logFunctions/logMessageDelete.ts @@ -32,7 +32,9 @@ export function logMessageDelete(pluginData: GuildPluginData<LogsPluginType>, da // See comment on FORMAT_NO_TIMESTAMP in types.ts const config = pluginData.config.get(); const timestampFormat = - (config.format.timestamp !== FORMAT_NO_TIMESTAMP ? config.format.timestamp : null) ?? config.timestamp_format ?? undefined; + (config.format.timestamp !== FORMAT_NO_TIMESTAMP ? config.format.timestamp : null) ?? + config.timestamp_format ?? + undefined; return log( pluginData, diff --git a/backend/src/plugins/Logs/types.ts b/backend/src/plugins/Logs/types.ts index 6b214c99..47557514 100644 --- a/backend/src/plugins/Logs/types.ts +++ b/backend/src/plugins/Logs/types.ts @@ -29,10 +29,12 @@ const MAX_BATCH_TIME = 5000; type ZLogFormatsHelper = { -readonly [K in keyof typeof LogType]: typeof zMessageContent; }; -export const zLogFormats = z.strictObject(keys(LogType).reduce((map, logType) => { - map[logType] = zMessageContent; - return map; -}, {} as ZLogFormatsHelper)); +export const zLogFormats = z.strictObject( + keys(LogType).reduce((map, logType) => { + map[logType] = zMessageContent; + return map; + }, {} as ZLogFormatsHelper), +); export type TLogFormats = z.infer<typeof zLogFormats>; const zLogChannel = z.strictObject({ @@ -58,10 +60,12 @@ export type TLogChannelMap = z.infer<typeof zLogChannelMap>; export const zLogsConfig = z.strictObject({ channels: zLogChannelMap, - format: zLogFormats.merge(z.strictObject({ - // Legacy/deprecated, use timestamp_format below instead - timestamp: zBoundedCharacters(0, 64).nullable(), - })), + format: zLogFormats.merge( + z.strictObject({ + // Legacy/deprecated, use timestamp_format below instead + timestamp: zBoundedCharacters(0, 64).nullable(), + }), + ), // Legacy/deprecated, if below is false mentions wont actually ping. In case you really want the old behavior, set below to true ping_user: z.boolean(), allow_user_mentions: z.boolean(), diff --git a/backend/src/plugins/Logs/util/isLogIgnored.ts b/backend/src/plugins/Logs/util/isLogIgnored.ts index 71255f50..8d3e15c1 100644 --- a/backend/src/plugins/Logs/util/isLogIgnored.ts +++ b/backend/src/plugins/Logs/util/isLogIgnored.ts @@ -2,6 +2,10 @@ import { GuildPluginData } from "knub"; import { LogType } from "../../../data/LogType"; import { LogsPluginType } from "../types"; -export function isLogIgnored(pluginData: GuildPluginData<LogsPluginType>, type: keyof typeof LogType, ignoreId: string) { +export function isLogIgnored( + pluginData: GuildPluginData<LogsPluginType>, + type: keyof typeof LogType, + ignoreId: string, +) { return pluginData.state.guildLogs.isLogIgnored(type, ignoreId); } diff --git a/backend/src/plugins/ModActions/functions/clearTempban.ts b/backend/src/plugins/ModActions/functions/clearTempban.ts index 5173ca7e..1133d651 100644 --- a/backend/src/plugins/ModActions/functions/clearTempban.ts +++ b/backend/src/plugins/ModActions/functions/clearTempban.ts @@ -3,7 +3,9 @@ import humanizeDuration from "humanize-duration"; import { GuildPluginData } from "knub"; import moment from "moment-timezone"; import { CaseTypes } from "../../../data/CaseTypes"; +import { LogType } from "../../../data/LogType"; import { Tempban } from "../../../data/entities/Tempban"; +import { logger } from "../../../logger"; import { resolveUser } from "../../../utils"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; @@ -11,8 +13,6 @@ import { IgnoredEventType, ModActionsPluginType } from "../types"; import { formatReasonWithAttachments } from "./formatReasonWithAttachments"; import { ignoreEvent } from "./ignoreEvent"; import { isBanned } from "./isBanned"; -import { LogType } from "../../../data/LogType"; -import { logger } from "../../../logger"; export async function clearTempban(pluginData: GuildPluginData<ModActionsPluginType>, tempban: Tempban) { if (!(await isBanned(pluginData, tempban.user_id))) { diff --git a/backend/src/plugins/ReactionRoles/types.ts b/backend/src/plugins/ReactionRoles/types.ts index a1c09f00..b955c6fa 100644 --- a/backend/src/plugins/ReactionRoles/types.ts +++ b/backend/src/plugins/ReactionRoles/types.ts @@ -22,10 +22,7 @@ export type PendingMemberRoleChanges = { }>; }; -const zReactionRolePair = z.union([ - z.tuple([z.string(), z.string(), z.string()]), - z.tuple([z.string(), z.string()]), -]); +const zReactionRolePair = z.union([z.tuple([z.string(), z.string(), z.string()]), z.tuple([z.string(), z.string()])]); export type TReactionRolePair = z.infer<typeof zReactionRolePair>; export interface ReactionRolesPluginType extends BasePluginType { diff --git a/backend/src/plugins/RoleButtons/types.ts b/backend/src/plugins/RoleButtons/types.ts index 3c94f8a8..95e46f79 100644 --- a/backend/src/plugins/RoleButtons/types.ts +++ b/backend/src/plugins/RoleButtons/types.ts @@ -11,89 +11,99 @@ const zRoleButtonOption = z.strictObject({ label: z.string().nullable().default(null), emoji: z.string().nullable().default(null), // https://discord.js.org/#/docs/discord.js/v13/typedef/MessageButtonStyle - style: z.union([ - z.literal(ButtonStyle.Primary), - z.literal(ButtonStyle.Secondary), - z.literal(ButtonStyle.Success), - z.literal(ButtonStyle.Danger), + style: z + .union([ + z.literal(ButtonStyle.Primary), + z.literal(ButtonStyle.Secondary), + z.literal(ButtonStyle.Success), + z.literal(ButtonStyle.Danger), - // The following are deprecated - z.literal("PRIMARY"), - z.literal("SECONDARY"), - z.literal("SUCCESS"), - z.literal("DANGER"), - // z.literal("LINK"), // Role buttons don't use link buttons, but adding this here so it's documented why it's not available - ]).nullable().default(null), + // The following are deprecated + z.literal("PRIMARY"), + z.literal("SECONDARY"), + z.literal("SUCCESS"), + z.literal("DANGER"), + // z.literal("LINK"), // Role buttons don't use link buttons, but adding this here so it's documented why it's not available + ]) + .nullable() + .default(null), start_new_row: z.boolean().default(false), }); export type TRoleButtonOption = z.infer<typeof zRoleButtonOption>; -const zRoleButtonsConfigItem = z.strictObject({ - // Typed as "never" because you are not expected to supply this directly. - // The transform instead picks it up from the property key and the output type is a string. - name: z.never().optional().transform((_, ctx) => { - const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); - if (! ruleName) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Role buttons must have names", - }); - return z.NEVER; - } - return ruleName; - }), - message: z.union([ - z.strictObject({ - channel_id: zSnowflake, - message_id: zSnowflake, - }), - z.strictObject({ - channel_id: zSnowflake, - content: zMessageContent, - }), - ]), - options: z.array(zRoleButtonOption).max(25), - exclusive: z.boolean().default(false), -}) - .refine((parsed) => { - try { - createButtonComponents(parsed); - } catch (err) { - if (err instanceof TooManyComponentsError) { - return false; +const zRoleButtonsConfigItem = z + .strictObject({ + // Typed as "never" because you are not expected to supply this directly. + // The transform instead picks it up from the property key and the output type is a string. + name: z + .never() + .optional() + .transform((_, ctx) => { + const ruleName = String(ctx.path[ctx.path.length - 2]).trim(); + if (!ruleName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Role buttons must have names", + }); + return z.NEVER; + } + return ruleName; + }), + message: z.union([ + z.strictObject({ + channel_id: zSnowflake, + message_id: zSnowflake, + }), + z.strictObject({ + channel_id: zSnowflake, + content: zMessageContent, + }), + ]), + options: z.array(zRoleButtonOption).max(25), + exclusive: z.boolean().default(false), + }) + .refine( + (parsed) => { + try { + createButtonComponents(parsed); + } catch (err) { + if (err instanceof TooManyComponentsError) { + return false; + } + throw err; } - throw err; - } - return true; - }, { - message: "Too many options; can only have max 5 buttons per row on max 5 rows." - }); + return true; + }, + { + message: "Too many options; can only have max 5 buttons per row on max 5 rows.", + }, + ); export type TRoleButtonsConfigItem = z.infer<typeof zRoleButtonsConfigItem>; -export const zRoleButtonsConfig = z.strictObject({ - buttons: zBoundedRecord( - z.record(zBoundedCharacters(1, 16), zRoleButtonsConfigItem), - 0, - 100, - ), - can_reset: z.boolean(), -}) - .refine((parsed) => { - const seenMessages = new Set(); - for (const button of Object.values(parsed.buttons)) { - if (button.message) { - if ("message_id" in button.message) { - if (seenMessages.has(button.message.message_id)) { - return false; +export const zRoleButtonsConfig = z + .strictObject({ + buttons: zBoundedRecord(z.record(zBoundedCharacters(1, 16), zRoleButtonsConfigItem), 0, 100), + can_reset: z.boolean(), + }) + .refine( + (parsed) => { + const seenMessages = new Set(); + for (const button of Object.values(parsed.buttons)) { + if (button.message) { + if ("message_id" in button.message) { + if (seenMessages.has(button.message.message_id)) { + return false; + } + seenMessages.add(button.message.message_id); } - seenMessages.add(button.message.message_id); } } - } - return true; - }, { - message: "Can't target the same message with two sets of role buttons", - }); + return true; + }, + { + message: "Can't target the same message with two sets of role buttons", + }, + ); export interface RoleButtonsPluginType extends BasePluginType { config: z.infer<typeof zRoleButtonsConfig>; diff --git a/backend/src/plugins/SelfGrantableRoles/types.ts b/backend/src/plugins/SelfGrantableRoles/types.ts index a1aed241..e3c7a786 100644 --- a/backend/src/plugins/SelfGrantableRoles/types.ts +++ b/backend/src/plugins/SelfGrantableRoles/types.ts @@ -4,9 +4,10 @@ import { zBoundedCharacters, zBoundedRecord } from "../../utils"; const zRoleMap = z.record( zBoundedCharacters(1, 100), - z.array(zBoundedCharacters(1, 2000)) + z + .array(zBoundedCharacters(1, 2000)) .max(100) - .transform((parsed) => parsed.map(v => v.toLowerCase())), + .transform((parsed) => parsed.map((v) => v.toLowerCase())), ); const zSelfGrantableRoleEntry = z.strictObject({ diff --git a/backend/src/plugins/Slowmode/types.ts b/backend/src/plugins/Slowmode/types.ts index c4fe44d5..7c8d5b56 100644 --- a/backend/src/plugins/Slowmode/types.ts +++ b/backend/src/plugins/Slowmode/types.ts @@ -7,7 +7,7 @@ import { SlowmodeChannel } from "../../data/entities/SlowmodeChannel"; export const zSlowmodeConfig = z.strictObject({ use_native_slowmode: z.boolean(), - + can_manage: z.boolean(), is_affected: z.boolean(), }); diff --git a/backend/src/plugins/Spam/types.ts b/backend/src/plugins/Spam/types.ts index 74ea872f..f71e4180 100644 --- a/backend/src/plugins/Spam/types.ts +++ b/backend/src/plugins/Spam/types.ts @@ -11,14 +11,8 @@ const zBaseSingleSpamConfig = z.strictObject({ count: z.number(), mute: z.boolean().default(false), mute_time: z.number().nullable().default(null), - remove_roles_on_mute: z.union([ - z.boolean(), - z.array(zSnowflake), - ]).default(false), - restore_roles_on_mute: z.union([ - z.boolean(), - z.array(zSnowflake), - ]).default(false), + remove_roles_on_mute: z.union([z.boolean(), z.array(zSnowflake)]).default(false), + restore_roles_on_mute: z.union([z.boolean(), z.array(zSnowflake)]).default(false), clean: z.boolean().default(false), }); export type TBaseSingleSpamConfig = z.infer<typeof zBaseSingleSpamConfig>; diff --git a/backend/src/plugins/Starboard/types.ts b/backend/src/plugins/Starboard/types.ts index 7dc2d8f5..bb8845fe 100644 --- a/backend/src/plugins/Starboard/types.ts +++ b/backend/src/plugins/Starboard/types.ts @@ -18,11 +18,7 @@ const zStarboardOpts = z.strictObject({ export type TStarboardOpts = z.infer<typeof zStarboardOpts>; export const zStarboardConfig = z.strictObject({ - boards: zBoundedRecord( - z.record(z.string(), zStarboardOpts), - 0, - 100, - ), + boards: zBoundedRecord(z.record(z.string(), zStarboardOpts), 0, 100), can_migrate: z.boolean(), }); diff --git a/backend/src/plugins/Tags/commands/TagEvalCmd.ts b/backend/src/plugins/Tags/commands/TagEvalCmd.ts index 27cbe4f3..3515ef74 100644 --- a/backend/src/plugins/Tags/commands/TagEvalCmd.ts +++ b/backend/src/plugins/Tags/commands/TagEvalCmd.ts @@ -1,11 +1,11 @@ import { MessageCreateOptions } from "discord.js"; import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { logger } from "../../../logger"; import { sendErrorMessage } from "../../../pluginUtils"; import { TemplateParseError } from "../../../templateFormatter"; import { memberToTemplateSafeMember, userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { tagsCmd } from "../types"; import { renderTagBody } from "../util/renderTagBody"; -import { logger } from "../../../logger"; export const TagEvalCmd = tagsCmd({ trigger: "tag eval", @@ -35,13 +35,11 @@ export const TagEvalCmd = tagsCmd({ msg.channel.send(rendered); } catch (e) { - const errorMessage = e instanceof TemplateParseError - ? e.message - : "Internal error"; + const errorMessage = e instanceof TemplateParseError ? e.message : "Internal error"; sendErrorMessage(pluginData, msg.channel, `Failed to render tag: ${errorMessage}`); - if (! (e instanceof TemplateParseError)) { + if (!(e instanceof TemplateParseError)) { logger.warn(`Internal error evaluating tag in ${pluginData.guild.id}: ${e}`); } diff --git a/backend/src/plugins/Tags/types.ts b/backend/src/plugins/Tags/types.ts index 441906fe..5ff7e9c7 100644 --- a/backend/src/plugins/Tags/types.ts +++ b/backend/src/plugins/Tags/types.ts @@ -9,52 +9,48 @@ import { zEmbedInput } from "../../utils"; export const zTag = z.union([z.string(), zEmbedInput]); export type TTag = z.infer<typeof zTag>; -export const zTagCategory = z.strictObject({ - prefix: z.string().nullable().default(null), - delete_with_command: z.boolean().default(false), +export const zTagCategory = z + .strictObject({ + prefix: z.string().nullable().default(null), + delete_with_command: z.boolean().default(false), - user_tag_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Per user, per tag - user_category_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Per user, per tag category - global_tag_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Any user, per tag - allow_mentions: z.boolean().nullable().default(null), - global_category_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Any user, per category - auto_delete_command: z.boolean().nullable().default(null), + user_tag_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Per user, per tag + user_category_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Per user, per tag category + global_tag_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Any user, per tag + allow_mentions: z.boolean().nullable().default(null), + global_category_cooldown: z.union([z.string(), z.number()]).nullable().default(null), // Any user, per category + auto_delete_command: z.boolean().nullable().default(null), - tags: z.record(z.string(), zTag), + tags: z.record(z.string(), zTag), - can_use: z.boolean().nullable().default(null), -}) - .refine( - (parsed) => ! (parsed.auto_delete_command && parsed.delete_with_command), - { - message: "Cannot have both (category specific) delete_with_command and auto_delete_command enabled", - }, - ); + can_use: z.boolean().nullable().default(null), + }) + .refine((parsed) => !(parsed.auto_delete_command && parsed.delete_with_command), { + message: "Cannot have both (category specific) delete_with_command and auto_delete_command enabled", + }); export type TTagCategory = z.infer<typeof zTagCategory>; -export const zTagsConfig = z.strictObject({ - prefix: z.string(), - delete_with_command: z.boolean(), +export const zTagsConfig = z + .strictObject({ + prefix: z.string(), + delete_with_command: z.boolean(), - user_tag_cooldown: z.union([z.string(), z.number()]).nullable(), // Per user, per tag - global_tag_cooldown: z.union([z.string(), z.number()]).nullable(), // Any user, per tag - user_cooldown: z.union([z.string(), z.number()]).nullable(), // Per user - allow_mentions: z.boolean(), // Per user - global_cooldown: z.union([z.string(), z.number()]).nullable(), // Any tag use - auto_delete_command: z.boolean(), // Any tag + user_tag_cooldown: z.union([z.string(), z.number()]).nullable(), // Per user, per tag + global_tag_cooldown: z.union([z.string(), z.number()]).nullable(), // Any user, per tag + user_cooldown: z.union([z.string(), z.number()]).nullable(), // Per user + allow_mentions: z.boolean(), // Per user + global_cooldown: z.union([z.string(), z.number()]).nullable(), // Any tag use + auto_delete_command: z.boolean(), // Any tag - categories: z.record(z.string(), zTagCategory), + categories: z.record(z.string(), zTagCategory), - can_create: z.boolean(), - can_use: z.boolean(), - can_list: z.boolean(), -}) -.refine( - (parsed) => ! (parsed.auto_delete_command && parsed.delete_with_command), - { + can_create: z.boolean(), + can_use: z.boolean(), + can_list: z.boolean(), + }) + .refine((parsed) => !(parsed.auto_delete_command && parsed.delete_with_command), { message: "Cannot have both (category specific) delete_with_command and auto_delete_command enabled", - }, -); + }); export interface TagsPluginType extends BasePluginType { config: z.infer<typeof zTagsConfig>; diff --git a/backend/src/plugins/Tags/util/onMessageCreate.ts b/backend/src/plugins/Tags/util/onMessageCreate.ts index aa7c8490..f06d3282 100644 --- a/backend/src/plugins/Tags/util/onMessageCreate.ts +++ b/backend/src/plugins/Tags/util/onMessageCreate.ts @@ -2,11 +2,11 @@ import { Snowflake, TextChannel } from "discord.js"; import { GuildPluginData } from "knub"; import { SavedMessage } from "../../../data/entities/SavedMessage"; import { convertDelayStringToMS, resolveMember, zStrictMessageContent } from "../../../utils"; +import { erisAllowedMentionsToDjsMentionOptions } from "../../../utils/erisAllowedMentionsToDjsMentionOptions"; import { messageIsEmpty } from "../../../utils/messageIsEmpty"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { TagsPluginType } from "../types"; import { matchAndRenderTagFromString } from "./matchAndRenderTagFromString"; -import { erisAllowedMentionsToDjsMentionOptions } from "../../../utils/erisAllowedMentionsToDjsMentionOptions"; export async function onMessageCreate(pluginData: GuildPluginData<TagsPluginType>, msg: SavedMessage) { if (msg.is_bot) return; @@ -85,7 +85,7 @@ export async function onMessageCreate(pluginData: GuildPluginData<TagsPluginType } const validated = zStrictMessageContent.safeParse(tagResult.renderedContent); - if (! validated.success) { + if (!validated.success) { pluginData.getPlugin(LogsPlugin).logBotAlert({ body: `Rendering tag ${tagResult.tagName} resulted in an invalid message: ${validated.error.message}`, }); diff --git a/backend/src/plugins/Tags/util/renderTagFromString.ts b/backend/src/plugins/Tags/util/renderTagFromString.ts index f1265858..10306777 100644 --- a/backend/src/plugins/Tags/util/renderTagFromString.ts +++ b/backend/src/plugins/Tags/util/renderTagFromString.ts @@ -1,13 +1,13 @@ import { GuildMember } from "discord.js"; import { GuildPluginData } from "knub"; import { parseArguments } from "knub-command-manager"; +import { logger } from "../../../logger"; import { TemplateParseError } from "../../../templateFormatter"; import { StrictMessageContent, validateAndParseMessageContent } from "../../../utils"; import { memberToTemplateSafeMember, userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { TTag, TagsPluginType } from "../types"; import { renderTagBody } from "./renderTagBody"; -import { logger } from "../../../logger"; export async function renderTagFromString( pluginData: GuildPluginData<TagsPluginType>, @@ -36,14 +36,12 @@ export async function renderTagFromString( return validateAndParseMessageContent(rendered); } catch (e) { const logs = pluginData.getPlugin(LogsPlugin); - const errorMessage = e instanceof TemplateParseError - ? e.message - : "Internal error"; + const errorMessage = e instanceof TemplateParseError ? e.message : "Internal error"; logs.logBotAlert({ body: `Failed to render tag \`${prefix}${tagName}\`: ${errorMessage}`, }); - if (! (e instanceof TemplateParseError)) { + if (!(e instanceof TemplateParseError)) { logger.warn(`Internal error rendering tag ${tagName} in ${pluginData.guild.id}: ${e}`); } diff --git a/backend/src/plugins/Utility/commands/InfoCmd.ts b/backend/src/plugins/Utility/commands/InfoCmd.ts index a430898e..62f89bd6 100644 --- a/backend/src/plugins/Utility/commands/InfoCmd.ts +++ b/backend/src/plugins/Utility/commands/InfoCmd.ts @@ -79,11 +79,7 @@ export const InfoCmd = utilityCmd({ const messageTarget = await resolveMessageTarget(pluginData, value); if (messageTarget) { if (canReadChannel(messageTarget.channel, message.member)) { - const embed = await getMessageInfoEmbed( - pluginData, - messageTarget.channel.id, - messageTarget.messageId, - ); + const embed = await getMessageInfoEmbed(pluginData, messageTarget.channel.id, messageTarget.messageId); if (embed) { message.channel.send({ embeds: [embed] }); return; diff --git a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts index 72df1f6c..19164ca4 100644 --- a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts +++ b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts @@ -20,11 +20,7 @@ export const MessageInfoCmd = utilityCmd({ return; } - const embed = await getMessageInfoEmbed( - pluginData, - args.message.channel.id, - args.message.messageId, - ); + const embed = await getMessageInfoEmbed(pluginData, args.message.channel.id, args.message.messageId); if (!embed) { sendErrorMessage(pluginData, message.channel, "Unknown message"); return; diff --git a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts index 700546ca..dd19d9b2 100644 --- a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts @@ -6,10 +6,7 @@ import { UtilityPluginType } from "../types"; const MENTION_ICON = "https://cdn.discordapp.com/attachments/705009450855039042/839284872152481792/mention.png"; -export async function getRoleInfoEmbed( - pluginData: GuildPluginData<UtilityPluginType>, - role: Role, -): Promise<APIEmbed> { +export async function getRoleInfoEmbed(pluginData: GuildPluginData<UtilityPluginType>, role: Role): Promise<APIEmbed> { const embed: EmbedWith<"fields" | "author" | "color"> = { fields: [], author: { diff --git a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts index 7009241c..8449ba74 100644 --- a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts @@ -4,10 +4,7 @@ import { snowflakeToTimestamp } from "../../../utils/snowflakeToTimestamp"; const SNOWFLAKE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/742020790471491668/snowflake.png"; -export async function getSnowflakeInfoEmbed( - snowflake: string, - showUnknownWarning = false, -): Promise<APIEmbed> { +export async function getSnowflakeInfoEmbed(snowflake: string, showUnknownWarning = false): Promise<APIEmbed> { const embed: EmbedWith<"fields" | "author"> = { fields: [], author: { diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index dbaacf27..92b5b75f 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -14,7 +14,15 @@ import { ArgsFromSignatureOrArray, GuildPluginData } from "knub"; import moment from "moment-timezone"; import { RegExpRunner, allowTimeout } from "../../RegExpRunner"; import { getBaseUrl, sendErrorMessage } from "../../pluginUtils"; -import { InvalidRegexError, MINUTES, inputPatternToRegExp, multiSorter, renderUserUsername, sorter, trimLines } from "../../utils"; +import { + InvalidRegexError, + MINUTES, + inputPatternToRegExp, + multiSorter, + renderUserUsername, + sorter, + trimLines, +} from "../../utils"; import { asyncFilter } from "../../utils/async"; import { hasDiscordPermissions } from "../../utils/hasDiscordPermissions"; import { banSearchSignature } from "./commands/BanSearchCmd"; diff --git a/backend/src/plugins/ZeppelinPluginBlueprint.ts b/backend/src/plugins/ZeppelinPluginBlueprint.ts index aa266abc..338182df 100644 --- a/backend/src/plugins/ZeppelinPluginBlueprint.ts +++ b/backend/src/plugins/ZeppelinPluginBlueprint.ts @@ -7,8 +7,8 @@ import { GuildPluginBlueprint, GuildPluginData, } from "knub"; -import { TMarkdown } from "../types"; import { ZodTypeAny } from "zod"; +import { TMarkdown } from "../types"; /** * GUILD PLUGINS diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 97f51301..8790ab50 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -87,8 +87,10 @@ export function isDiscordAPIError(err: Error | string): err is DiscordAPIError { } // null | undefined -> undefined -export function zNullishToUndefined<T extends z.ZodTypeAny>(type: T): ZodEffects<T, NonNullable<z.output<T>> | undefined> { - return type.transform(v => v ?? undefined); +export function zNullishToUndefined<T extends z.ZodTypeAny>( + type: T, +): ZodEffects<T, NonNullable<z.output<T>> | undefined> { + return type.transform((v) => v ?? undefined); } export function getScalarDifference<T extends object>( @@ -151,15 +153,18 @@ export type GroupDMInvite = Invite & { }; export function zBoundedCharacters(min: number, max: number) { - return z.string().refine(str => { - const len = [...str].length; // Unicode aware character split - return (len >= min && len <= max); - }, { - message: `String must be between ${min} and ${max} characters long`, - }); + return z.string().refine( + (str) => { + const len = [...str].length; // Unicode aware character split + return len >= min && len <= max; + }, + { + message: `String must be between ${min} and ${max} characters long`, + }, + ); } -export const zSnowflake = z.string().refine(str => isSnowflake(str), { +export const zSnowflake = z.string().refine((str) => isSnowflake(str), { message: "Invalid snowflake ID", }); @@ -188,7 +193,7 @@ export function zRegex<T extends ZodString>(zStr: T) { if (err instanceof InvalidRegexError) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Invalid regex" + message: "Invalid regex", }); return z.NEVER; } @@ -343,8 +348,18 @@ function dropNullValuesRecursively(obj: any) { */ export const zAllowedMentions = z.strictObject({ everyone: zNullishToUndefined(z.boolean().nullable().optional()), - users: zNullishToUndefined(z.union([z.boolean(), z.array(z.string())]).nullable().optional()), - roles: zNullishToUndefined(z.union([z.boolean(), z.array(z.string())]).nullable().optional()), + users: zNullishToUndefined( + z + .union([z.boolean(), z.array(z.string())]) + .nullable() + .optional(), + ), + roles: zNullishToUndefined( + z + .union([z.boolean(), z.array(z.string())]) + .nullable() + .optional(), + ), replied_user: zNullishToUndefined(z.boolean().nullable().optional()), }); @@ -359,18 +374,28 @@ export function dropPropertiesByName(obj, propName) { } } -export function zBoundedRecord<TRecord extends ZodRecord<any, any>>(record: TRecord, minKeys: number, maxKeys: number): ZodEffects<TRecord> { - return record.refine(data => { - const len = Object.keys(data).length; - return (len >= minKeys && len <= maxKeys); - }, { - message: `Object must have ${minKeys}-${maxKeys} keys`, - }); +export function zBoundedRecord<TRecord extends ZodRecord<any, any>>( + record: TRecord, + minKeys: number, + maxKeys: number, +): ZodEffects<TRecord> { + return record.refine( + (data) => { + const len = Object.keys(data).length; + return len >= minKeys && len <= maxKeys; + }, + { + message: `Object must have ${minKeys}-${maxKeys} keys`, + }, + ); } -export const zDelayString = z.string().max(32).refine(str => convertDelayStringToMS(str) !== null, { - message: "Invalid delay string", -}); +export const zDelayString = z + .string() + .max(32) + .refine((str) => convertDelayStringToMS(str) !== null, { + message: "Invalid delay string", + }); // To avoid running into issues with the JS max date vaLue, we cap maximum delay strings *far* below that. // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#The_ECMAScript_epoch_and_timestamps @@ -1487,9 +1512,11 @@ export function renderUserUsername(user: User | UnknownUser): string { return renderUsername(user.username, user.discriminator); } -type Entries<T> = Array<{ - [Key in keyof T]-?: [Key, T[Key]]; -}[keyof T]>; +type Entries<T> = Array< + { + [Key in keyof T]-?: [Key, T[Key]]; + }[keyof T] +>; export function entries<T extends object>(object: T) { return Object.entries(object) as Entries<T>; diff --git a/backend/src/utils/formatZodIssue.ts b/backend/src/utils/formatZodIssue.ts index 2b5c3d8e..93932f66 100644 --- a/backend/src/utils/formatZodIssue.ts +++ b/backend/src/utils/formatZodIssue.ts @@ -1,6 +1,6 @@ import { ZodIssue } from "zod"; export function formatZodIssue(issue: ZodIssue): string { - const path = issue.path.join("/"); - return `${path}: ${issue.message}`; + const path = issue.path.join("/"); + return `${path}: ${issue.message}`; } diff --git a/backend/src/validateActiveConfigs.ts b/backend/src/validateActiveConfigs.ts index c9e3414b..f985fd14 100644 --- a/backend/src/validateActiveConfigs.ts +++ b/backend/src/validateActiveConfigs.ts @@ -6,7 +6,10 @@ import { loadYamlSafely } from "./utils/loadYamlSafely"; import { ObjectAliasError } from "./utils/validateNoObjectAliases"; function writeError(key: string, error: string) { - const indented = error.split("\n").map(s => " ".repeat(64) + s).join("\n"); + const indented = error + .split("\n") + .map((s) => " ".repeat(64) + s) + .join("\n"); const prefix = `Invalid config ${key}:`; const prefixed = prefix + indented.slice(prefix.length); console.log(prefixed + "\n\n"); From 710bedd050b59e501d6209b9c3ca38e6f83e0353 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:38:02 +0200 Subject: [PATCH 45/51] chore: run prettier --- backend/src/plugins/Automod/actions/startThread.ts | 7 +------ backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts | 5 +---- .../src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts | 5 +---- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/backend/src/plugins/Automod/actions/startThread.ts b/backend/src/plugins/Automod/actions/startThread.ts index 94e0e0e3..41648ad4 100644 --- a/backend/src/plugins/Automod/actions/startThread.ts +++ b/backend/src/plugins/Automod/actions/startThread.ts @@ -1,9 +1,4 @@ -import { - ChannelType, - GuildTextThreadCreateOptions, - ThreadAutoArchiveDuration, - ThreadChannel, -} from "discord.js"; +import { ChannelType, GuildTextThreadCreateOptions, ThreadAutoArchiveDuration, ThreadChannel } from "discord.js"; import z from "zod"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { MINUTES, convertDelayStringToMS, noop, zBoundedCharacters, zDelayString } from "../../../utils"; diff --git a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts index 700546ca..dd19d9b2 100644 --- a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts @@ -6,10 +6,7 @@ import { UtilityPluginType } from "../types"; const MENTION_ICON = "https://cdn.discordapp.com/attachments/705009450855039042/839284872152481792/mention.png"; -export async function getRoleInfoEmbed( - pluginData: GuildPluginData<UtilityPluginType>, - role: Role, -): Promise<APIEmbed> { +export async function getRoleInfoEmbed(pluginData: GuildPluginData<UtilityPluginType>, role: Role): Promise<APIEmbed> { const embed: EmbedWith<"fields" | "author" | "color"> = { fields: [], author: { diff --git a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts index 7009241c..8449ba74 100644 --- a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts @@ -4,10 +4,7 @@ import { snowflakeToTimestamp } from "../../../utils/snowflakeToTimestamp"; const SNOWFLAKE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/742020790471491668/snowflake.png"; -export async function getSnowflakeInfoEmbed( - snowflake: string, - showUnknownWarning = false, -): Promise<APIEmbed> { +export async function getSnowflakeInfoEmbed(snowflake: string, showUnknownWarning = false): Promise<APIEmbed> { const embed: EmbedWith<"fields" | "author"> = { fields: [], author: { From e42bbb953fe34bd8d6b4991c5979b6fc443d6909 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:47:59 +0200 Subject: [PATCH 46/51] fix: use lower casesPerPage Temporary fix to make it unlikely to go over the 4096 character limit Needs a proper fix in the future where we pre-render the case summaries and paginate based on their length. --- backend/src/plugins/ModActions/commands/CasesModCmd.ts | 2 +- backend/src/plugins/ModActions/commands/CasesUserCmd.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/ModActions/commands/CasesModCmd.ts b/backend/src/plugins/ModActions/commands/CasesModCmd.ts index 67363fe5..306c1ad1 100644 --- a/backend/src/plugins/ModActions/commands/CasesModCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesModCmd.ts @@ -12,7 +12,7 @@ const opts = { mod: ct.userId({ option: true }), }; -const casesPerPage = 10; +const casesPerPage = 5; export const CasesModCmd = modActionsCmd({ trigger: ["cases", "modlogs", "infractions"], diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts index 08c6f2a6..1fd8973b 100644 --- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts @@ -28,7 +28,7 @@ const opts = { unbans: ct.switchOption({ def: false, shortcut: "ub" }), }; -const casesPerPage = 10; +const casesPerPage = 5; export const CasesUserCmd = modActionsCmd({ trigger: ["cases", "modlogs"], From 90c7024b053e393b975579222107b5ef4b02f9d4 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:50:57 +0200 Subject: [PATCH 47/51] chore: run prettier --- backend/src/plugins/ModActions/commands/CasesUserCmd.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts index 1fd8973b..08e586c8 100644 --- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts @@ -3,14 +3,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes"; import { CaseTypes } from "../../../data/CaseTypes"; import { sendErrorMessage } from "../../../pluginUtils"; import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; -import { - UnknownUser, - chunkArray, - emptyEmbedValue, - renderUsername, - resolveMember, - resolveUser, -} from "../../../utils"; +import { UnknownUser, chunkArray, emptyEmbedValue, renderUsername, resolveMember, resolveUser } from "../../../utils"; import { asyncMap } from "../../../utils/async"; import { createPaginatedMessage } from "../../../utils/createPaginatedMessage.js"; import { getGuildPrefix } from "../../../utils/getGuildPrefix"; From 6840fb464632aafd1359020df5f8a8b40cde0243 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:57:57 +0200 Subject: [PATCH 48/51] fix: clamp counter values in config --- backend/src/data/GuildCounters.ts | 9 ++++++--- backend/src/plugins/Automod/actions/setCounter.ts | 3 ++- backend/src/plugins/Counters/types.ts | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/src/data/GuildCounters.ts b/backend/src/data/GuildCounters.ts index 0c8fde5b..18c9b5e6 100644 --- a/backend/src/data/GuildCounters.ts +++ b/backend/src/data/GuildCounters.ts @@ -12,7 +12,8 @@ import { CounterValue } from "./entities/CounterValue"; const DELETE_UNUSED_COUNTERS_AFTER = 1 * DAYS; const DELETE_UNUSED_COUNTER_TRIGGERS_AFTER = 1 * DAYS; -const MAX_COUNTER_VALUE = 2147483647; // 2^31-1, for MySQL INT +export const MIN_COUNTER_VALUE = 0; +export const MAX_COUNTER_VALUE = 2147483647; // 2^31-1, for MySQL INT const decayQueue = new Queue(); @@ -115,7 +116,9 @@ export class GuildCounters extends BaseGuildRepository { userId = userId || "0"; const rawUpdate = - change >= 0 ? `value = LEAST(value + ${change}, ${MAX_COUNTER_VALUE})` : `value = GREATEST(value ${change}, 0)`; + change >= 0 + ? `value = LEAST(value + ${change}, ${MAX_COUNTER_VALUE})` + : `value = GREATEST(value ${change}, ${MIN_COUNTER_VALUE})`; await this.counterValues.query( ` @@ -173,7 +176,7 @@ export class GuildCounters extends BaseGuildRepository { const rawUpdate = decayAmountToApply >= 0 - ? `GREATEST(value - ${decayAmountToApply}, 0)` + ? `GREATEST(value - ${decayAmountToApply}, ${MIN_COUNTER_VALUE})` : `LEAST(value + ${Math.abs(decayAmountToApply)}, ${MAX_COUNTER_VALUE})`; // Using an UPDATE with ORDER BY in an attempt to avoid deadlocks from simultaneous decays diff --git a/backend/src/plugins/Automod/actions/setCounter.ts b/backend/src/plugins/Automod/actions/setCounter.ts index dea4bdd6..604d9bf4 100644 --- a/backend/src/plugins/Automod/actions/setCounter.ts +++ b/backend/src/plugins/Automod/actions/setCounter.ts @@ -1,4 +1,5 @@ import z from "zod"; +import { MAX_COUNTER_VALUE, MIN_COUNTER_VALUE } from "../../../data/GuildCounters"; import { zBoundedCharacters } from "../../../utils"; import { CountersPlugin } from "../../Counters/CountersPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin"; @@ -7,7 +8,7 @@ import { automodAction } from "../helpers"; export const SetCounterAction = automodAction({ configSchema: z.strictObject({ counter: zBoundedCharacters(0, 100), - value: z.number(), + value: z.number().min(MIN_COUNTER_VALUE).max(MAX_COUNTER_VALUE), }), async apply({ pluginData, contexts, actionConfig, ruleName }) { diff --git a/backend/src/plugins/Counters/types.ts b/backend/src/plugins/Counters/types.ts index 8e70c4ab..ef322e7b 100644 --- a/backend/src/plugins/Counters/types.ts +++ b/backend/src/plugins/Counters/types.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events"; import { BasePluginType } from "knub"; import z from "zod"; -import { GuildCounters } from "../../data/GuildCounters"; +import { GuildCounters, MAX_COUNTER_VALUE, MIN_COUNTER_VALUE } from "../../data/GuildCounters"; import { CounterTrigger, buildCounterConditionString, @@ -93,7 +93,7 @@ export const zCounter = z.strictObject({ pretty_name: zBoundedCharacters(0, 100).nullable().default(null), per_channel: z.boolean().default(false), per_user: z.boolean().default(false), - initial_value: z.number().default(0), + initial_value: z.number().min(MIN_COUNTER_VALUE).max(MAX_COUNTER_VALUE).default(0), triggers: zBoundedRecord(z.record(zBoundedCharacters(0, 100), zTriggerInput), 1, MAX_TRIGGERS_PER_COUNTER), decay: z .strictObject({ From 008ac1bec0b86a0c0f0c708c5502bd9bcc2d274a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 15:04:39 +0200 Subject: [PATCH 49/51] fix: missing imports --- backend/src/plugins/Automod/triggers/roleAdded.ts | 2 +- backend/src/plugins/Automod/triggers/roleRemoved.ts | 2 +- backend/src/plugins/Utility/search.ts | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/src/plugins/Automod/triggers/roleAdded.ts b/backend/src/plugins/Automod/triggers/roleAdded.ts index e2a5767b..5a9ee3af 100644 --- a/backend/src/plugins/Automod/triggers/roleAdded.ts +++ b/backend/src/plugins/Automod/triggers/roleAdded.ts @@ -1,6 +1,6 @@ import { Snowflake } from "discord.js"; import z from "zod"; -import { zSnowflake } from "../../../utils"; +import { renderUsername, zSnowflake } from "../../../utils"; import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges"; import { automodTrigger } from "../helpers"; diff --git a/backend/src/plugins/Automod/triggers/roleRemoved.ts b/backend/src/plugins/Automod/triggers/roleRemoved.ts index a5659ecb..82479c0e 100644 --- a/backend/src/plugins/Automod/triggers/roleRemoved.ts +++ b/backend/src/plugins/Automod/triggers/roleRemoved.ts @@ -1,6 +1,6 @@ import { Snowflake } from "discord.js"; import z from "zod"; -import { zSnowflake } from "../../../utils"; +import { renderUsername, zSnowflake } from "../../../utils"; import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges"; import { automodTrigger } from "../helpers"; diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index 105afc86..4f5d08ef 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -14,7 +14,15 @@ import { ArgsFromSignatureOrArray, GuildPluginData } from "knub"; import moment from "moment-timezone"; import { RegExpRunner, allowTimeout } from "../../RegExpRunner"; import { getBaseUrl, sendErrorMessage } from "../../pluginUtils"; -import { MINUTES, multiSorter, renderUsername, sorter, trimLines } from "../../utils"; +import { + InvalidRegexError, + MINUTES, + inputPatternToRegExp, + multiSorter, + renderUsername, + sorter, + trimLines, +} from "../../utils"; import { asyncFilter } from "../../utils/async"; import { hasDiscordPermissions } from "../../utils/hasDiscordPermissions"; import { banSearchSignature } from "./commands/BanSearchCmd"; From 2ce5082018118fafd5cd860b62d692d7da6cc688 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 15:05:35 +0200 Subject: [PATCH 50/51] fix: shorter than 6 character invite codes Fixes ZDEV-108 --- backend/src/utils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 49a03c27..174a720f 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -557,11 +557,12 @@ export function getUrlsInString(str: string, onlyUnique = false): MatchedURL[] { } export function parseInviteCodeInput(str: string): string { - if (str.match(/^[a-z0-9]{6,}$/i)) { - return str; + const parsedInviteCodes = getInviteCodesInString(str); + if (parsedInviteCodes.length) { + return parsedInviteCodes[0]; } - return getInviteCodesInString(str)[0]; + return str; } export function isNotNull<T>(value: T): value is Exclude<T, null | undefined> { From ffa9eeb3f53762fa9014f2fe5cd7bdfa8c867138 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 27 Jan 2024 16:01:48 +0200 Subject: [PATCH 51/51] feat: handle template errors Fixes ZDEV-20 --- backend/src/RecoverablePluginError.ts | 2 + .../plugins/Automod/actions/changePerms.ts | 55 +++++++++++----- backend/src/plugins/Automod/actions/reply.ts | 21 ++++-- .../plugins/Automod/actions/startThread.ts | 23 +++++-- .../CustomEvents/CustomEventsPlugin.ts | 2 + .../CustomEvents/actions/addRoleAction.ts | 6 +- .../CustomEvents/actions/createCaseAction.ts | 11 ++-- .../CustomEvents/actions/messageAction.ts | 6 +- .../actions/moveToVoiceChannelAction.ts | 11 +++- .../CustomEvents/catchTemplateError.ts | 13 ++++ .../plugins/ModActions/functions/banUserId.ts | 66 ++++++++++++------- .../ModActions/functions/kickMember.ts | 33 ++++++---- .../ModActions/functions/warnMember.ts | 33 ++++++---- .../src/plugins/Mutes/functions/muteUser.ts | 43 +++++++----- 14 files changed, 231 insertions(+), 94 deletions(-) create mode 100644 backend/src/plugins/CustomEvents/catchTemplateError.ts diff --git a/backend/src/RecoverablePluginError.ts b/backend/src/RecoverablePluginError.ts index 46a86ae5..64fed618 100644 --- a/backend/src/RecoverablePluginError.ts +++ b/backend/src/RecoverablePluginError.ts @@ -11,6 +11,7 @@ export enum ERRORS { MUTE_ROLE_ABOVE_ZEP, USER_ABOVE_ZEP, USER_NOT_MODERATABLE, + TEMPLATE_PARSE_ERROR, } export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = { @@ -24,6 +25,7 @@ export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = { [ERRORS.MUTE_ROLE_ABOVE_ZEP]: "Specified mute role is above Zeppelin in the role hierarchy", [ERRORS.USER_ABOVE_ZEP]: "Cannot mute user, specified user is above Zeppelin in the role hierarchy", [ERRORS.USER_NOT_MODERATABLE]: "Cannot mute user, specified user is not moderatable", + [ERRORS.TEMPLATE_PARSE_ERROR]: "Template parse error", }; export class RecoverablePluginError extends Error { diff --git a/backend/src/plugins/Automod/actions/changePerms.ts b/backend/src/plugins/Automod/actions/changePerms.ts index 7b7eba37..351b1637 100644 --- a/backend/src/plugins/Automod/actions/changePerms.ts +++ b/backend/src/plugins/Automod/actions/changePerms.ts @@ -1,13 +1,14 @@ import { PermissionsBitField, PermissionsString } from "discord.js"; import { U } from "ts-toolbelt"; import z from "zod"; -import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; +import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { isValidSnowflake, keys, noop, zBoundedCharacters } from "../../../utils"; import { guildToTemplateSafeGuild, savedMessageToTemplateSafeSavedMessage, userToTemplateSafeUser, } from "../../../utils/templateSafeObjects"; +import { LogsPlugin } from "../../Logs/LogsPlugin"; import { automodAction } from "../helpers"; type LegacyPermMap = Record<string, keyof (typeof PermissionsBitField)["Flags"]>; @@ -71,30 +72,52 @@ export const ChangePermsAction = automodAction({ perms: z.record(z.enum(allPermissionNames), z.boolean().nullable()), }), - async apply({ pluginData, contexts, actionConfig }) { + async apply({ pluginData, contexts, actionConfig, ruleName }) { const user = contexts.find((c) => c.user)?.user; const message = contexts.find((c) => c.message)?.message; - const renderTarget = async (str: string) => - renderTemplate( - str, + let target: string; + try { + target = await renderTemplate( + actionConfig.target, new TemplateSafeValueContainer({ user: user ? userToTemplateSafeUser(user) : null, guild: guildToTemplateSafeGuild(pluginData.guild), message: message ? savedMessageToTemplateSafeSavedMessage(message) : null, }), ); - const renderChannel = async (str: string) => - renderTemplate( - str, - new TemplateSafeValueContainer({ - user: user ? userToTemplateSafeUser(user) : null, - guild: guildToTemplateSafeGuild(pluginData.guild), - message: message ? savedMessageToTemplateSafeSavedMessage(message) : null, - }), - ); - const target = await renderTarget(actionConfig.target); - const channelId = actionConfig.channel ? await renderChannel(actionConfig.channel) : null; + } catch (err) { + if (err instanceof TemplateParseError) { + pluginData.getPlugin(LogsPlugin).logBotAlert({ + body: `Error in target format of automod rule ${ruleName}: ${err.message}`, + }); + return; + } + throw err; + } + + let channelId: string | null = null; + if (actionConfig.channel) { + try { + channelId = await renderTemplate( + actionConfig.channel, + new TemplateSafeValueContainer({ + user: user ? userToTemplateSafeUser(user) : null, + guild: guildToTemplateSafeGuild(pluginData.guild), + message: message ? savedMessageToTemplateSafeSavedMessage(message) : null, + }), + ); + } catch (err) { + if (err instanceof TemplateParseError) { + pluginData.getPlugin(LogsPlugin).logBotAlert({ + body: `Error in channel format of automod rule ${ruleName}: ${err.message}`, + }); + return; + } + throw err; + } + } + const role = pluginData.guild.roles.resolve(target); if (!role) { const member = await pluginData.guild.members.fetch(target).catch(noop); diff --git a/backend/src/plugins/Automod/actions/reply.ts b/backend/src/plugins/Automod/actions/reply.ts index e8b1fdf8..0628e26f 100644 --- a/backend/src/plugins/Automod/actions/reply.ts +++ b/backend/src/plugins/Automod/actions/reply.ts @@ -1,6 +1,6 @@ import { GuildTextBasedChannel, MessageCreateOptions, PermissionsBitField, Snowflake, User } from "discord.js"; import z from "zod"; -import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; +import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { convertDelayStringToMS, noop, @@ -58,10 +58,21 @@ export const ReplyAction = automodAction({ }), ); - const formatted = - typeof actionConfig === "string" - ? await renderReplyText(actionConfig) - : ((await renderRecursively(actionConfig.text, renderReplyText)) as MessageCreateOptions); + let formatted: string | MessageCreateOptions; + try { + formatted = + typeof actionConfig === "string" + ? await renderReplyText(actionConfig) + : ((await renderRecursively(actionConfig.text, renderReplyText)) as MessageCreateOptions); + } catch (err) { + if (err instanceof TemplateParseError) { + pluginData.getPlugin(LogsPlugin).logBotAlert({ + body: `Error in reply format of automod rule \`${ruleName}\`: ${err.message}`, + }); + return; + } + throw err; + } if (formatted) { const channel = pluginData.guild.channels.cache.get(channelId as Snowflake) as GuildTextBasedChannel; diff --git a/backend/src/plugins/Automod/actions/startThread.ts b/backend/src/plugins/Automod/actions/startThread.ts index 41648ad4..a521d72f 100644 --- a/backend/src/plugins/Automod/actions/startThread.ts +++ b/backend/src/plugins/Automod/actions/startThread.ts @@ -1,8 +1,9 @@ import { ChannelType, GuildTextThreadCreateOptions, ThreadAutoArchiveDuration, ThreadChannel } from "discord.js"; import z from "zod"; -import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; +import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { MINUTES, convertDelayStringToMS, noop, zBoundedCharacters, zDelayString } from "../../../utils"; import { savedMessageToTemplateSafeSavedMessage, userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; +import { LogsPlugin } from "../../Logs/LogsPlugin"; import { automodAction } from "../helpers"; const validThreadAutoArchiveDurations: ThreadAutoArchiveDuration[] = [ @@ -21,7 +22,7 @@ export const StartThreadAction = automodAction({ limit_per_channel: z.number().nullable().default(5), }), - async apply({ pluginData, contexts, actionConfig }) { + async apply({ pluginData, contexts, actionConfig, ruleName }) { // check if the message still exists, we don't want to create threads for deleted messages const threads = contexts.filter((c) => { if (!c.message || !c.user) return false; @@ -48,15 +49,25 @@ export const StartThreadAction = automodAction({ const channel = pluginData.guild.channels.cache.get(threadContext.message!.channel_id); if (!channel || !("threads" in channel) || channel.isThreadOnly()) continue; - const renderThreadName = async (str: string) => - renderTemplate( - str, + let threadName: string; + try { + threadName = await renderTemplate( + actionConfig.name ?? "{user.renderedUsername}'s thread", new TemplateSafeValueContainer({ user: userToTemplateSafeUser(threadContext.user!), msg: savedMessageToTemplateSafeSavedMessage(threadContext.message!), }), ); - const threadName = await renderThreadName(actionConfig.name ?? "{user.renderedUsername}'s thread"); + } catch (err) { + if (err instanceof TemplateParseError) { + pluginData.getPlugin(LogsPlugin).logBotAlert({ + body: `Error in thread name format of automod rule ${ruleName}: ${err.message}`, + }); + return; + } + throw err; + } + const threadOptions: GuildTextThreadCreateOptions<unknown> = { name: threadName, autoArchiveDuration: autoArchive, diff --git a/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts b/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts index e17cd96b..82c5e612 100644 --- a/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts +++ b/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts @@ -11,6 +11,7 @@ import { messageToTemplateSafeMessage, userToTemplateSafeUser, } from "../../utils/templateSafeObjects"; +import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { runEvent } from "./functions/runEvent"; import { CustomEventsPluginType, zCustomEventsConfig } from "./types"; @@ -25,6 +26,7 @@ export const CustomEventsPlugin = zeppelinGuildPlugin<CustomEventsPluginType>()( name: "custom_events", showInDocs: false, + dependencies: () => [LogsPlugin], configParser: (input) => zCustomEventsConfig.parse(input), defaultOptions, diff --git a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts index f0870958..cd647687 100644 --- a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts +++ b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts @@ -4,6 +4,7 @@ import { canActOn } from "../../../pluginUtils"; import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter"; import { resolveMember, zSnowflake } from "../../../utils"; import { ActionError } from "../ActionError"; +import { catchTemplateError } from "../catchTemplateError"; import { CustomEventsPluginType, TCustomEvent } from "../types"; export const zAddRoleAction = z.strictObject({ @@ -20,7 +21,10 @@ export async function addRoleAction( event: TCustomEvent, eventData: any, ) { - const targetId = await renderTemplate(action.target, values, false); + const targetId = await catchTemplateError( + () => renderTemplate(action.target, values, false), + "Invalid target format", + ); const target = await resolveMember(pluginData.client, pluginData.guild, targetId); if (!target) throw new ActionError(`Unknown target member: ${targetId}`); diff --git a/backend/src/plugins/CustomEvents/actions/createCaseAction.ts b/backend/src/plugins/CustomEvents/actions/createCaseAction.ts index e894a446..a5e624fa 100644 --- a/backend/src/plugins/CustomEvents/actions/createCaseAction.ts +++ b/backend/src/plugins/CustomEvents/actions/createCaseAction.ts @@ -5,6 +5,7 @@ import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFor import { zBoundedCharacters, zSnowflake } from "../../../utils"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { ActionError } from "../ActionError"; +import { catchTemplateError } from "../catchTemplateError"; import { CustomEventsPluginType, TCustomEvent } from "../types"; export const zCreateCaseAction = z.strictObject({ @@ -23,10 +24,12 @@ export async function createCaseAction( event: TCustomEvent, eventData: any, // eslint-disable-line @typescript-eslint/no-unused-vars ) { - const modId = await renderTemplate(action.mod, values, false); - const targetId = await renderTemplate(action.target, values, false); - - const reason = await renderTemplate(action.reason, values, false); + const modId = await catchTemplateError(() => renderTemplate(action.mod, values, false), "Invalid mod format"); + const targetId = await catchTemplateError( + () => renderTemplate(action.target, values, false), + "Invalid target format", + ); + const reason = await catchTemplateError(() => renderTemplate(action.reason, values, false), "Invalid reason format"); if (CaseTypes[action.case_type] == null) { throw new ActionError(`Invalid case type: ${action.type}`); diff --git a/backend/src/plugins/CustomEvents/actions/messageAction.ts b/backend/src/plugins/CustomEvents/actions/messageAction.ts index 40eee4b8..27780265 100644 --- a/backend/src/plugins/CustomEvents/actions/messageAction.ts +++ b/backend/src/plugins/CustomEvents/actions/messageAction.ts @@ -4,6 +4,7 @@ import z from "zod"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { zBoundedCharacters, zSnowflake } from "../../../utils"; import { ActionError } from "../ActionError"; +import { catchTemplateError } from "../catchTemplateError"; import { CustomEventsPluginType } from "../types"; export const zMessageAction = z.strictObject({ @@ -18,7 +19,10 @@ export async function messageAction( action: TMessageAction, values: TemplateSafeValueContainer, ) { - const targetChannelId = await renderTemplate(action.channel, values, false); + const targetChannelId = await catchTemplateError( + () => renderTemplate(action.channel, values, false), + "Invalid channel format", + ); const targetChannel = pluginData.guild.channels.cache.get(targetChannelId as Snowflake); if (!targetChannel) throw new ActionError("Unknown target channel"); if (!(targetChannel instanceof TextChannel)) throw new ActionError("Target channel is not a text channel"); diff --git a/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts b/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts index d42059f5..40adcb58 100644 --- a/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts +++ b/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts @@ -5,6 +5,7 @@ import { canActOn } from "../../../pluginUtils"; import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { resolveMember, zSnowflake } from "../../../utils"; import { ActionError } from "../ActionError"; +import { catchTemplateError } from "../catchTemplateError"; import { CustomEventsPluginType, TCustomEvent } from "../types"; export const zMoveToVoiceChannelAction = z.strictObject({ @@ -21,7 +22,10 @@ export async function moveToVoiceChannelAction( event: TCustomEvent, eventData: any, ) { - const targetId = await renderTemplate(action.target, values, false); + const targetId = await catchTemplateError( + () => renderTemplate(action.target, values, false), + "Invalid target format", + ); const target = await resolveMember(pluginData.client, pluginData.guild, targetId); if (!target) throw new ActionError("Unknown target member"); @@ -29,7 +33,10 @@ export async function moveToVoiceChannelAction( throw new ActionError("Missing permissions"); } - const targetChannelId = await renderTemplate(action.channel, values, false); + const targetChannelId = await catchTemplateError( + () => renderTemplate(action.channel, values, false), + "Invalid channel format", + ); const targetChannel = pluginData.guild.channels.cache.get(targetChannelId as Snowflake); if (!targetChannel) throw new ActionError("Unknown target channel"); if (!(targetChannel instanceof VoiceChannel)) throw new ActionError("Target channel is not a voice channel"); diff --git a/backend/src/plugins/CustomEvents/catchTemplateError.ts b/backend/src/plugins/CustomEvents/catchTemplateError.ts new file mode 100644 index 00000000..015a4298 --- /dev/null +++ b/backend/src/plugins/CustomEvents/catchTemplateError.ts @@ -0,0 +1,13 @@ +import { TemplateParseError } from "../../templateFormatter"; +import { ActionError } from "./ActionError"; + +export function catchTemplateError(fn: () => Promise<string>, errorText: string): Promise<string> { + try { + return fn(); + } catch (err) { + if (err instanceof TemplateParseError) { + throw new ActionError(`${errorText}: ${err.message}`); + } + throw err; + } +} diff --git a/backend/src/plugins/ModActions/functions/banUserId.ts b/backend/src/plugins/ModActions/functions/banUserId.ts index d9d1454b..65686c81 100644 --- a/backend/src/plugins/ModActions/functions/banUserId.ts +++ b/backend/src/plugins/ModActions/functions/banUserId.ts @@ -5,7 +5,7 @@ import { CaseTypes } from "../../../data/CaseTypes"; import { LogType } from "../../../data/LogType"; import { registerExpiringTempban } from "../../../data/loops/expiringTempbansLoop"; import { logger } from "../../../logger"; -import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; +import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { DAYS, SECONDS, @@ -52,30 +52,52 @@ export async function banUserId( if (contactMethods.length) { if (!banTime && config.ban_message) { - const banMessage = await renderTemplate( - config.ban_message, - new TemplateSafeValueContainer({ - guildName: pluginData.guild.name, - reason, - moderator: banOptions.caseArgs?.modId - ? userToTemplateSafeUser(await resolveUser(pluginData.client, banOptions.caseArgs.modId)) - : null, - }), - ); + let banMessage: string; + try { + banMessage = await renderTemplate( + config.ban_message, + new TemplateSafeValueContainer({ + guildName: pluginData.guild.name, + reason, + moderator: banOptions.caseArgs?.modId + ? userToTemplateSafeUser(await resolveUser(pluginData.client, banOptions.caseArgs.modId)) + : null, + }), + ); + } catch (err) { + if (err instanceof TemplateParseError) { + return { + status: "failed", + error: `Invalid ban_message format: ${err.message}`, + }; + } + throw err; + } notifyResult = await notifyUser(member.user, banMessage, contactMethods); } else if (banTime && config.tempban_message) { - const banMessage = await renderTemplate( - config.tempban_message, - new TemplateSafeValueContainer({ - guildName: pluginData.guild.name, - reason, - moderator: banOptions.caseArgs?.modId - ? userToTemplateSafeUser(await resolveUser(pluginData.client, banOptions.caseArgs.modId)) - : null, - banTime: humanizeDuration(banTime), - }), - ); + let banMessage: string; + try { + banMessage = await renderTemplate( + config.tempban_message, + new TemplateSafeValueContainer({ + guildName: pluginData.guild.name, + reason, + moderator: banOptions.caseArgs?.modId + ? userToTemplateSafeUser(await resolveUser(pluginData.client, banOptions.caseArgs.modId)) + : null, + banTime: humanizeDuration(banTime), + }), + ); + } catch (err) { + if (err instanceof TemplateParseError) { + return { + status: "failed", + error: `Invalid tempban_message format: ${err.message}`, + }; + } + throw err; + } notifyResult = await notifyUser(member.user, banMessage, contactMethods); } else { diff --git a/backend/src/plugins/ModActions/functions/kickMember.ts b/backend/src/plugins/ModActions/functions/kickMember.ts index d54dfd15..6dc26e35 100644 --- a/backend/src/plugins/ModActions/functions/kickMember.ts +++ b/backend/src/plugins/ModActions/functions/kickMember.ts @@ -2,7 +2,7 @@ import { GuildMember } from "discord.js"; import { GuildPluginData } from "knub"; import { CaseTypes } from "../../../data/CaseTypes"; import { LogType } from "../../../data/LogType"; -import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter"; +import { renderTemplate, TemplateParseError, TemplateSafeValueContainer } from "../../../templateFormatter"; import { createUserNotificationError, notifyUser, resolveUser, ucfirst, UserNotificationResult } from "../../../utils"; import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { CasesPlugin } from "../../Cases/CasesPlugin"; @@ -31,16 +31,27 @@ export async function kickMember( if (contactMethods.length) { if (config.kick_message) { - const kickMessage = await renderTemplate( - config.kick_message, - new TemplateSafeValueContainer({ - guildName: pluginData.guild.name, - reason, - moderator: kickOptions.caseArgs?.modId - ? userToTemplateSafeUser(await resolveUser(pluginData.client, kickOptions.caseArgs.modId)) - : null, - }), - ); + let kickMessage: string; + try { + kickMessage = await renderTemplate( + config.kick_message, + new TemplateSafeValueContainer({ + guildName: pluginData.guild.name, + reason, + moderator: kickOptions.caseArgs?.modId + ? userToTemplateSafeUser(await resolveUser(pluginData.client, kickOptions.caseArgs.modId)) + : null, + }), + ); + } catch (err) { + if (err instanceof TemplateParseError) { + return { + status: "failed", + error: `Invalid kick_message format: ${err.message}`, + }; + } + throw err; + } notifyResult = await notifyUser(member.user, kickMessage, contactMethods); } else { diff --git a/backend/src/plugins/ModActions/functions/warnMember.ts b/backend/src/plugins/ModActions/functions/warnMember.ts index 9bc0fda9..9aba15f4 100644 --- a/backend/src/plugins/ModActions/functions/warnMember.ts +++ b/backend/src/plugins/ModActions/functions/warnMember.ts @@ -1,7 +1,7 @@ import { GuildMember, Snowflake } from "discord.js"; import { GuildPluginData } from "knub"; import { CaseTypes } from "../../../data/CaseTypes"; -import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; +import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { UserNotificationResult, createUserNotificationError, notifyUser, resolveUser, ucfirst } from "../../../utils"; import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; @@ -20,16 +20,27 @@ export async function warnMember( let notifyResult: UserNotificationResult; if (config.warn_message) { - const warnMessage = await renderTemplate( - config.warn_message, - new TemplateSafeValueContainer({ - guildName: pluginData.guild.name, - reason, - moderator: warnOptions.caseArgs?.modId - ? userToTemplateSafeUser(await resolveUser(pluginData.client, warnOptions.caseArgs.modId)) - : null, - }), - ); + let warnMessage: string; + try { + warnMessage = await renderTemplate( + config.warn_message, + new TemplateSafeValueContainer({ + guildName: pluginData.guild.name, + reason, + moderator: warnOptions.caseArgs?.modId + ? userToTemplateSafeUser(await resolveUser(pluginData.client, warnOptions.caseArgs.modId)) + : null, + }), + ); + } catch (err) { + if (err instanceof TemplateParseError) { + return { + status: "failed", + error: `Invalid warn_message format: ${err.message}`, + }; + } + throw err; + } const contactMethods = warnOptions?.contactMethods ? warnOptions.contactMethods : getDefaultContactMethods(pluginData, "warn"); diff --git a/backend/src/plugins/Mutes/functions/muteUser.ts b/backend/src/plugins/Mutes/functions/muteUser.ts index 575f96fb..fcc27307 100644 --- a/backend/src/plugins/Mutes/functions/muteUser.ts +++ b/backend/src/plugins/Mutes/functions/muteUser.ts @@ -9,7 +9,7 @@ import { Case } from "../../../data/entities/Case"; import { Mute } from "../../../data/entities/Mute"; import { registerExpiringMute } from "../../../data/loops/expiringMutesLoop"; import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin"; -import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; +import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { UserNotificationMethod, UserNotificationResult, @@ -61,9 +61,10 @@ export async function muteUser( const member = await resolveMember(pluginData.client, pluginData.guild, user.id, true); // Grab the fresh member so we don't have stale role info const config = await pluginData.config.getMatchingConfig({ member, userId }); + const logs = pluginData.getPlugin(LogsPlugin); + let rolesToRestore: string[] = []; if (member) { - const logs = pluginData.getPlugin(LogsPlugin); // remove and store any roles to be removed/restored const currentUserRoles = [...member.roles.cache.keys()]; let newRoles: string[] = currentUserRoles; @@ -187,19 +188,31 @@ export async function muteUser( ? config.timed_mute_message : config.mute_message; - const muteMessage = - template && - (await renderTemplate( - template, - new TemplateSafeValueContainer({ - guildName: pluginData.guild.name, - reason: reason || "None", - time: timeUntilUnmuteStr, - moderator: muteOptions.caseArgs?.modId - ? userToTemplateSafeUser(await resolveUser(pluginData.client, muteOptions.caseArgs.modId)) - : null, - }), - )); + let muteMessage: string | null = null; + try { + muteMessage = + template && + (await renderTemplate( + template, + new TemplateSafeValueContainer({ + guildName: pluginData.guild.name, + reason: reason || "None", + time: timeUntilUnmuteStr, + moderator: muteOptions.caseArgs?.modId + ? userToTemplateSafeUser(await resolveUser(pluginData.client, muteOptions.caseArgs.modId)) + : null, + }), + )); + } catch (err) { + if (err instanceof TemplateParseError) { + logs.logBotAlert({ + body: `Invalid mute message format. The mute was still applied: ${err.message}`, + }); + } else { + lock.unlock(); + throw err; + } + } if (muteMessage && member) { let contactMethods: UserNotificationMethod[] = [];