3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-07-10 12:37:19 +00:00

Merge remote-tracking branch 'drag/master' into fix-server-info

Signed-off-by: GitHub <noreply@github.com>
This commit is contained in:
Tiago R 2023-04-09 17:54:06 +00:00 committed by GitHub
commit d24dd61f32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
495 changed files with 21282 additions and 7526 deletions

View file

@ -16,7 +16,8 @@
"roflmaoqwerty", "roflmaoqwerty",
"thewilloftheshadow", "thewilloftheshadow",
"usoka", "usoka",
"vcokltfre" "vcokltfre",
"Dragory"
], ],
"message": "Thank you for contributing to Zeppelin! We require contributors to sign our Contributor License Agreement (CLA). To let us review and merge your code, please visit https://github.com/ZeppelinBot/CLA to sign the CLA!" "message": "Thank you for contributing to Zeppelin! We require contributors to sign our Contributor License Agreement (CLA). To let us review and merge your code, please visit https://github.com/ZeppelinBot/CLA to sign the CLA!"
} }

3
.gitignore vendored
View file

@ -6,6 +6,9 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.clinic
.clinic-bot
.clinic-api
# Runtime data # Runtime data
pids pids

16425
backend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,10 +8,14 @@
"watch-yaml-parse-test": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node dist/backend/src/yamlParseTest.js\"", "watch-yaml-parse-test": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node dist/backend/src/yamlParseTest.js\"",
"build": "rimraf dist && tsc", "build": "rimraf dist && tsc",
"start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js", "start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js",
"start-bot-dev-debug": "NODE_ENV=development clinic heapprofiler --collect-only --dest .clinic-bot -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js",
"start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js", "start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js",
"start-bot-prod-debug": "NODE_ENV=production clinic heapprofiler --collect-only --dest .clinic-bot -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js",
"watch-bot": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-bot-dev\"", "watch-bot": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-bot-dev\"",
"start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js", "start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
"start-api-dev-debug": "NODE_ENV=development clinic heapprofiler --collect-only --dest .clinic-api -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
"start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js", "start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js",
"start-api-prod-debug": "NODE_ENV=production clinic heapprofiler --collect-only --dest .clinic-api -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js",
"watch-api": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-api-dev\"", "watch-api": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-api-dev\"",
"typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js", "typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js",
"migrate-prod": "cross-env NODE_ENV=production npm run typeorm -- migration:run", "migrate-prod": "cross-env NODE_ENV=production npm run typeorm -- migration:run",
@ -25,11 +29,11 @@
"dependencies": { "dependencies": {
"@silvia-odwyer/photon-node": "^0.3.1", "@silvia-odwyer/photon-node": "^0.3.1",
"bufferutil": "^4.0.3", "bufferutil": "^4.0.3",
"clinic": "^12.1.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"deep-diff": "^1.0.2", "deep-diff": "^1.0.2",
"discord-api-types": "^0.33.1", "discord.js": "^14.9.0",
"discord.js": "13.8",
"dotenv": "^4.0.0", "dotenv": "^4.0.0",
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
"erlpack": "github:discord/erlpack", "erlpack": "github:discord/erlpack",
@ -39,7 +43,7 @@
"humanize-duration": "^3.15.0", "humanize-duration": "^3.15.0",
"io-ts": "^2.0.0", "io-ts": "^2.0.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"knub": "^30.0.0-beta.46", "knub": "^32.0.0-next.10",
"knub-command-manager": "^9.1.0", "knub-command-manager": "^9.1.0",
"last-commit-log": "^2.1.0", "last-commit-log": "^2.1.0",
"lodash.chunk": "^4.2.0", "lodash.chunk": "^4.2.0",
@ -68,6 +72,7 @@
"tsconfig-paths": "^3.9.0", "tsconfig-paths": "^3.9.0",
"twemoji": "^12.1.4", "twemoji": "^12.1.4",
"typeorm": "^0.2.31", "typeorm": "^0.2.31",
"typescript": "~4.9.5",
"utf-8-validate": "^5.0.5", "utf-8-validate": "^5.0.5",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build", "yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build",

View file

@ -8,8 +8,8 @@ import { ApiLogins } from "../data/ApiLogins";
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments"; import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
import { ApiUserInfo } from "../data/ApiUserInfo"; import { ApiUserInfo } from "../data/ApiUserInfo";
import { ApiUserInfoData } from "../data/entities/ApiUserInfo"; import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
import { ok } from "./responses";
import { env } from "../env"; import { env } from "../env";
import { ok } from "./responses";
interface IPassportApiUser { interface IPassportApiUser {
apiKey: string; apiKey: string;
@ -56,7 +56,7 @@ export function initAuth(app: express.Express) {
app.use(passport.initialize()); app.use(passport.initialize());
passport.serializeUser((user, done) => done(null, user)); passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user)); passport.deserializeUser((user, done) => done(null, user as IPassportApiUser));
const apiLogins = new ApiLogins(); const apiLogins = new ApiLogins();
const apiUserInfo = new ApiUserInfo(); const apiUserInfo = new ApiUserInfo();

View file

@ -54,9 +54,10 @@ export function initDocs(app: express.Express) {
} }
const name = plugin.name; const name = plugin.name;
const info = plugin.info || {}; const info = { ...(plugin.info || {}) };
delete info.configSchema;
const commands = (plugin.commands || []).map((cmd) => ({ const messageCommands = (plugin.messageCommands || []).map((cmd) => ({
trigger: cmd.trigger, trigger: cmd.trigger,
permission: cmd.permission, permission: cmd.permission,
signature: cmd.signature, signature: cmd.signature,
@ -66,14 +67,14 @@ export function initDocs(app: express.Express) {
})); }));
const defaultOptions = plugin.defaultOptions || {}; const defaultOptions = plugin.defaultOptions || {};
const configSchema = plugin.configSchema && formatConfigSchema(plugin.configSchema); const configSchema = plugin.info?.configSchema && formatConfigSchema(plugin.info.configSchema);
res.json({ res.json({
name, name,
info, info,
configSchema, configSchema,
defaultOptions, defaultOptions,
commands, messageCommands,
}); });
}); });
} }

View file

@ -1,20 +1,20 @@
import { ApiPermissions } from "@shared/apiPermissions"; import { ApiPermissions } from "@shared/apiPermissions";
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import { YAMLException } from "js-yaml"; import { YAMLException } from "js-yaml";
import moment from "moment-timezone";
import { validateGuildConfig } from "../configValidator"; import { validateGuildConfig } from "../configValidator";
import { AllowedGuilds } from "../data/AllowedGuilds"; import { AllowedGuilds } from "../data/AllowedGuilds";
import { ApiAuditLog } from "../data/ApiAuditLog";
import { AuditLogEventTypes } from "../data/apiAuditLogTypes";
import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments"; import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments";
import { Configs } from "../data/Configs"; import { Configs } from "../data/Configs";
import { Queue } from "../Queue";
import { isSnowflake } from "../utils";
import { loadYamlSafely } from "../utils/loadYamlSafely";
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
import { apiTokenAuthHandlers } from "./auth"; import { apiTokenAuthHandlers } from "./auth";
import { hasGuildPermission, requireGuildPermission } from "./permissions"; import { hasGuildPermission, requireGuildPermission } from "./permissions";
import { clientError, ok, serverError, unauthorized } from "./responses"; import { clientError, ok, serverError, unauthorized } from "./responses";
import { loadYamlSafely } from "../utils/loadYamlSafely";
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
import { isSnowflake } from "../utils";
import moment from "moment-timezone";
import { ApiAuditLog } from "../data/ApiAuditLog";
import { AuditLogEventTypes } from "../data/apiAuditLogTypes";
import { Queue } from "../Queue";
const apiPermissionAssignments = new ApiPermissionAssignments(); const apiPermissionAssignments = new ApiPermissionAssignments();
const auditLog = new ApiAuditLog(); const auditLog = new ApiAuditLog();

View file

@ -1,13 +1,13 @@
import { ApiPermissions } from "@shared/apiPermissions"; import { ApiPermissions } from "@shared/apiPermissions";
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import { requireGuildPermission } from "../permissions"; import moment from "moment-timezone";
import { clientError, ok } from "../responses";
import { GuildCases } from "../../data/GuildCases";
import { z } from "zod"; import { z } from "zod";
import { Case } from "../../data/entities/Case"; import { Case } from "../../data/entities/Case";
import { rateLimit } from "../rateLimits"; import { GuildCases } from "../../data/GuildCases";
import { MINUTES } from "../../utils"; import { MINUTES } from "../../utils";
import moment from "moment-timezone"; import { requireGuildPermission } from "../permissions";
import { rateLimit } from "../rateLimits";
import { clientError, ok } from "../responses";
const caseHandlingModeSchema = z.union([ const caseHandlingModeSchema = z.union([
z.literal("replace"), z.literal("replace"),

View file

@ -1,7 +1,7 @@
import express from "express"; import express from "express";
import { apiTokenAuthHandlers } from "../auth"; import { apiTokenAuthHandlers } from "../auth";
import { initGuildsMiscAPI } from "./misc";
import { initGuildsImportExportAPI } from "./importExport"; import { initGuildsImportExportAPI } from "./importExport";
import { initGuildsMiscAPI } from "./misc";
export function initGuildsAPI(app: express.Express) { export function initGuildsAPI(app: express.Express) {
const guildRouter = express.Router(); const guildRouter = express.Router();

View file

@ -1,22 +1,19 @@
import { ApiPermissions } from "@shared/apiPermissions"; import { ApiPermissions } from "@shared/apiPermissions";
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import { YAMLException } from "js-yaml"; import { YAMLException } from "js-yaml";
import moment from "moment-timezone";
import { validateGuildConfig } from "../../configValidator"; import { validateGuildConfig } from "../../configValidator";
import { AllowedGuilds } from "../../data/AllowedGuilds"; import { AllowedGuilds } from "../../data/AllowedGuilds";
import { ApiPermissionAssignments, ApiPermissionTypes } from "../../data/ApiPermissionAssignments";
import { Configs } from "../../data/Configs";
import { apiTokenAuthHandlers } from "../auth";
import { hasGuildPermission, requireGuildPermission } from "../permissions";
import { clientError, ok, serverError, unauthorized } from "../responses";
import { loadYamlSafely } from "../../utils/loadYamlSafely";
import { ObjectAliasError } from "../../utils/validateNoObjectAliases";
import { isSnowflake } from "../../utils";
import moment from "moment-timezone";
import { ApiAuditLog } from "../../data/ApiAuditLog"; import { ApiAuditLog } from "../../data/ApiAuditLog";
import { AuditLogEventTypes } from "../../data/apiAuditLogTypes"; import { AuditLogEventTypes } from "../../data/apiAuditLogTypes";
import { ApiPermissionAssignments, ApiPermissionTypes } from "../../data/ApiPermissionAssignments";
import { Configs } from "../../data/Configs";
import { Queue } from "../../Queue"; import { Queue } from "../../Queue";
import { GuildCases } from "../../data/GuildCases"; import { isSnowflake } from "../../utils";
import { z } from "zod"; import { loadYamlSafely } from "../../utils/loadYamlSafely";
import { ObjectAliasError } from "../../utils/validateNoObjectAliases";
import { hasGuildPermission, requireGuildPermission } from "../permissions";
import { clientError, ok, serverError, unauthorized } from "../responses";
const apiPermissionAssignments = new ApiPermissionAssignments(); const apiPermissionAssignments = new ApiPermissionAssignments();
const auditLog = new ApiAuditLog(); const auditLog = new ApiAuditLog();

View file

@ -1,6 +1,6 @@
import { connect } from "../data/db"; import { connect } from "../data/db";
import { setIsAPI } from "../globals";
import { env } from "../env"; import { env } from "../env";
import { setIsAPI } from "../globals";
if (!env.KEY) { if (!env.KEY) {
// tslint:disable-next-line:no-console // tslint:disable-next-line:no-console
@ -20,5 +20,5 @@ setIsAPI(true);
// Connect to the database before loading the rest of the code (that depend on the database connection) // Connect to the database before loading the rest of the code (that depend on the database connection)
console.log("Connecting to database..."); // tslint:disable-line console.log("Connecting to database..."); // tslint:disable-line
connect().then(() => { connect().then(() => {
import("./start"); import("./start.js");
}); });

View file

@ -1,14 +1,14 @@
import cors from "cors"; import cors from "cors";
import express from "express"; import express from "express";
import multer from "multer";
import { TokenError } from "passport-oauth2"; import { TokenError } from "passport-oauth2";
import { env } from "../env";
import { initArchives } from "./archives"; import { initArchives } from "./archives";
import { initAuth } from "./auth"; import { initAuth } from "./auth";
import { initDocs } from "./docs"; import { initDocs } from "./docs";
import { initGuildsAPI } from "./guilds/index"; import { initGuildsAPI } from "./guilds/index";
import { clientError, error, notFound } from "./responses"; import { clientError, error, notFound } from "./responses";
import { startBackgroundTasks } from "./tasks"; import { startBackgroundTasks } from "./tasks";
import multer from "multer";
import { env } from "../env";
const app = express(); const app = express();

View file

@ -1,5 +1,18 @@
import { GuildChannel, GuildMember, Snowflake, Util, User, GuildTextBasedChannel } from "discord.js"; import {
import { baseCommandParameterTypeHelpers, baseTypeConverters, CommandContext, TypeConversionError } from "knub"; escapeCodeBlock,
escapeInlineCode,
GuildChannel,
GuildMember,
GuildTextBasedChannel,
Snowflake,
User,
} from "discord.js";
import {
baseCommandParameterTypeHelpers,
CommandContext,
messageCommandBaseTypeConverters,
TypeConversionError,
} from "knub";
import { createTypeHelper } from "knub-command-manager"; import { createTypeHelper } from "knub-command-manager";
import { import {
channelMentionRegex, channelMentionRegex,
@ -14,11 +27,9 @@ import {
import { isValidTimezone } from "./utils/isValidTimezone"; import { isValidTimezone } from "./utils/isValidTimezone";
import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarget"; import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarget";
import { inputPatternToRegExp } from "./validatorUtils"; import { inputPatternToRegExp } from "./validatorUtils";
import { getChannelId } from "knub/dist/utils";
import { disableCodeBlocks } from "knub/dist/helpers";
export const commandTypes = { export const commandTypes = {
...baseTypeConverters, ...messageCommandBaseTypeConverters,
delay(value) { delay(value) {
const result = convertDelayStringToMS(value); const result = convertDelayStringToMS(value);
@ -32,7 +43,7 @@ export const commandTypes = {
async resolvedUser(value, context: CommandContext<any>) { async resolvedUser(value, context: CommandContext<any>) {
const result = await resolveUser(context.pluginData.client, value); const result = await resolveUser(context.pluginData.client, value);
if (result == null || result instanceof UnknownUser) { if (result == null || result instanceof UnknownUser) {
throw new TypeConversionError(`User \`${Util.escapeCodeBlock(value)}\` was not found`); throw new TypeConversionError(`User \`${escapeCodeBlock(value)}\` was not found`);
} }
return result; return result;
}, },
@ -40,7 +51,7 @@ export const commandTypes = {
async resolvedUserLoose(value, context: CommandContext<any>) { async resolvedUserLoose(value, context: CommandContext<any>) {
const result = await resolveUser(context.pluginData.client, value); const result = await resolveUser(context.pluginData.client, value);
if (result == null) { if (result == null) {
throw new TypeConversionError(`Invalid user: \`${Util.escapeCodeBlock(value)}\``); throw new TypeConversionError(`Invalid user: \`${escapeCodeBlock(value)}\``);
} }
return result; return result;
}, },
@ -52,9 +63,7 @@ export const commandTypes = {
const result = await resolveMember(context.pluginData.client, context.message.channel.guild, value); const result = await resolveMember(context.pluginData.client, context.message.channel.guild, value);
if (result == null) { if (result == null) {
throw new TypeConversionError( throw new TypeConversionError(`Member \`${escapeCodeBlock(value)}\` was not found or they have left the server`);
`Member \`${Util.escapeCodeBlock(value)}\` was not found or they have left the server`,
);
} }
return result; return result;
}, },
@ -64,7 +73,7 @@ export const commandTypes = {
const result = await resolveMessageTarget(context.pluginData, value); const result = await resolveMessageTarget(context.pluginData, value);
if (!result) { if (!result) {
throw new TypeConversionError(`Unknown message \`${Util.escapeInlineCode(value)}\``); throw new TypeConversionError(`Unknown message \`${escapeInlineCode(value)}\``);
} }
return result; return result;
@ -84,28 +93,27 @@ export const commandTypes = {
return value as Snowflake; return value as Snowflake;
} }
throw new TypeConversionError(`Could not parse ID: \`${Util.escapeInlineCode(value)}\``); throw new TypeConversionError(`Could not parse ID: \`${escapeInlineCode(value)}\``);
}, },
regex(value: string, context: CommandContext<any>): RegExp { regex(value: string, context: CommandContext<any>): RegExp {
try { try {
return inputPatternToRegExp(value); return inputPatternToRegExp(value);
} catch (e) { } catch (e) {
throw new TypeConversionError(`Could not parse RegExp: \`${Util.escapeInlineCode(e.message)}\``); throw new TypeConversionError(`Could not parse RegExp: \`${escapeInlineCode(e.message)}\``);
} }
}, },
timezone(value: string) { timezone(value: string) {
if (!isValidTimezone(value)) { if (!isValidTimezone(value)) {
throw new TypeConversionError(`Invalid timezone: ${Util.escapeInlineCode(value)}`); throw new TypeConversionError(`Invalid timezone: ${escapeInlineCode(value)}`);
} }
return value; return value;
}, },
guildTextBasedChannel(value: string, context: CommandContext<any>) { guildTextBasedChannel(value: string, context: CommandContext<any>) {
// FIXME: Remove once Knub's types have been fixed return messageCommandBaseTypeConverters.textChannel(value, context);
return baseTypeConverters.textChannel(value, context) as GuildTextBasedChannel;
}, },
}; };

View file

@ -1,4 +1,4 @@
import { configUtils, ConfigValidationError, PluginOptions } from "knub"; import { ConfigValidationError, PluginConfigManager } from "knub";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { guildPlugins } from "./plugins/availablePlugins"; import { guildPlugins } from "./plugins/availablePlugins";
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin"; import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
@ -34,9 +34,12 @@ export async function validateGuildConfig(config: any): Promise<string | null> {
} }
const plugin = pluginNameToPlugin.get(pluginName)!; const plugin = pluginNameToPlugin.get(pluginName)!;
const configManager = new PluginConfigManager(plugin.defaultOptions || { config: {} }, pluginOptions, {
levels: {},
parser: plugin.configParser,
});
try { try {
const mergedOptions = configUtils.mergeConfig(plugin.defaultOptions || {}, pluginOptions); await configManager.init();
await plugin.configPreprocessor?.(mergedOptions as unknown as PluginOptions<any>, true);
} catch (err) { } catch (err) {
if (err instanceof ConfigValidationError || err instanceof StrictValidationError) { if (err instanceof ConfigValidationError || err instanceof StrictValidationError) {
return `${pluginName}: ${err.message}`; return `${pluginName}: ${err.message}`;

View file

@ -1,10 +1,9 @@
import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm"; import { getRepository, Repository } from "typeorm";
import { DBDateFormat } from "../utils";
import { ApiPermissionTypes } from "./ApiPermissionAssignments"; import { ApiPermissionTypes } from "./ApiPermissionAssignments";
import { BaseRepository } from "./BaseRepository"; import { BaseRepository } from "./BaseRepository";
import { AllowedGuild } from "./entities/AllowedGuild"; import { AllowedGuild } from "./entities/AllowedGuild";
import moment from "moment-timezone";
import { DBDateFormat } from "../utils";
import { env } from "../env";
export class AllowedGuilds extends BaseRepository { export class AllowedGuilds extends BaseRepository {
private allowedGuilds: Repository<AllowedGuild>; private allowedGuilds: Repository<AllowedGuild>;

View file

@ -1,8 +1,7 @@
import { BaseRepository } from "./BaseRepository";
import { getRepository, Repository } from "typeorm/index"; import { getRepository, Repository } from "typeorm/index";
import { ApiAuditLogEntry } from "./entities/ApiAuditLogEntry";
import { ApiLogin } from "./entities/ApiLogin";
import { AuditLogEventData, AuditLogEventType } from "./apiAuditLogTypes"; import { AuditLogEventData, AuditLogEventType } from "./apiAuditLogTypes";
import { BaseRepository } from "./BaseRepository";
import { ApiAuditLogEntry } from "./entities/ApiAuditLogEntry";
export class ApiAuditLog extends BaseRepository { export class ApiAuditLog extends BaseRepository {
private auditLog: Repository<ApiAuditLogEntry<any>>; private auditLog: Repository<ApiAuditLogEntry<any>>;

View file

@ -1,10 +1,9 @@
import { ApiPermissions } from "@shared/apiPermissions"; import { ApiPermissions } from "@shared/apiPermissions";
import { getRepository, Repository } from "typeorm"; import { getRepository, Repository } from "typeorm";
import { BaseRepository } from "./BaseRepository";
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
import { Permissions } from "discord.js";
import { ApiAuditLog } from "./ApiAuditLog"; import { ApiAuditLog } from "./ApiAuditLog";
import { AuditLogEventTypes } from "./apiAuditLogTypes"; import { AuditLogEventTypes } from "./apiAuditLogTypes";
import { BaseRepository } from "./BaseRepository";
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
export enum ApiPermissionTypes { export enum ApiPermissionTypes {
User = "USER", User = "USER",

View file

@ -1,6 +1,6 @@
import { getRepository, Repository } from "typeorm"; import { getRepository, Repository } from "typeorm";
import { ArchiveEntry } from "./entities/ArchiveEntry";
import { BaseRepository } from "./BaseRepository"; import { BaseRepository } from "./BaseRepository";
import { ArchiveEntry } from "./entities/ArchiveEntry";
export class Archives extends BaseRepository { export class Archives extends BaseRepository {
protected archives: Repository<ArchiveEntry>; protected archives: Repository<ArchiveEntry>;

View file

@ -1,18 +1,14 @@
import { Guild, Snowflake, User } from "discord.js"; import { Guild, Snowflake } from "discord.js";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { isDefaultSticker } from "src/utils/isDefaultSticker"; import { isDefaultSticker } from "src/utils/isDefaultSticker";
import { getRepository, Repository } from "typeorm"; import { getRepository, Repository } from "typeorm";
import { renderTemplate, TemplateSafeValueContainer } from "../templateFormatter"; import { renderTemplate, TemplateSafeValueContainer } from "../templateFormatter";
import { trimLines } from "../utils"; import { trimLines } from "../utils";
import { decrypt, encrypt } from "../utils/crypt";
import { channelToTemplateSafeChannel, guildToTemplateSafeGuild } from "../utils/templateSafeObjects";
import { BaseGuildRepository } from "./BaseGuildRepository"; import { BaseGuildRepository } from "./BaseGuildRepository";
import { ArchiveEntry } from "./entities/ArchiveEntry"; import { ArchiveEntry } from "./entities/ArchiveEntry";
import {
channelToTemplateSafeChannel,
guildToTemplateSafeGuild,
userToTemplateSafeUser,
} from "../utils/templateSafeObjects";
import { SavedMessage } from "./entities/SavedMessage"; import { SavedMessage } from "./entities/SavedMessage";
import { decrypt, encrypt } from "../utils/crypt";
const DEFAULT_EXPIRY_DAYS = 30; const DEFAULT_EXPIRY_DAYS = 30;

View file

@ -1,12 +1,11 @@
import { getRepository, In, InsertResult, Repository } from "typeorm"; import { getRepository, In, InsertResult, Repository } from "typeorm";
import { Queue } from "../Queue";
import { chunkArray } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository"; import { BaseGuildRepository } from "./BaseGuildRepository";
import { CaseTypes } from "./CaseTypes"; import { CaseTypes } from "./CaseTypes";
import { connection } from "./db"; import { connection } from "./db";
import { Case } from "./entities/Case"; import { Case } from "./entities/Case";
import { CaseNote } from "./entities/CaseNote"; import { CaseNote } from "./entities/CaseNote";
import moment from "moment-timezone";
import { chunkArray } from "../utils";
import { Queue } from "../Queue";
const CASE_SUMMARY_REASON_MAX_LENGTH = 300; const CASE_SUMMARY_REASON_MAX_LENGTH = 300;

View file

@ -1,11 +1,12 @@
import { Mute } from "./entities/Mute"; import { Mute } from "./entities/Mute";
import { ScheduledPost } from "./entities/ScheduledPost";
import { Reminder } from "./entities/Reminder"; import { Reminder } from "./entities/Reminder";
import { ScheduledPost } from "./entities/ScheduledPost";
import { Tempban } from "./entities/Tempban"; import { Tempban } from "./entities/Tempban";
import { VCAlert } from "./entities/VCAlert"; import { VCAlert } from "./entities/VCAlert";
interface GuildEventArgs extends Record<string, unknown[]> { interface GuildEventArgs extends Record<string, unknown[]> {
expiredMute: [Mute]; expiredMute: [Mute];
timeoutMuteToRenew: [Mute];
scheduledPost: [ScheduledPost]; scheduledPost: [ScheduledPost];
reminder: [Reminder]; reminder: [Reminder];
expiredTempban: [Tempban]; expiredTempban: [Tempban];

View file

@ -1,7 +1,18 @@
import moment from "moment-timezone"; import moment from "moment-timezone";
import { Brackets, getRepository, Repository } from "typeorm"; import { Brackets, getRepository, Repository } from "typeorm";
import { DBDateFormat } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository"; import { BaseGuildRepository } from "./BaseGuildRepository";
import { Mute } from "./entities/Mute"; import { Mute } from "./entities/Mute";
import { MuteTypes } from "./MuteTypes";
export type AddMuteParams = {
userId: Mute["user_id"];
type: MuteTypes;
expiresAt: number | null;
rolesToRestore?: Mute["roles_to_restore"];
muteRole?: string | null;
timeoutExpiresAt?: number;
};
export class GuildMutes extends BaseGuildRepository { export class GuildMutes extends BaseGuildRepository {
private mutes: Repository<Mute>; private mutes: Repository<Mute>;
@ -34,14 +45,18 @@ export class GuildMutes extends BaseGuildRepository {
return mute != null; return mute != null;
} }
async addMute(userId, expiryTime, rolesToRestore?: string[]): Promise<Mute> { async addMute(params: AddMuteParams): Promise<Mute> {
const expiresAt = expiryTime ? moment.utc().add(expiryTime, "ms").format("YYYY-MM-DD HH:mm:ss") : null; const expiresAt = params.expiresAt ? moment.utc(params.expiresAt).format(DBDateFormat) : null;
const timeoutExpiresAt = params.timeoutExpiresAt ? moment.utc(params.timeoutExpiresAt).format(DBDateFormat) : null;
const result = await this.mutes.insert({ const result = await this.mutes.insert({
guild_id: this.guildId, guild_id: this.guildId,
user_id: userId, user_id: params.userId,
type: params.type,
expires_at: expiresAt, expires_at: expiresAt,
roles_to_restore: rolesToRestore ?? [], roles_to_restore: params.rolesToRestore ?? [],
mute_role: params.muteRole,
timeout_expires_at: timeoutExpiresAt,
}); });
return (await this.mutes.findOne({ where: result.identifiers[0] }))!; return (await this.mutes.findOne({ where: result.identifiers[0] }))!;
@ -74,6 +89,32 @@ export class GuildMutes extends BaseGuildRepository {
} }
} }
async updateExpiresAt(userId: string, timestamp: number | null): Promise<void> {
const expiresAt = timestamp ? moment.utc(timestamp).format("YYYY-MM-DD HH:mm:ss") : null;
await this.mutes.update(
{
guild_id: this.guildId,
user_id: userId,
},
{
expires_at: expiresAt,
},
);
}
async updateTimeoutExpiresAt(userId: string, timestamp: number): Promise<void> {
const timeoutExpiresAt = moment.utc(timestamp).format(DBDateFormat);
await this.mutes.update(
{
guild_id: this.guildId,
user_id: userId,
},
{
timeout_expires_at: timeoutExpiresAt,
},
);
}
async getActiveMutes(): Promise<Mute[]> { async getActiveMutes(): Promise<Mute[]> {
return this.mutes return this.mutes
.createQueryBuilder("mutes") .createQueryBuilder("mutes")
@ -104,4 +145,16 @@ export class GuildMutes extends BaseGuildRepository {
user_id: userId, user_id: userId,
}); });
} }
async fillMissingMuteRole(muteRole: string): Promise<void> {
await this.mutes
.createQueryBuilder()
.where("guild_id = :guild_id", { guild_id: this.guildId })
.andWhere("type = :type", { type: MuteTypes.Role })
.andWhere("mute_role IS NULL")
.update({
mute_role: muteRole,
})
.execute();
}
} }

View file

@ -1,11 +1,5 @@
import { getRepository, Repository } from "typeorm"; import { getRepository, Repository } from "typeorm";
import { Reminder } from "./entities/Reminder";
import { BaseRepository } from "./BaseRepository";
import moment from "moment-timezone";
import { DBDateFormat } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository"; import { BaseGuildRepository } from "./BaseGuildRepository";
import { RoleQueueItem } from "./entities/RoleQueueItem";
import { connection } from "./db";
import { RoleButtonsItem } from "./entities/RoleButtonsItem"; import { RoleButtonsItem } from "./entities/RoleButtonsItem";
export class GuildRoleButtons extends BaseGuildRepository { export class GuildRoleButtons extends BaseGuildRepository {

View file

@ -1,11 +1,7 @@
import { getRepository, Repository } from "typeorm"; import { getRepository, Repository } from "typeorm";
import { Reminder } from "./entities/Reminder";
import { BaseRepository } from "./BaseRepository";
import moment from "moment-timezone";
import { DBDateFormat } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository"; import { BaseGuildRepository } from "./BaseGuildRepository";
import { RoleQueueItem } from "./entities/RoleQueueItem";
import { connection } from "./db"; import { connection } from "./db";
import { RoleQueueItem } from "./entities/RoleQueueItem";
export class GuildRoleQueue extends BaseGuildRepository { export class GuildRoleQueue extends BaseGuildRepository {
private roleQueue: Repository<RoleQueueItem>; private roleQueue: Repository<RoleQueueItem>;

View file

@ -1,15 +1,13 @@
import { GuildChannel, Message } from "discord.js"; import { GuildChannel, Message } from "discord.js";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm"; import { getRepository, Repository } from "typeorm";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { QueuedEventEmitter } from "../QueuedEventEmitter"; import { QueuedEventEmitter } from "../QueuedEventEmitter";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
import { buildEntity } from "./buildEntity";
import { noop } from "../utils"; import { noop } from "../utils";
import { decrypt } from "../utils/crypt";
import { decryptJson, encryptJson } from "../utils/cryptHelpers";
import { asyncMap } from "../utils/async"; import { asyncMap } from "../utils/async";
import { decryptJson, encryptJson } from "../utils/cryptHelpers";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { buildEntity } from "./buildEntity";
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> { export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
private messages: Repository<SavedMessage>; private messages: Repository<SavedMessage>;
@ -53,13 +51,13 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
title: embed.title, title: embed.title,
description: embed.description, description: embed.description,
url: embed.url, url: embed.url,
timestamp: embed.timestamp, timestamp: embed.timestamp ? Date.parse(embed.timestamp) : null,
color: embed.color, color: embed.color,
fields: embed.fields.map((field) => ({ fields: embed.fields.map((field) => ({
name: field.name, name: field.name,
value: field.value, value: field.value,
inline: field.inline, inline: field.inline ?? false,
})), })),
author: embed.author author: embed.author

View file

@ -0,0 +1,4 @@
export enum MuteTypes {
Role = 1,
Timeout = 2,
}

View file

@ -1,11 +1,16 @@
import moment from "moment-timezone"; import moment from "moment-timezone";
import { Brackets, getRepository, Repository } from "typeorm"; import { getRepository, Repository } from "typeorm";
import { Mute } from "./entities/Mute";
import { DAYS, DBDateFormat } from "../utils"; import { DAYS, DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository"; import { BaseRepository } from "./BaseRepository";
import { Mute } from "./entities/Mute";
import { MuteTypes } from "./MuteTypes";
const OLD_EXPIRED_MUTE_THRESHOLD = 7 * DAYS; const OLD_EXPIRED_MUTE_THRESHOLD = 7 * DAYS;
export const MAX_TIMEOUT_DURATION = 28 * DAYS;
// When a timeout is under this duration but the mute expires later, the timeout will be reset to max duration
export const TIMEOUT_RENEWAL_THRESHOLD = 21 * DAYS;
export class Mutes extends BaseRepository { export class Mutes extends BaseRepository {
private mutes: Repository<Mute>; private mutes: Repository<Mute>;
@ -14,7 +19,16 @@ export class Mutes extends BaseRepository {
this.mutes = getRepository(Mute); this.mutes = getRepository(Mute);
} }
async getSoonExpiringMutes(threshold: number): Promise<Mute[]> { findMute(guildId: string, userId: string): Promise<Mute | undefined> {
return this.mutes.findOne({
where: {
guild_id: guildId,
user_id: userId,
},
});
}
getSoonExpiringMutes(threshold: number): Promise<Mute[]> {
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat); const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
return this.mutes return this.mutes
.createQueryBuilder("mutes") .createQueryBuilder("mutes")
@ -23,6 +37,16 @@ export class Mutes extends BaseRepository {
.getMany(); .getMany();
} }
getTimeoutMutesToRenew(threshold: number): Promise<Mute[]> {
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
return this.mutes
.createQueryBuilder("mutes")
.andWhere("type = :type", { type: MuteTypes.Timeout })
.andWhere("(expires_at IS NULL OR timeout_expires_at < expires_at)")
.andWhere("timeout_expires_at <= :date", { date: thresholdDateStr })
.getMany();
}
async clearOldExpiredMutes(): Promise<void> { async clearOldExpiredMutes(): Promise<void> {
const thresholdDateStr = moment.utc().subtract(OLD_EXPIRED_MUTE_THRESHOLD, "ms").format(DBDateFormat); const thresholdDateStr = moment.utc().subtract(OLD_EXPIRED_MUTE_THRESHOLD, "ms").format(DBDateFormat);
await this.mutes await this.mutes

View file

@ -1,12 +1,12 @@
import { getRepository, Repository } from "typeorm";
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry";
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman";
import fetch, { Headers } from "node-fetch";
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
import moment from "moment-timezone";
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
import crypto from "crypto"; import crypto from "crypto";
import moment from "moment-timezone";
import fetch, { Headers } from "node-fetch";
import { getRepository, Repository } from "typeorm";
import { env } from "../env"; import { env } from "../env";
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry";
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman";
const API_URL = "https://api.phisherman.gg"; const API_URL = "https://api.phisherman.gg";
const MASTER_API_KEY = env.PHISHERMAN_API_KEY; const MASTER_API_KEY = env.PHISHERMAN_API_KEY;

View file

@ -1,8 +1,8 @@
import { getRepository, Repository } from "typeorm";
import { Reminder } from "./entities/Reminder";
import { BaseRepository } from "./BaseRepository";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm";
import { DBDateFormat } from "../utils"; import { DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { Reminder } from "./entities/Reminder";
export class Reminders extends BaseRepository { export class Reminders extends BaseRepository {
private reminders: Repository<Reminder>; private reminders: Repository<Reminder>;

View file

@ -1,8 +1,8 @@
import { getRepository, Repository } from "typeorm";
import { ScheduledPost } from "./entities/ScheduledPost";
import { BaseRepository } from "./BaseRepository";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm";
import { DBDateFormat } from "../utils"; import { DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { ScheduledPost } from "./entities/ScheduledPost";
export class ScheduledPosts extends BaseRepository { export class ScheduledPosts extends BaseRepository {
private scheduledPosts: Repository<ScheduledPost>; private scheduledPosts: Repository<ScheduledPost>;

View file

@ -1,8 +1,8 @@
import moment from "moment-timezone"; import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm"; import { getRepository, Repository } from "typeorm";
import { Tempban } from "./entities/Tempban";
import { BaseRepository } from "./BaseRepository";
import { DBDateFormat } from "../utils"; import { DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { Tempban } from "./entities/Tempban";
export class Tempbans extends BaseRepository { export class Tempbans extends BaseRepository {
private tempbans: Repository<Tempban>; private tempbans: Repository<Tempban>;

View file

@ -1,8 +1,8 @@
import { getRepository, Repository } from "typeorm";
import { VCAlert } from "./entities/VCAlert";
import { BaseRepository } from "./BaseRepository";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm";
import { DBDateFormat } from "../utils"; import { DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { VCAlert } from "./entities/VCAlert";
export class VCAlerts extends BaseRepository { export class VCAlerts extends BaseRepository {
private allAlerts: Repository<VCAlert>; private allAlerts: Repository<VCAlert>;

View file

@ -1,7 +1,7 @@
import { getRepository, Repository } from "typeorm"; import { getRepository, Repository } from "typeorm";
import { Webhook } from "./entities/Webhook";
import { BaseRepository } from "./BaseRepository";
import { decrypt, encrypt } from "../utils/crypt"; import { decrypt, encrypt } from "../utils/crypt";
import { BaseRepository } from "./BaseRepository";
import { Webhook } from "./entities/Webhook";
export class Webhooks extends BaseRepository { export class Webhooks extends BaseRepository {
repository: Repository<Webhook> = getRepository(Webhook); repository: Repository<Webhook> = getRepository(Webhook);

View file

@ -1,8 +1,8 @@
import path from "path";
import { Connection, createConnection } from "typeorm"; import { Connection, createConnection } from "typeorm";
import { backendDir } from "../paths";
import { SimpleError } from "../SimpleError"; import { SimpleError } from "../SimpleError";
import { QueryLogger } from "./queryLogger"; import { QueryLogger } from "./queryLogger";
import path from "path";
import { backendDir } from "../paths";
const ormconfigPath = path.join(backendDir, "ormconfig.js"); const ormconfigPath = path.join(backendDir, "ormconfig.js");
const connectionOptions = require(ormconfigPath); const connectionOptions = require(ormconfigPath);

View file

@ -1,5 +1,4 @@
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm"; import { Column, Entity, PrimaryColumn } from "typeorm";
import { ApiUserInfo } from "./ApiUserInfo";
import { AuditLogEventData, AuditLogEventType } from "../apiAuditLogTypes"; import { AuditLogEventData, AuditLogEventType } from "../apiAuditLogTypes";
@Entity("api_audit_log") @Entity("api_audit_log")

View file

@ -1,6 +1,6 @@
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm"; import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
import { ApiUserInfo } from "./ApiUserInfo";
import { ApiPermissionTypes } from "../ApiPermissionAssignments"; import { ApiPermissionTypes } from "../ApiPermissionAssignments";
import { ApiUserInfo } from "./ApiUserInfo";
@Entity("api_permissions") @Entity("api_permissions")
export class ApiPermissionAssignment { export class ApiPermissionAssignment {

View file

@ -2,7 +2,7 @@ import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
export const TRIGGER_COMPARISON_OPS = ["=", "!=", ">", "<", ">=", "<="] as const; export const TRIGGER_COMPARISON_OPS = ["=", "!=", ">", "<", ">=", "<="] as const;
export type TriggerComparisonOp = typeof TRIGGER_COMPARISON_OPS[number]; export type TriggerComparisonOp = (typeof TRIGGER_COMPARISON_OPS)[number];
const REVERSE_OPS: Record<TriggerComparisonOp, TriggerComparisonOp> = { const REVERSE_OPS: Record<TriggerComparisonOp, TriggerComparisonOp> = {
"=": "!=", "=": "!=",

View file

@ -10,6 +10,8 @@ export class Mute {
@PrimaryColumn() @PrimaryColumn()
user_id: string; user_id: string;
@Column() type: number;
@Column() created_at: string; @Column() created_at: string;
@Column({ type: String, nullable: true }) expires_at: string | null; @Column({ type: String, nullable: true }) expires_at: string | null;
@ -17,4 +19,8 @@ export class Mute {
@Column() case_id: number; @Column() case_id: number;
@Column("simple-array") roles_to_restore: string[]; @Column("simple-array") roles_to_restore: string[];
@Column({ type: String, nullable: true }) mute_role: string | null;
@Column({ type: String, nullable: true }) timeout_expires_at: string | null;
} }

View file

@ -1,4 +1,4 @@
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity("reminders") @Entity("reminders")
export class Reminder { export class Reminder {

View file

@ -1,4 +1,4 @@
import { Snowflake } from "discord.js"; import { Snowflake, StickerFormatType, StickerType } from "discord.js";
import { Column, Entity, PrimaryColumn } from "typeorm"; import { Column, Entity, PrimaryColumn } from "typeorm";
export interface ISavedMessageAttachmentData { export interface ISavedMessageAttachmentData {
@ -55,13 +55,13 @@ export interface ISavedMessageEmbedData {
} }
export interface ISavedMessageStickerData { export interface ISavedMessageStickerData {
format: string; format: StickerFormatType;
guildId: Snowflake | null; guildId: Snowflake | null;
id: Snowflake; id: Snowflake;
name: string; name: string;
description: string | null; description: string | null;
available: boolean | null; available: boolean | null;
type: string | null; type: StickerType | null;
} }
export interface ISavedMessageData { export interface ISavedMessageData {

View file

@ -1,5 +1,5 @@
import { MessageAttachment } from "discord.js"; import { Attachment } from "discord.js";
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { StrictMessageContent } from "../../utils"; import { StrictMessageContent } from "../../utils";
@Entity("scheduled_posts") @Entity("scheduled_posts")
@ -17,7 +17,7 @@ export class ScheduledPost {
@Column("simple-json") content: StrictMessageContent; @Column("simple-json") content: StrictMessageContent;
@Column("simple-json") attachments: MessageAttachment[]; @Column("simple-json") attachments: Attachment[];
@Column({ type: String, nullable: true }) post_at: string | null; @Column({ type: String, nullable: true }) post_at: string | null;

View file

@ -1,4 +1,4 @@
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity("vc_alerts") @Entity("vc_alerts")
export class VCAlert { export class VCAlert {

View file

@ -2,7 +2,6 @@
import { lazyMemoize, MINUTES } from "../../utils"; import { lazyMemoize, MINUTES } from "../../utils";
import { Archives } from "../Archives"; import { Archives } from "../Archives";
import moment from "moment-timezone";
const LOOP_INTERVAL = 15 * MINUTES; const LOOP_INTERVAL = 15 * MINUTES;
const getArchivesRepository = lazyMemoize(() => new Archives()); const getArchivesRepository = lazyMemoize(() => new Archives());

View file

@ -1,11 +1,11 @@
// tslint:disable:no-console // tslint:disable:no-console
import { lazyMemoize, memoize, MINUTES } from "../../utils";
import { Mutes } from "../Mutes";
import Timeout = NodeJS.Timeout;
import moment from "moment-timezone"; import moment from "moment-timezone";
import { lazyMemoize, MINUTES, SECONDS } from "../../utils";
import { Mute } from "../entities/Mute"; import { Mute } from "../entities/Mute";
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents"; import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
import { Mutes, TIMEOUT_RENEWAL_THRESHOLD } from "../Mutes";
import Timeout = NodeJS.Timeout;
const LOOP_INTERVAL = 15 * MINUTES; const LOOP_INTERVAL = 15 * MINUTES;
const MAX_TRIES_PER_SERVER = 3; const MAX_TRIES_PER_SERVER = 3;
@ -16,14 +16,24 @@ function muteToKey(mute: Mute) {
return `${mute.guild_id}/${mute.user_id}`; return `${mute.guild_id}/${mute.user_id}`;
} }
function broadcastExpiredMute(mute: Mute, tries = 0) { async function broadcastExpiredMute(guildId: string, userId: string, tries = 0) {
const mute = await getMutesRepository().findMute(guildId, userId);
if (!mute) {
// Mute was already cleared
return;
}
if (!mute.expires_at || moment(mute.expires_at).diff(moment()) > 10 * SECONDS) {
// Mute duration was changed and it's no longer expiring now
return;
}
console.log(`[EXPIRING MUTES LOOP] Broadcasting expired mute: ${mute.guild_id}/${mute.user_id}`); console.log(`[EXPIRING MUTES LOOP] Broadcasting expired mute: ${mute.guild_id}/${mute.user_id}`);
if (!hasGuildEventListener(mute.guild_id, "expiredMute")) { if (!hasGuildEventListener(mute.guild_id, "expiredMute")) {
// If there are no listeners registered for the server yet, try again in a bit // If there are no listeners registered for the server yet, try again in a bit
if (tries < MAX_TRIES_PER_SERVER) { if (tries < MAX_TRIES_PER_SERVER) {
timeouts.set( timeouts.set(
muteToKey(mute), muteToKey(mute),
setTimeout(() => broadcastExpiredMute(mute, tries + 1), 1 * MINUTES), setTimeout(() => broadcastExpiredMute(guildId, userId, tries + 1), 1 * MINUTES),
); );
} }
return; return;
@ -31,6 +41,21 @@ function broadcastExpiredMute(mute: Mute, tries = 0) {
emitGuildEvent(mute.guild_id, "expiredMute", [mute]); emitGuildEvent(mute.guild_id, "expiredMute", [mute]);
} }
function broadcastTimeoutMuteToRenew(mute: Mute, tries = 0) {
console.log(`[EXPIRING MUTES LOOP] Broadcasting timeout mute to renew: ${mute.guild_id}/${mute.user_id}`);
if (!hasGuildEventListener(mute.guild_id, "timeoutMuteToRenew")) {
// If there are no listeners registered for the server yet, try again in a bit
if (tries < MAX_TRIES_PER_SERVER) {
timeouts.set(
muteToKey(mute),
setTimeout(() => broadcastTimeoutMuteToRenew(mute, tries + 1), 1 * MINUTES),
);
}
return;
}
emitGuildEvent(mute.guild_id, "timeoutMuteToRenew", [mute]);
}
export async function runExpiringMutesLoop() { export async function runExpiringMutesLoop() {
console.log("[EXPIRING MUTES LOOP] Clearing old timeouts"); console.log("[EXPIRING MUTES LOOP] Clearing old timeouts");
for (const timeout of timeouts.values()) { for (const timeout of timeouts.values()) {
@ -46,10 +71,16 @@ export async function runExpiringMutesLoop() {
const remaining = Math.max(0, moment.utc(mute.expires_at!).diff(moment.utc())); const remaining = Math.max(0, moment.utc(mute.expires_at!).diff(moment.utc()));
timeouts.set( timeouts.set(
muteToKey(mute), muteToKey(mute),
setTimeout(() => broadcastExpiredMute(mute), remaining), setTimeout(() => broadcastExpiredMute(mute.guild_id, mute.user_id), remaining),
); );
} }
console.log("[EXPIRING MUTES LOOP] Broadcasting timeout mutes to renew");
const timeoutMutesToRenew = await getMutesRepository().getTimeoutMutesToRenew(TIMEOUT_RENEWAL_THRESHOLD);
for (const mute of timeoutMutesToRenew) {
broadcastTimeoutMuteToRenew(mute);
}
console.log("[EXPIRING MUTES LOOP] Scheduling next loop"); console.log("[EXPIRING MUTES LOOP] Scheduling next loop");
setTimeout(() => runExpiringMutesLoop(), LOOP_INTERVAL); setTimeout(() => runExpiringMutesLoop(), LOOP_INTERVAL);
} }
@ -69,7 +100,7 @@ export function registerExpiringMute(mute: Mute) {
timeouts.set( timeouts.set(
muteToKey(mute), muteToKey(mute),
setTimeout(() => broadcastExpiredMute(mute), remaining), setTimeout(() => broadcastExpiredMute(mute.guild_id, mute.user_id), remaining),
); );
} }

View file

@ -1,10 +1,10 @@
// tslint:disable:no-console // tslint:disable:no-console
import { lazyMemoize, MINUTES } from "../../utils";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { lazyMemoize, MINUTES } from "../../utils";
import { Tempban } from "../entities/Tempban";
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents"; import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
import { Tempbans } from "../Tempbans"; import { Tempbans } from "../Tempbans";
import { Tempban } from "../entities/Tempban";
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
const LOOP_INTERVAL = 15 * MINUTES; const LOOP_INTERVAL = 15 * MINUTES;

View file

@ -1,11 +1,11 @@
// tslint:disable:no-console // tslint:disable:no-console
import { lazyMemoize, MINUTES } from "../../utils";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents"; import { lazyMemoize, MINUTES } from "../../utils";
import Timeout = NodeJS.Timeout;
import { VCAlerts } from "../VCAlerts";
import { VCAlert } from "../entities/VCAlert"; import { VCAlert } from "../entities/VCAlert";
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
import { VCAlerts } from "../VCAlerts";
import Timeout = NodeJS.Timeout;
const LOOP_INTERVAL = 15 * MINUTES; const LOOP_INTERVAL = 15 * MINUTES;
const MAX_TRIES_PER_SERVER = 3; const MAX_TRIES_PER_SERVER = 3;

View file

@ -1,6 +1,6 @@
// tslint:disable:no-console // tslint:disable:no-console
import { HOURS, MINUTES } from "../../utils"; import { MINUTES } from "../../utils";
import { import {
deleteStalePhishermanCacheEntries, deleteStalePhishermanCacheEntries,
deleteStalePhishermanKeyCacheEntries, deleteStalePhishermanKeyCacheEntries,

View file

@ -1,9 +1,9 @@
// tslint:disable:no-console // tslint:disable:no-console
import { lazyMemoize, MINUTES } from "../../utils";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents"; import { lazyMemoize, MINUTES } from "../../utils";
import { Reminder } from "../entities/Reminder"; import { Reminder } from "../entities/Reminder";
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
import { Reminders } from "../Reminders"; import { Reminders } from "../Reminders";
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;

View file

@ -1,10 +1,10 @@
// tslint:disable:no-console // tslint:disable:no-console
import { lazyMemoize, MINUTES } from "../../utils";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { lazyMemoize, MINUTES } from "../../utils";
import { ScheduledPost } from "../entities/ScheduledPost";
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents"; import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
import { ScheduledPosts } from "../ScheduledPosts"; import { ScheduledPosts } from "../ScheduledPosts";
import { ScheduledPost } from "../entities/ScheduledPost";
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
const LOOP_INTERVAL = 15 * MINUTES; const LOOP_INTERVAL = 15 * MINUTES;

View file

@ -1,5 +1,5 @@
import { AdvancedConsoleLogger } from "typeorm/logger/AdvancedConsoleLogger";
import type { QueryRunner } from "typeorm"; import type { QueryRunner } from "typeorm";
import { AdvancedConsoleLogger } from "typeorm/logger/AdvancedConsoleLogger";
let groupedQueryStats: Map<string, number> = new Map(); let groupedQueryStats: Map<string, number> = new Map();

View file

@ -1,8 +1,8 @@
import path from "path";
import fs from "fs";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { rootDir } from "./paths"; import fs from "fs";
import path from "path";
import { z } from "zod"; import { z } from "zod";
import { rootDir } from "./paths";
const envType = z.object({ const envType = z.object({
KEY: z.string().length(32), KEY: z.string().length(32),
@ -52,11 +52,11 @@ const envType = z.object({
DB_DATABASE: z.string().optional().default("zeppelin"), DB_DATABASE: z.string().optional().default("zeppelin"),
}); });
let toValidate = {}; let toValidate = { ...process.env };
const envPath = path.join(rootDir, ".env"); const envPath = path.join(rootDir, ".env");
if (fs.existsSync(envPath)) { if (fs.existsSync(envPath)) {
const buf = fs.readFileSync(envPath); const buf = fs.readFileSync(envPath);
toValidate = dotenv.parse(buf); toValidate = { ...toValidate, ...dotenv.parse(buf) };
} }
export const env = envType.parse(toValidate); export const env = envType.parse(toValidate);

View file

@ -1,42 +1,45 @@
import { Client, Constants, Intents, Options, TextChannel, ThreadChannel } from "discord.js"; import {
import { Knub, PluginError } from "knub"; Client,
import { PluginLoadError } from "knub/dist/plugins/PluginLoadError"; Events,
// Always use UTC internally GatewayIntentBits,
// This is also enforced for the database in data/db.ts Options,
Partials,
RESTEvents,
TextChannel,
ThreadChannel,
} from "discord.js";
import { EventEmitter } from "events";
import { Knub, PluginError, PluginLoadError, PluginNotLoadedError } from "knub";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { performance } from "perf_hooks";
import { AllowedGuilds } from "./data/AllowedGuilds"; import { AllowedGuilds } from "./data/AllowedGuilds";
import { Configs } from "./data/Configs"; import { Configs } from "./data/Configs";
import { connect } from "./data/db"; import { connect } from "./data/db";
import { GuildLogs } from "./data/GuildLogs"; import { GuildLogs } from "./data/GuildLogs";
import { LogType } from "./data/LogType"; import { LogType } from "./data/LogType";
import { DiscordJSError } from "./DiscordJSError"; import { runExpiredArchiveDeletionLoop } from "./data/loops/expiredArchiveDeletionLoop";
import { logger } from "./logger";
import { baseGuildPlugins, globalPlugins, guildPlugins } from "./plugins/availablePlugins";
import { RecoverablePluginError } from "./RecoverablePluginError";
import { SimpleError } from "./SimpleError";
import { ZeppelinGlobalConfig, ZeppelinGuildConfig } from "./types";
import { startUptimeCounter } from "./uptime";
import { errorMessage, isDiscordAPIError, isDiscordHTTPError, MINUTES, SECONDS, sleep, successMessage } from "./utils";
import { loadYamlSafely } from "./utils/loadYamlSafely";
import { DecayingCounter } from "./utils/DecayingCounter";
import { PluginNotLoadedError } from "knub/dist/plugins/PluginNotLoadedError";
import { logRestCall } from "./restCallStats";
import { logRateLimit } from "./rateLimitStats";
import { runExpiringMutesLoop } from "./data/loops/expiringMutesLoop"; import { runExpiringMutesLoop } from "./data/loops/expiringMutesLoop";
import { runUpcomingRemindersLoop } from "./data/loops/upcomingRemindersLoop";
import { runUpcomingScheduledPostsLoop } from "./data/loops/upcomingScheduledPostsLoop";
import { runExpiringTempbansLoop } from "./data/loops/expiringTempbansLoop"; import { runExpiringTempbansLoop } from "./data/loops/expiringTempbansLoop";
import { runExpiringVCAlertsLoop } from "./data/loops/expiringVCAlertsLoop"; import { runExpiringVCAlertsLoop } from "./data/loops/expiringVCAlertsLoop";
import { runExpiredArchiveDeletionLoop } from "./data/loops/expiredArchiveDeletionLoop";
import { runSavedMessageCleanupLoop } from "./data/loops/savedMessageCleanupLoop";
import { performance } from "perf_hooks";
import { setProfiler } from "./profiler";
import { enableProfiling } from "./utils/easyProfiler";
import { runPhishermanCacheCleanupLoop, runPhishermanReportingLoop } from "./data/loops/phishermanLoops"; import { runPhishermanCacheCleanupLoop, runPhishermanReportingLoop } from "./data/loops/phishermanLoops";
import { runSavedMessageCleanupLoop } from "./data/loops/savedMessageCleanupLoop";
import { runUpcomingRemindersLoop } from "./data/loops/upcomingRemindersLoop";
import { runUpcomingScheduledPostsLoop } from "./data/loops/upcomingScheduledPostsLoop";
import { hasPhishermanMasterAPIKey } from "./data/Phisherman"; import { hasPhishermanMasterAPIKey } from "./data/Phisherman";
import { consumeQueryStats } from "./data/queryLogger"; import { consumeQueryStats } from "./data/queryLogger";
import { EventEmitter } from "events"; import { DiscordJSError } from "./DiscordJSError";
import { env } from "./env"; import { env } from "./env";
import { logger } from "./logger";
import { baseGuildPlugins, globalPlugins, guildPlugins } from "./plugins/availablePlugins";
import { setProfiler } from "./profiler";
import { logRateLimit } from "./rateLimitStats";
import { RecoverablePluginError } from "./RecoverablePluginError";
import { SimpleError } from "./SimpleError";
import { startUptimeCounter } from "./uptime";
import { errorMessage, isDiscordAPIError, isDiscordHTTPError, MINUTES, SECONDS, sleep, successMessage } from "./utils";
import { DecayingCounter } from "./utils/DecayingCounter";
import { enableProfiling } from "./utils/easyProfiler";
import { loadYamlSafely } from "./utils/loadYamlSafely";
// Error handling // Error handling
let recentPluginErrors = 0; let recentPluginErrors = 0;
@ -58,8 +61,8 @@ const SAFE_TO_IGNORE_ERIS_ERROR_CODES = [
const SAFE_TO_IGNORE_ERIS_ERROR_MESSAGES = ["Server didn't acknowledge previous heartbeat, possible lost connection"]; const SAFE_TO_IGNORE_ERIS_ERROR_MESSAGES = ["Server didn't acknowledge previous heartbeat, possible lost connection"];
function errorHandler(err) { function errorHandler(err) {
const guildName = err.guild?.name || "Global"; const guildId = err.guild?.id || err.guildId || "0";
const guildId = err.guild?.id || "0"; const guildName = err.guild?.name || (guildId && guildId !== "0" ? "Unknown" : "Global");
if (err instanceof RecoverablePluginError) { if (err instanceof RecoverablePluginError) {
// Recoverable plugin errors can be, well, recovered from. // Recoverable plugin errors can be, well, recovered from.
@ -162,6 +165,8 @@ for (const [i, part] of actualVersionParts.entries()) {
throw new SimpleError(`Unsupported Node.js version! Must be at least ${REQUIRED_NODE_VERSION}`); throw new SimpleError(`Unsupported Node.js version! Must be at least ${REQUIRED_NODE_VERSION}`);
} }
// Always use UTC internally
// This is also enforced for the database in data/db.ts
moment.tz.setDefault("UTC"); moment.tz.setDefault("UTC");
// Blocking check // Blocking check
@ -188,17 +193,19 @@ setInterval(() => {
logger.info("Connecting to database"); logger.info("Connecting to database");
connect().then(async () => { connect().then(async () => {
const client = new Client({ const client = new Client({
partials: ["USER", "CHANNEL", "GUILD_MEMBER", "MESSAGE", "REACTION"], partials: [Partials.User, Partials.Channel, Partials.GuildMember, Partials.Message, Partials.Reaction],
makeCache: Options.cacheWithLimits({ makeCache: Options.cacheWithLimits({
...Options.defaultMakeCacheSettings, ...Options.DefaultMakeCacheSettings,
MessageManager: 1, MessageManager: 1,
// GuildMemberManager: 15000, // GuildMemberManager: 15000,
GuildInviteManager: 0, GuildInviteManager: 0,
}), }),
restGlobalRateLimit: 50, rest: {
// restTimeOffset: 1000, // globalRequestsPerSecond: 50,
// offset: 1000,
},
// Disable mentions by default // Disable mentions by default
allowedMentions: { allowedMentions: {
@ -209,25 +216,26 @@ connect().then(async () => {
}, },
intents: [ intents: [
// Privileged // Privileged
Intents.FLAGS.GUILD_MEMBERS, GatewayIntentBits.GuildMembers,
// Intents.FLAGS.GUILD_PRESENCES, GatewayIntentBits.MessageContent,
Intents.FLAGS.GUILD_MESSAGE_TYPING, // GatewayIntentBits.GuildPresences,
// Regular // Regular
Intents.FLAGS.DIRECT_MESSAGES, GatewayIntentBits.GuildMessageTyping,
Intents.FLAGS.GUILD_BANS, GatewayIntentBits.DirectMessages,
Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS, GatewayIntentBits.GuildModeration,
Intents.FLAGS.GUILD_INVITES, GatewayIntentBits.GuildEmojisAndStickers,
Intents.FLAGS.GUILD_MESSAGE_REACTIONS, GatewayIntentBits.GuildInvites,
Intents.FLAGS.GUILD_MESSAGES, GatewayIntentBits.GuildMessageReactions,
Intents.FLAGS.GUILDS, GatewayIntentBits.GuildMessages,
Intents.FLAGS.GUILD_VOICE_STATES, GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
], ],
}); });
// FIXME: TS doesn't see Client as a child of EventEmitter for some reason // FIXME: TS doesn't see Client as a child of EventEmitter for some reason
(client as unknown as EventEmitter).setMaxListeners(200); (client as unknown as EventEmitter).setMaxListeners(200);
client.on(Constants.Events.RATE_LIMIT, (data) => { client.rest.on(RESTEvents.RateLimited, (data) => {
// tslint:disable-next-line:no-console // tslint:disable-next-line:no-console
// console.log(`[DEBUG] [RATE_LIMIT] ${JSON.stringify(data)}`); // console.log(`[DEBUG] [RATE_LIMIT] ${JSON.stringify(data)}`);
}); });
@ -235,7 +243,7 @@ connect().then(async () => {
const safe429DecayInterval = 5 * SECONDS; const safe429DecayInterval = 5 * SECONDS;
const safe429MaxCount = 5; const safe429MaxCount = 5;
const safe429Counter = new DecayingCounter(safe429DecayInterval); const safe429Counter = new DecayingCounter(safe429DecayInterval);
client.on(Constants.Events.DEBUG, (errorText) => { client.on(Events.Debug, (errorText) => {
if (!errorText.includes("429")) { if (!errorText.includes("429")) {
return; return;
} }
@ -258,7 +266,7 @@ connect().then(async () => {
const allowedGuilds = new AllowedGuilds(); const allowedGuilds = new AllowedGuilds();
const guildConfigs = new Configs(); const guildConfigs = new Configs();
const bot = new Knub<ZeppelinGuildConfig, ZeppelinGlobalConfig>(client, { const bot = new Knub(client, {
guildPlugins, guildPlugins,
globalPlugins, globalPlugins,
@ -299,7 +307,11 @@ connect().then(async () => {
const row = await guildConfigs.getActiveByKey(key); const row = await guildConfigs.getActiveByKey(key);
if (row) { if (row) {
try { try {
return loadYamlSafely(row.config); const loaded = loadYamlSafely(row.config);
// Remove deprecated properties some may still have in their config
delete loaded.success_emoji;
delete loaded.error_emoji;
return loaded;
} catch (err) { } catch (err) {
logger.error(`Error while loading config "${key}": ${err.message}`); logger.error(`Error while loading config "${key}": ${err.message}`);
return {}; return {};
@ -329,6 +341,7 @@ connect().then(async () => {
sendSuccessMessageFn(channel, body) { sendSuccessMessageFn(channel, body) {
const guildId = const guildId =
channel instanceof TextChannel || channel instanceof ThreadChannel ? channel.guild.id : undefined; channel instanceof TextChannel || channel instanceof ThreadChannel ? channel.guild.id : undefined;
// @ts-expect-error
const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.success_emoji : undefined; const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.success_emoji : undefined;
channel.send(successMessage(body, emoji)); channel.send(successMessage(body, emoji));
}, },
@ -336,6 +349,7 @@ connect().then(async () => {
sendErrorMessageFn(channel, body) { sendErrorMessageFn(channel, body) {
const guildId = const guildId =
channel instanceof TextChannel || channel instanceof ThreadChannel ? channel.guild.id : undefined; channel instanceof TextChannel || channel instanceof ThreadChannel ? channel.guild.id : undefined;
// @ts-expect-error
const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.error_emoji : undefined; const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.error_emoji : undefined;
channel.send(errorMessage(body, emoji)); channel.send(errorMessage(body, emoji));
}, },
@ -346,7 +360,7 @@ connect().then(async () => {
startUptimeCounter(); startUptimeCounter();
}); });
client.on(Constants.Events.RATE_LIMIT, (data) => { client.rest.on(RESTEvents.RateLimited, (data) => {
logRateLimit(data); logRateLimit(data);
}); });

View file

@ -0,0 +1,44 @@
import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from "typeorm";
export class AddTimeoutColumnsToMutes1680354053183 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumns("mutes", [
new TableColumn({
name: "type",
type: "tinyint",
unsigned: true,
default: 1, // The value for "Role" mute at the time of this migration
}),
new TableColumn({
name: "mute_role",
type: "bigint",
unsigned: true,
isNullable: true,
default: null,
}),
new TableColumn({
name: "timeout_expires_at",
type: "datetime",
isNullable: true,
default: null,
}),
]);
await queryRunner.createIndex(
"mutes",
new TableIndex({
columnNames: ["type"],
}),
);
await queryRunner.createIndex(
"mutes",
new TableIndex({
columnNames: ["timeout_expires_at"],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("mutes", "type");
await queryRunner.dropColumn("mutes", "mute_role");
}
}

View file

@ -2,19 +2,30 @@
* @file Utility functions that are plugin-instance-specific (i.e. use PluginData) * @file Utility functions that are plugin-instance-specific (i.e. use PluginData)
*/ */
import { GuildMember, Message, MessageMentionOptions, MessageOptions, TextChannel } from "discord.js"; import {
GuildMember,
Message,
MessageCreateOptions,
MessageMentionOptions,
PermissionsBitField,
TextBasedChannel,
} from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { CommandContext, configUtils, ConfigValidationError, GuildPluginData, helpers, PluginOptions } from "knub"; import {
import { PluginOverrideCriteria } from "knub/dist/config/configTypes"; AnyPluginData,
import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager"; // TODO: Export from Knub index CommandContext,
import { AnyPluginData } from "knub/dist/plugins/PluginData"; ConfigValidationError,
ExtendedMatchParams,
GuildPluginData,
helpers,
PluginOverrideCriteria,
} from "knub";
import { logger } from "./logger"; import { logger } from "./logger";
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
import { TZeppelinKnub } from "./types";
import { deepKeyIntersect, errorMessage, successMessage, tDeepPartial, tNullable } from "./utils";
import { Tail } from "./utils/typeUtils";
import { decodeAndValidateStrict, StrictValidationError, validate } from "./validatorUtils";
import { isStaff } from "./staff"; import { isStaff } from "./staff";
import { TZeppelinKnub } from "./types";
import { errorMessage, successMessage, tNullable } from "./utils";
import { Tail } from "./utils/typeUtils";
import { parseIoTsSchema, StrictValidationError } from "./validatorUtils";
const { getMemberLevel } = helpers; const { getMemberLevel } = helpers;
@ -23,10 +34,16 @@ export function canActOn(
member1: GuildMember, member1: GuildMember,
member2: GuildMember, member2: GuildMember,
allowSameLevel = false, allowSameLevel = false,
allowAdmins = false,
) { ) {
if (member2.id === pluginData.client.user!.id) { if (member2.id === pluginData.client.user!.id) {
return false; return false;
} }
const isOwnerOrAdmin =
member2.id === member2.guild.ownerId || member2.permissions.has(PermissionsBitField.Flags.Administrator);
if (isOwnerOrAdmin && !allowAdmins) {
return false;
}
const ourLevel = getMemberLevel(pluginData, member1); const ourLevel = getMemberLevel(pluginData, member1);
const memberLevel = getMemberLevel(pluginData, member2); const memberLevel = getMemberLevel(pluginData, member2);
@ -91,117 +108,35 @@ export function strictValidationErrorToConfigValidationError(err: StrictValidati
); );
} }
export function getPluginConfigPreprocessor( export function makeIoTsConfigParser<Schema extends t.Type<any>>(schema: Schema): (input: unknown) => t.TypeOf<Schema> {
blueprint: ZeppelinPlugin, return (input: unknown) => {
customPreprocessor?: ZeppelinPlugin["configPreprocessor"], try {
) { return parseIoTsSchema(schema, input);
return async (options: PluginOptions<any>, strict?: boolean) => { } catch (err) {
// 1. Validate the basic structure of plugin config if (err instanceof StrictValidationError) {
const basicOptionsValidation = validate(BasicPluginStructureType, options); throw strictValidationErrorToConfigValidationError(err);
if (basicOptionsValidation instanceof StrictValidationError) {
throw strictValidationErrorToConfigValidationError(basicOptionsValidation);
}
// 2. Validate config/overrides against *partial* config schema. This ensures valid properties have valid types.
const partialConfigSchema = tDeepPartial(blueprint.configSchema);
if (options.config) {
const partialConfigValidation = validate(partialConfigSchema, options.config);
if (partialConfigValidation instanceof StrictValidationError) {
throw strictValidationErrorToConfigValidationError(partialConfigValidation);
} }
throw err;
} }
if (options.overrides) {
for (const override of options.overrides) {
// Validate criteria and extra criteria
// FIXME: This is ugly
for (const key of Object.keys(override)) {
if (!validTopLevelOverrideKeys.includes(key)) {
if (strict) {
throw new ConfigValidationError(`Unknown override criterion '${key}'`);
}
delete override[key];
}
}
if (override.extra != null) {
for (const extraCriterion of Object.keys(override.extra)) {
if (!blueprint.customOverrideCriteriaFunctions?.[extraCriterion]) {
if (strict) {
throw new ConfigValidationError(`Unknown override extra criterion '${extraCriterion}'`);
}
delete override.extra[extraCriterion];
}
}
}
// Validate override config
const partialOverrideConfigValidation = decodeAndValidateStrict(partialConfigSchema, override.config || {});
if (partialOverrideConfigValidation instanceof StrictValidationError) {
throw strictValidationErrorToConfigValidationError(partialOverrideConfigValidation);
}
}
}
// 3. Run custom preprocessor, if any
if (customPreprocessor) {
options = await customPreprocessor(options);
}
// 4. Merge with default options and validate/decode the entire config
let decodedConfig = {};
const decodedOverrides: Array<PluginOverrideCriteria<unknown> & { config: any }> = [];
if (options.config) {
decodedConfig = blueprint.configSchema
? decodeAndValidateStrict(blueprint.configSchema, options.config)
: options.config;
if (decodedConfig instanceof StrictValidationError) {
throw strictValidationErrorToConfigValidationError(decodedConfig);
}
}
if (options.overrides) {
for (const override of options.overrides) {
const overrideConfigMergedWithBaseConfig = configUtils.mergeConfig(options.config || {}, override.config || {});
const decodedOverrideConfig = blueprint.configSchema
? decodeAndValidateStrict(blueprint.configSchema, overrideConfigMergedWithBaseConfig)
: overrideConfigMergedWithBaseConfig;
if (decodedOverrideConfig instanceof StrictValidationError) {
throw strictValidationErrorToConfigValidationError(decodedOverrideConfig);
}
decodedOverrides.push({
...override,
config: deepKeyIntersect(decodedOverrideConfig, override.config || {}),
});
}
}
return {
config: decodedConfig,
overrides: decodedOverrides,
};
}; };
} }
export async function sendSuccessMessage( export async function sendSuccessMessage(
pluginData: AnyPluginData<any>, pluginData: AnyPluginData<any>,
channel: TextChannel, channel: TextBasedChannel,
body: string, body: string,
allowedMentions?: MessageMentionOptions, allowedMentions?: MessageMentionOptions,
): Promise<Message | undefined> { ): Promise<Message | undefined> {
const emoji = pluginData.fullConfig.success_emoji || undefined; const emoji = pluginData.fullConfig.success_emoji || undefined;
const formattedBody = successMessage(body, emoji); const formattedBody = successMessage(body, emoji);
const content: MessageOptions = allowedMentions const content: MessageCreateOptions = allowedMentions
? { content: formattedBody, allowedMentions } ? { content: formattedBody, allowedMentions }
: { content: formattedBody }; : { content: formattedBody };
return channel return channel
.send({ ...content }) // Force line break .send({ ...content }) // Force line break
.catch((err) => { .catch((err) => {
const channelInfo = channel.guild ? `${channel.id} (${channel.guild.id})` : channel.id; const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id;
logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`); logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`);
return undefined; return undefined;
}); });
@ -209,20 +144,20 @@ export async function sendSuccessMessage(
export async function sendErrorMessage( export async function sendErrorMessage(
pluginData: AnyPluginData<any>, pluginData: AnyPluginData<any>,
channel: TextChannel, channel: TextBasedChannel,
body: string, body: string,
allowedMentions?: MessageMentionOptions, allowedMentions?: MessageMentionOptions,
): Promise<Message | undefined> { ): Promise<Message | undefined> {
const emoji = pluginData.fullConfig.error_emoji || undefined; const emoji = pluginData.fullConfig.error_emoji || undefined;
const formattedBody = errorMessage(body, emoji); const formattedBody = errorMessage(body, emoji);
const content: MessageOptions = allowedMentions const content: MessageCreateOptions = allowedMentions
? { content: formattedBody, allowedMentions } ? { content: formattedBody, allowedMentions }
: { content: formattedBody }; : { content: formattedBody };
return channel return channel
.send({ ...content }) // Force line break .send({ ...content }) // Force line break
.catch((err) => { .catch((err) => {
const channelInfo = channel.guild ? `${channel.id} (${channel.guild.id})` : channel.id; const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id;
logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`); logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`);
return undefined; return undefined;
}); });
@ -230,11 +165,13 @@ export async function sendErrorMessage(
export function getBaseUrl(pluginData: AnyPluginData<any>) { export function getBaseUrl(pluginData: AnyPluginData<any>) {
const knub = pluginData.getKnubInstance() as TZeppelinKnub; const knub = pluginData.getKnubInstance() as TZeppelinKnub;
// @ts-expect-error
return knub.getGlobalConfig().url; return knub.getGlobalConfig().url;
} }
export function isOwner(pluginData: AnyPluginData<any>, userId: string) { export function isOwner(pluginData: AnyPluginData<any>, userId: string) {
const knub = pluginData.getKnubInstance() as TZeppelinKnub; const knub = pluginData.getKnubInstance() as TZeppelinKnub;
// @ts-expect-error
const owners = knub.getGlobalConfig()?.owners; const owners = knub.getGlobalConfig()?.owners;
if (!owners) { if (!owners) {
return false; return false;

View file

@ -1,6 +1,7 @@
import { PluginOptions } from "knub"; import { PluginOptions } from "knub";
import { GuildLogs } from "../../data/GuildLogs"; import { GuildLogs } from "../../data/GuildLogs";
import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { makeIoTsConfigParser } from "../../pluginUtils";
import { LogsPlugin } from "../Logs/LogsPlugin"; import { LogsPlugin } from "../Logs/LogsPlugin";
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
@ -23,10 +24,11 @@ export const AutoDeletePlugin = zeppelinGuildPlugin<AutoDeletePluginType>()({
prettyName: "Auto-delete", prettyName: "Auto-delete",
description: "Allows Zeppelin to auto-delete messages from a channel after a delay", description: "Allows Zeppelin to auto-delete messages from a channel after a delay",
configurationGuide: "Maximum deletion delay is currently 5 minutes", configurationGuide: "Maximum deletion delay is currently 5 minutes",
configSchema: ConfigSchema,
}, },
dependencies: () => [TimeAndDatePlugin, LogsPlugin], dependencies: () => [TimeAndDatePlugin, LogsPlugin],
configSchema: ConfigSchema, configParser: makeIoTsConfigParser(ConfigSchema),
defaultOptions, defaultOptions,
beforeLoad(pluginData) { beforeLoad(pluginData) {
@ -56,8 +58,10 @@ export const AutoDeletePlugin = zeppelinGuildPlugin<AutoDeletePluginType>()({
}, },
beforeUnload(pluginData) { beforeUnload(pluginData) {
pluginData.state.guildSavedMessages.events.off("create", pluginData.state.onMessageCreateFn); const { state, guild } = pluginData;
pluginData.state.guildSavedMessages.events.off("delete", pluginData.state.onMessageDeleteFn);
pluginData.state.guildSavedMessages.events.off("deleteBulk", pluginData.state.onMessageDeleteBulkFn); state.guildSavedMessages.events.off("create", state.onMessageCreateFn);
state.guildSavedMessages.events.off("delete", state.onMessageDeleteFn);
state.guildSavedMessages.events.off("deleteBulk", state.onMessageDeleteBulkFn);
}, },
}); });

View file

@ -1,7 +1,6 @@
import { Permissions, Snowflake, TextChannel } from "discord.js"; import { ChannelType, PermissionsBitField, Snowflake } from "discord.js";
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { channelToTemplateSafeChannel, userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { LogType } from "../../../data/LogType"; import { LogType } from "../../../data/LogType";
import { logger } from "../../../logger"; import { logger } from "../../../logger";
import { resolveUser, verboseChannelMention } from "../../../utils"; import { resolveUser, verboseChannelMention } from "../../../utils";
@ -17,8 +16,8 @@ export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePlugi
scheduleNextDeletion(pluginData); scheduleNextDeletion(pluginData);
const channel = pluginData.guild.channels.cache.get(itemToDelete.message.channel_id as Snowflake) as TextChannel; const channel = pluginData.guild.channels.cache.get(itemToDelete.message.channel_id as Snowflake);
if (!channel) { if (!channel || channel.type === ChannelType.GuildCategory) {
// Channel was deleted, ignore // Channel was deleted, ignore
return; return;
} }
@ -26,7 +25,9 @@ export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePlugi
const logs = pluginData.getPlugin(LogsPlugin); const logs = pluginData.getPlugin(LogsPlugin);
const perms = channel.permissionsFor(pluginData.client.user!.id); const perms = channel.permissionsFor(pluginData.client.user!.id);
if (!hasDiscordPermissions(perms, Permissions.FLAGS.VIEW_CHANNEL | Permissions.FLAGS.READ_MESSAGE_HISTORY)) { if (
!hasDiscordPermissions(perms, PermissionsBitField.Flags.ViewChannel | PermissionsBitField.Flags.ReadMessageHistory)
) {
logs.logBotAlert({ logs.logBotAlert({
body: `Missing permissions to read messages or message history in auto-delete channel ${verboseChannelMention( body: `Missing permissions to read messages or message history in auto-delete channel ${verboseChannelMention(
channel, channel,
@ -35,7 +36,7 @@ export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePlugi
return; return;
} }
if (!hasDiscordPermissions(perms, Permissions.FLAGS.MANAGE_MESSAGES)) { if (!hasDiscordPermissions(perms, PermissionsBitField.Flags.ManageMessages)) {
logs.logBotAlert({ logs.logBotAlert({
body: `Missing permissions to delete messages in auto-delete channel ${verboseChannelMention(channel)}`, body: `Missing permissions to delete messages in auto-delete channel ${verboseChannelMention(channel)}`,
}); });
@ -45,7 +46,7 @@ export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePlugi
const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
pluginData.state.guildLogs.ignoreLog(LogType.MESSAGE_DELETE, itemToDelete.message.id); pluginData.state.guildLogs.ignoreLog(LogType.MESSAGE_DELETE, itemToDelete.message.id);
(channel as TextChannel).messages.delete(itemToDelete.message.id as Snowflake).catch((err) => { channel.messages.delete(itemToDelete.message.id as Snowflake).catch((err) => {
if (err.code === 10008) { if (err.code === 10008) {
// "Unknown Message", probably already deleted by automod or another bot, ignore // "Unknown Message", probably already deleted by automod or another bot, ignore
return; return;

View file

@ -1,10 +1,9 @@
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { SavedMessage } from "../../../data/entities/SavedMessage"; import { SavedMessage } from "../../../data/entities/SavedMessage";
import { LogType } from "../../../data/LogType";
import { convertDelayStringToMS, resolveMember } from "../../../utils"; import { convertDelayStringToMS, resolveMember } from "../../../utils";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { AutoDeletePluginType, MAX_DELAY } from "../types"; import { AutoDeletePluginType, MAX_DELAY } from "../types";
import { addMessageToDeletionQueue } from "./addMessageToDeletionQueue"; import { addMessageToDeletionQueue } from "./addMessageToDeletionQueue";
import { LogsPlugin } from "../../Logs/LogsPlugin";
export async function onMessageCreate(pluginData: GuildPluginData<AutoDeletePluginType>, msg: SavedMessage) { export async function onMessageCreate(pluginData: GuildPluginData<AutoDeletePluginType>, msg: SavedMessage) {
const member = await resolveMember(pluginData.client, pluginData.guild, msg.user_id); const member = await resolveMember(pluginData.client, pluginData.guild, msg.user_id);

View file

@ -1,6 +1,7 @@
import { PluginOptions } from "knub"; import { PluginOptions } from "knub";
import { GuildAutoReactions } from "../../data/GuildAutoReactions"; import { GuildAutoReactions } from "../../data/GuildAutoReactions";
import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { makeIoTsConfigParser } from "../../pluginUtils";
import { trimPluginDescription } from "../../utils"; import { trimPluginDescription } from "../../utils";
import { LogsPlugin } from "../Logs/LogsPlugin"; import { LogsPlugin } from "../Logs/LogsPlugin";
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
@ -31,6 +32,7 @@ export const AutoReactionsPlugin = zeppelinGuildPlugin<AutoReactionsPluginType>(
description: trimPluginDescription(` description: trimPluginDescription(`
Allows setting up automatic reactions to all new messages on a channel Allows setting up automatic reactions to all new messages on a channel
`), `),
configSchema: ConfigSchema,
}, },
// prettier-ignore // prettier-ignore
@ -38,11 +40,11 @@ export const AutoReactionsPlugin = zeppelinGuildPlugin<AutoReactionsPluginType>(
LogsPlugin, LogsPlugin,
], ],
configSchema: ConfigSchema, configParser: makeIoTsConfigParser(ConfigSchema),
defaultOptions, defaultOptions,
// prettier-ignore // prettier-ignore
commands: [ messageCommands: [
NewAutoReactionsCmd, NewAutoReactionsCmd,
DisableAutoReactionsCmd, DisableAutoReactionsCmd,
], ],
@ -53,8 +55,10 @@ export const AutoReactionsPlugin = zeppelinGuildPlugin<AutoReactionsPluginType>(
], ],
beforeLoad(pluginData) { beforeLoad(pluginData) {
pluginData.state.savedMessages = GuildSavedMessages.getGuildInstance(pluginData.guild.id); const { state, guild } = pluginData;
pluginData.state.autoReactions = GuildAutoReactions.getGuildInstance(pluginData.guild.id);
pluginData.state.cache = new Map(); state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id);
state.autoReactions = GuildAutoReactions.getGuildInstance(guild.id);
state.cache = new Map();
}, },
}); });

View file

@ -1,4 +1,4 @@
import { GuildChannel, Permissions } from "discord.js"; import { PermissionsBitField } from "discord.js";
import { commandTypeHelpers as ct } from "../../../commandTypes"; import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
import { canUseEmoji, customEmojiRegex, isEmoji } from "../../../utils"; import { canUseEmoji, customEmojiRegex, isEmoji } from "../../../utils";
@ -7,7 +7,7 @@ import { missingPermissionError } from "../../../utils/missingPermissionError";
import { readChannelPermissions } from "../../../utils/readChannelPermissions"; import { readChannelPermissions } from "../../../utils/readChannelPermissions";
import { autoReactionsCmd } from "../types"; import { autoReactionsCmd } from "../types";
const requiredPermissions = readChannelPermissions | Permissions.FLAGS.ADD_REACTIONS; const requiredPermissions = readChannelPermissions | PermissionsBitField.Flags.AddReactions;
export const NewAutoReactionsCmd = autoReactionsCmd({ export const NewAutoReactionsCmd = autoReactionsCmd({
trigger: "auto_reactions", trigger: "auto_reactions",

View file

@ -1,14 +1,13 @@
import { GuildChannel, GuildTextBasedChannel, Permissions } from "discord.js"; import { GuildTextBasedChannel, PermissionsBitField } from "discord.js";
import { LogType } from "../../../data/LogType"; import { AutoReaction } from "../../../data/entities/AutoReaction";
import { isDiscordAPIError } from "../../../utils"; import { isDiscordAPIError } from "../../../utils";
import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions"; import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions";
import { missingPermissionError } from "../../../utils/missingPermissionError"; import { missingPermissionError } from "../../../utils/missingPermissionError";
import { readChannelPermissions } from "../../../utils/readChannelPermissions"; import { readChannelPermissions } from "../../../utils/readChannelPermissions";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { autoReactionsEvt } from "../types"; import { autoReactionsEvt } from "../types";
import { AutoReaction } from "../../../data/entities/AutoReaction";
const p = Permissions.FLAGS; const p = PermissionsBitField.Flags;
export const AddReactionsEvt = autoReactionsEvt({ export const AddReactionsEvt = autoReactionsEvt({
event: "messageCreate", event: "messageCreate",
@ -40,7 +39,7 @@ export const AddReactionsEvt = autoReactionsEvt({
const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!; const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!;
if (me) { if (me) {
const missingPermissions = getMissingChannelPermissions(me, channel, readChannelPermissions | p.ADD_REACTIONS); const missingPermissions = getMissingChannelPermissions(me, channel, readChannelPermissions | p.AddReactions);
if (missingPermissions) { if (missingPermissions) {
const logs = pluginData.getPlugin(LogsPlugin); const logs = pluginData.getPlugin(LogsPlugin);
logs.logBotAlert({ logs.logBotAlert({

View file

@ -1,9 +1,9 @@
import * as t from "io-ts"; import * as t from "io-ts";
import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub";
import { AutoReaction } from "../../data/entities/AutoReaction";
import { GuildAutoReactions } from "../../data/GuildAutoReactions"; import { GuildAutoReactions } from "../../data/GuildAutoReactions";
import { GuildLogs } from "../../data/GuildLogs"; import { GuildLogs } from "../../data/GuildLogs";
import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { AutoReaction } from "../../data/entities/AutoReaction";
export const ConfigSchema = t.type({ export const ConfigSchema = t.type({
can_manage: t.boolean, can_manage: t.boolean,
@ -20,5 +20,5 @@ export interface AutoReactionsPluginType extends BasePluginType {
}; };
} }
export const autoReactionsCmd = typedGuildCommand<AutoReactionsPluginType>(); export const autoReactionsCmd = guildPluginMessageCommand<AutoReactionsPluginType>();
export const autoReactionsEvt = typedGuildEventListener<AutoReactionsPluginType>(); export const autoReactionsEvt = guildPluginEventListener<AutoReactionsPluginType>();

View file

@ -1,5 +1,4 @@
import { configUtils, CooldownManager } from "knub"; import { configUtils, CooldownManager } from "knub";
import { ConfigPreprocessorFn } from "knub/dist/config/configTypes";
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels"; import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
import { GuildArchives } from "../../data/GuildArchives"; import { GuildArchives } from "../../data/GuildArchives";
import { GuildLogs } from "../../data/GuildLogs"; import { GuildLogs } from "../../data/GuildLogs";
@ -9,11 +8,13 @@ import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners";
import { MINUTES, SECONDS } from "../../utils"; import { MINUTES, SECONDS } from "../../utils";
import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap"; import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap";
import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap"; import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap";
import { StrictValidationError } from "../../validatorUtils"; import { parseIoTsSchema, StrictValidationError } from "../../validatorUtils";
import { CountersPlugin } from "../Counters/CountersPlugin"; import { CountersPlugin } from "../Counters/CountersPlugin";
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin";
import { LogsPlugin } from "../Logs/LogsPlugin"; import { LogsPlugin } from "../Logs/LogsPlugin";
import { ModActionsPlugin } from "../ModActions/ModActionsPlugin"; import { ModActionsPlugin } from "../ModActions/ModActionsPlugin";
import { MutesPlugin } from "../Mutes/MutesPlugin"; import { MutesPlugin } from "../Mutes/MutesPlugin";
import { PhishermanPlugin } from "../Phisherman/PhishermanPlugin";
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
import { availableActions } from "./actions/availableActions"; import { availableActions } from "./actions/availableActions";
import { AntiraidClearCmd } from "./commands/AntiraidClearCmd"; import { AntiraidClearCmd } from "./commands/AntiraidClearCmd";
@ -35,8 +36,6 @@ import { clearOldRecentSpam } from "./functions/clearOldRecentSpam";
import { pluginInfo } from "./info"; import { pluginInfo } from "./info";
import { availableTriggers } from "./triggers/availableTriggers"; import { availableTriggers } from "./triggers/availableTriggers";
import { AutomodPluginType, ConfigSchema } from "./types"; import { AutomodPluginType, ConfigSchema } from "./types";
import { PhishermanPlugin } from "../Phisherman/PhishermanPlugin";
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin";
const defaultOptions = { const defaultOptions = {
config: { config: {
@ -63,13 +62,15 @@ const defaultOptions = {
/** /**
* Config preprocessor to set default values for triggers and perform extra validation * Config preprocessor to set default values for triggers and perform extra validation
* TODO: Separate input and output types
*/ */
const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) => { const configParser = (input: unknown) => {
if (options.config?.rules) { const rules = (input as any).rules;
if (rules) {
// Loop through each rule // Loop through each rule
for (const [name, rule] of Object.entries(options.config.rules)) { for (const [name, rule] of Object.entries(rules)) {
if (rule == null) { if (rule == null) {
delete options.config.rules[name]; delete rules[name];
continue; continue;
} }
@ -97,7 +98,7 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
for (const triggerObj of rule["triggers"]) { for (const triggerObj of rule["triggers"]) {
for (const triggerName in triggerObj) { for (const triggerName in triggerObj) {
if (!availableTriggers[triggerName]) { if (!availableTriggers[triggerName]) {
throw new StrictValidationError([`Unknown trigger '${triggerName}' in rule '${rule.name}'`]); throw new StrictValidationError([`Unknown trigger '${triggerName}' in rule '${rule["name"]}'`]);
} }
const triggerBlueprint = availableTriggers[triggerName]; const triggerBlueprint = availableTriggers[triggerName];
@ -117,11 +118,11 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
if (white && black) { if (white && black) {
throw new StrictValidationError([ throw new StrictValidationError([
`Cannot have both blacklist and whitelist enabled at rule <${rule.name}/match_attachment_type>`, `Cannot have both blacklist and whitelist enabled at rule <${rule["name"]}/match_attachment_type>`,
]); ]);
} else if (!white && !black) { } else if (!white && !black) {
throw new StrictValidationError([ throw new StrictValidationError([
`Must have either blacklist or whitelist enabled at rule <${rule.name}/match_attachment_type>`, `Must have either blacklist or whitelist enabled at rule <${rule["name"]}/match_attachment_type>`,
]); ]);
} }
} }
@ -132,11 +133,11 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
if (white && black) { if (white && black) {
throw new StrictValidationError([ throw new StrictValidationError([
`Cannot have both blacklist and whitelist enabled at rule <${rule.name}/match_mime_type>`, `Cannot have both blacklist and whitelist enabled at rule <${rule["name"]}/match_mime_type>`,
]); ]);
} else if (!white && !black) { } else if (!white && !black) {
throw new StrictValidationError([ throw new StrictValidationError([
`Must have either blacklist or whitelist enabled at rule <${rule.name}/match_mime_type>`, `Must have either blacklist or whitelist enabled at rule <${rule["name"]}/match_mime_type>`,
]); ]);
} }
} }
@ -147,7 +148,7 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
if (rule["actions"]) { if (rule["actions"]) {
for (const actionName in rule["actions"]) { for (const actionName in rule["actions"]) {
if (!availableActions[actionName]) { if (!availableActions[actionName]) {
throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule.name}'`]); throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule["name"]}'`]);
} }
const actionBlueprint = availableActions[actionName]; const actionBlueprint = availableActions[actionName];
@ -163,9 +164,9 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
// Enable logging of automod actions by default // Enable logging of automod actions by default
if (rule["actions"]) { if (rule["actions"]) {
for (const actionName in rule.actions) { for (const actionName in rule["actions"]) {
if (!availableActions[actionName]) { if (!availableActions[actionName]) {
throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule.name}'`]); throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule["name"]}'`]);
} }
} }
@ -173,13 +174,13 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
rule["actions"]["log"] = true; rule["actions"]["log"] = true;
} }
if (rule["actions"]["clean"] && rule["actions"]["start_thread"]) { if (rule["actions"]["clean"] && rule["actions"]["start_thread"]) {
throw new StrictValidationError([`Cannot have both clean and start_thread at rule '${rule.name}'`]); throw new StrictValidationError([`Cannot have both clean and start_thread at rule '${rule["name"]}'`]);
} }
} }
} }
} }
return options; return parseIoTsSchema(ConfigSchema, input);
}; };
export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
@ -197,9 +198,8 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
InternalPosterPlugin, InternalPosterPlugin,
], ],
configSchema: ConfigSchema,
defaultOptions, defaultOptions,
configPreprocessor, configParser,
customOverrideCriteriaFunctions: { customOverrideCriteriaFunctions: {
antiraid_level: (pluginData, matchParams, value) => { antiraid_level: (pluginData, matchParams, value) => {
@ -218,136 +218,126 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
// Messages use message events from SavedMessages, see onLoad below // Messages use message events from SavedMessages, see onLoad below
], ],
commands: [AntiraidClearCmd, SetAntiraidCmd, ViewAntiraidCmd], messageCommands: [AntiraidClearCmd, SetAntiraidCmd, ViewAntiraidCmd],
async beforeLoad(pluginData) { async beforeLoad(pluginData) {
pluginData.state.queue = new Queue(); const { state, guild } = pluginData;
pluginData.state.regexRunner = getRegExpRunner(`guild-${pluginData.guild.id}`); state.queue = new Queue();
pluginData.state.recentActions = []; state.regexRunner = getRegExpRunner(`guild-${guild.id}`);
pluginData.state.recentSpam = []; state.recentActions = [];
pluginData.state.recentNicknameChanges = new Map(); state.recentSpam = [];
pluginData.state.ignoredRoleChanges = new Set(); state.recentNicknameChanges = new Map();
pluginData.state.cooldownManager = new CooldownManager(); state.ignoredRoleChanges = new Set();
pluginData.state.logs = new GuildLogs(pluginData.guild.id); state.cooldownManager = new CooldownManager();
pluginData.state.savedMessages = GuildSavedMessages.getGuildInstance(pluginData.guild.id);
pluginData.state.antiraidLevels = GuildAntiraidLevels.getGuildInstance(pluginData.guild.id);
pluginData.state.archives = GuildArchives.getGuildInstance(pluginData.guild.id);
pluginData.state.cachedAntiraidLevel = await pluginData.state.antiraidLevels.get(); state.logs = new GuildLogs(guild.id);
state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id);
state.antiraidLevels = GuildAntiraidLevels.getGuildInstance(guild.id);
state.archives = GuildArchives.getGuildInstance(guild.id);
state.cachedAntiraidLevel = await state.antiraidLevels.get();
}, },
async afterLoad(pluginData) { async afterLoad(pluginData) {
pluginData.state.clearRecentActionsInterval = setInterval(() => clearOldRecentActions(pluginData), 1 * MINUTES); const { state, guild } = pluginData;
pluginData.state.clearRecentSpamInterval = setInterval(() => clearOldRecentSpam(pluginData), 1 * SECONDS);
pluginData.state.clearRecentNicknameChangesInterval = setInterval( state.clearRecentActionsInterval = setInterval(() => clearOldRecentActions(pluginData), 1 * MINUTES);
state.clearRecentSpamInterval = setInterval(() => clearOldRecentSpam(pluginData), 1 * SECONDS);
state.clearRecentNicknameChangesInterval = setInterval(
() => clearOldRecentNicknameChanges(pluginData), () => clearOldRecentNicknameChanges(pluginData),
30 * SECONDS, 30 * SECONDS,
); );
pluginData.state.onMessageCreateFn = (message) => runAutomodOnMessage(pluginData, message, false); state.onMessageCreateFn = (message) => runAutomodOnMessage(pluginData, message, false);
pluginData.state.savedMessages.events.on("create", pluginData.state.onMessageCreateFn); state.savedMessages.events.on("create", state.onMessageCreateFn);
pluginData.state.onMessageUpdateFn = (message) => runAutomodOnMessage(pluginData, message, true);
pluginData.state.savedMessages.events.on("update", pluginData.state.onMessageUpdateFn);
state.onMessageUpdateFn = (message) => runAutomodOnMessage(pluginData, message, true);
state.savedMessages.events.on("update", state.onMessageUpdateFn);
const countersPlugin = pluginData.getPlugin(CountersPlugin); const countersPlugin = pluginData.getPlugin(CountersPlugin);
pluginData.state.onCounterTrigger = (name, triggerName, channelId, userId) => { state.onCounterTrigger = (name, triggerName, channelId, userId) => {
runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, false); runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, false);
}; };
pluginData.state.onCounterReverseTrigger = (name, triggerName, channelId, userId) => { state.onCounterReverseTrigger = (name, triggerName, channelId, userId) => {
runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, true); runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, true);
}; };
countersPlugin.onCounterEvent("trigger", state.onCounterTrigger);
countersPlugin.onCounterEvent("trigger", pluginData.state.onCounterTrigger); countersPlugin.onCounterEvent("reverseTrigger", state.onCounterReverseTrigger);
countersPlugin.onCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger);
const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter(); const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter();
pluginData.state.modActionsListeners = new Map(); state.modActionsListeners = new Map();
pluginData.state.modActionsListeners.set("note", (userId: string) => state.modActionsListeners.set("note", (userId: string) => runAutomodOnModAction(pluginData, "note", userId));
runAutomodOnModAction(pluginData, "note", userId), state.modActionsListeners.set("warn", (userId: string, reason: string | undefined, isAutomodAction: boolean) =>
runAutomodOnModAction(pluginData, "warn", userId, reason, isAutomodAction),
); );
pluginData.state.modActionsListeners.set( state.modActionsListeners.set("kick", (userId: string, reason: string | undefined, isAutomodAction: boolean) =>
"warn", runAutomodOnModAction(pluginData, "kick", userId, reason, isAutomodAction),
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
runAutomodOnModAction(pluginData, "warn", userId, reason, isAutomodAction),
); );
pluginData.state.modActionsListeners.set( state.modActionsListeners.set("ban", (userId: string, reason: string | undefined, isAutomodAction: boolean) =>
"kick", runAutomodOnModAction(pluginData, "ban", userId, reason, isAutomodAction),
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
runAutomodOnModAction(pluginData, "kick", userId, reason, isAutomodAction),
); );
pluginData.state.modActionsListeners.set( state.modActionsListeners.set("unban", (userId: string) => runAutomodOnModAction(pluginData, "unban", userId));
"ban", registerEventListenersFromMap(modActionsEvents, state.modActionsListeners);
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
runAutomodOnModAction(pluginData, "ban", userId, reason, isAutomodAction),
);
pluginData.state.modActionsListeners.set("unban", (userId: string) =>
runAutomodOnModAction(pluginData, "unban", userId),
);
registerEventListenersFromMap(modActionsEvents, pluginData.state.modActionsListeners);
const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter(); const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter();
pluginData.state.mutesListeners = new Map(); state.mutesListeners = new Map();
pluginData.state.mutesListeners.set( state.mutesListeners.set("mute", (userId: string, reason: string | undefined, isAutomodAction: boolean) =>
"mute", runAutomodOnModAction(pluginData, "mute", userId, reason, isAutomodAction),
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
runAutomodOnModAction(pluginData, "mute", userId, reason, isAutomodAction),
); );
pluginData.state.mutesListeners.set("unmute", (userId: string) => state.mutesListeners.set("unmute", (userId: string) => runAutomodOnModAction(pluginData, "unmute", userId));
runAutomodOnModAction(pluginData, "unmute", userId), registerEventListenersFromMap(mutesEvents, state.mutesListeners);
);
registerEventListenersFromMap(mutesEvents, pluginData.state.mutesListeners);
}, },
async beforeUnload(pluginData) { async beforeUnload(pluginData) {
const { state, guild } = pluginData;
const countersPlugin = pluginData.getPlugin(CountersPlugin); const countersPlugin = pluginData.getPlugin(CountersPlugin);
if (pluginData.state.onCounterTrigger) { if (state.onCounterTrigger) {
countersPlugin.offCounterEvent("trigger", pluginData.state.onCounterTrigger); countersPlugin.offCounterEvent("trigger", state.onCounterTrigger);
} }
if (pluginData.state.onCounterReverseTrigger) { if (state.onCounterReverseTrigger) {
countersPlugin.offCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger); countersPlugin.offCounterEvent("reverseTrigger", state.onCounterReverseTrigger);
} }
const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter(); const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter();
if (pluginData.state.modActionsListeners) { if (state.modActionsListeners) {
unregisterEventListenersFromMap(modActionsEvents, pluginData.state.modActionsListeners); unregisterEventListenersFromMap(modActionsEvents, state.modActionsListeners);
} }
const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter(); const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter();
if (pluginData.state.mutesListeners) { if (state.mutesListeners) {
unregisterEventListenersFromMap(mutesEvents, pluginData.state.mutesListeners); unregisterEventListenersFromMap(mutesEvents, state.mutesListeners);
} }
pluginData.state.queue.clear(); state.queue.clear();
discardRegExpRunner(`guild-${pluginData.guild.id}`); discardRegExpRunner(`guild-${guild.id}`);
if (pluginData.state.clearRecentActionsInterval) { if (state.clearRecentActionsInterval) {
clearInterval(pluginData.state.clearRecentActionsInterval); clearInterval(state.clearRecentActionsInterval);
} }
if (pluginData.state.clearRecentSpamInterval) { if (state.clearRecentSpamInterval) {
clearInterval(pluginData.state.clearRecentSpamInterval); clearInterval(state.clearRecentSpamInterval);
} }
if (pluginData.state.clearRecentNicknameChangesInterval) { if (state.clearRecentNicknameChangesInterval) {
clearInterval(pluginData.state.clearRecentNicknameChangesInterval); clearInterval(state.clearRecentNicknameChangesInterval);
} }
if (pluginData.state.onMessageCreateFn) { if (state.onMessageCreateFn) {
pluginData.state.savedMessages.events.off("create", pluginData.state.onMessageCreateFn); state.savedMessages.events.off("create", state.onMessageCreateFn);
} }
if (pluginData.state.onMessageUpdateFn) { if (state.onMessageUpdateFn) {
pluginData.state.savedMessages.events.off("update", pluginData.state.onMessageUpdateFn); state.savedMessages.events.off("update", state.onMessageUpdateFn);
} }
}, },
}); });

View file

@ -1,6 +1,5 @@
import { Permissions, Snowflake } from "discord.js"; import { PermissionFlagsBits, Snowflake } from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { LogType } from "../../../data/LogType";
import { nonNullish, unique } from "../../../utils"; import { nonNullish, unique } from "../../../utils";
import { canAssignRole } from "../../../utils/canAssignRole"; import { canAssignRole } from "../../../utils/canAssignRole";
import { getMissingPermissions } from "../../../utils/getMissingPermissions"; import { getMissingPermissions } from "../../../utils/getMissingPermissions";
@ -10,7 +9,7 @@ import { LogsPlugin } from "../../Logs/LogsPlugin";
import { ignoreRoleChange } from "../functions/ignoredRoleChanges"; import { ignoreRoleChange } from "../functions/ignoredRoleChanges";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
const p = Permissions.FLAGS; const p = PermissionFlagsBits;
export const AddRolesAction = automodAction({ export const AddRolesAction = automodAction({
configType: t.array(t.string), configType: t.array(t.string),
@ -20,7 +19,7 @@ export const AddRolesAction = automodAction({
const members = unique(contexts.map((c) => c.member).filter(nonNullish)); const members = unique(contexts.map((c) => c.member).filter(nonNullish));
const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!; const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!;
const missingPermissions = getMissingPermissions(me.permissions, p.MANAGE_ROLES); const missingPermissions = getMissingPermissions(me.permissions, p.ManageRoles);
if (missingPermissions) { if (missingPermissions) {
const logs = pluginData.getPlugin(LogsPlugin); const logs = pluginData.getPlugin(LogsPlugin);
logs.logBotAlert({ logs.logBotAlert({

View file

@ -1,8 +1,7 @@
import * as t from "io-ts"; import * as t from "io-ts";
import { LogType } from "../../../data/LogType";
import { CountersPlugin } from "../../Counters/CountersPlugin"; import { CountersPlugin } from "../../Counters/CountersPlugin";
import { automodAction } from "../helpers";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
export const AddToCounterAction = automodAction({ export const AddToCounterAction = automodAction({
configType: t.type({ configType: t.type({

View file

@ -1,4 +1,4 @@
import { Snowflake, TextChannel, ThreadChannel } from "discord.js"; import { Snowflake } from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { erisAllowedMentionsToDjsMentionOptions } from "src/utils/erisAllowedMentionsToDjsMentionOptions"; import { erisAllowedMentionsToDjsMentionOptions } from "src/utils/erisAllowedMentionsToDjsMentionOptions";
import { LogType } from "../../../data/LogType"; import { LogType } from "../../../data/LogType";
@ -9,21 +9,19 @@ import {
TemplateSafeValueContainer, TemplateSafeValueContainer,
} from "../../../templateFormatter"; } from "../../../templateFormatter";
import { import {
createChunkedMessage, chunkMessageLines,
isTruthy,
messageLink, messageLink,
stripObjectToScalars,
tAllowedMentions, tAllowedMentions,
tNormalizedNullOptional, tNormalizedNullOptional,
isTruthy,
verboseChannelMention,
validateAndParseMessageContent, validateAndParseMessageContent,
chunkMessageLines, verboseChannelMention,
} from "../../../utils"; } from "../../../utils";
import { messageIsEmpty } from "../../../utils/messageIsEmpty";
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { InternalPosterPlugin } from "../../InternalPoster/InternalPosterPlugin";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
import { TemplateSafeUser, userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { messageIsEmpty } from "../../../utils/messageIsEmpty";
import { InternalPosterPlugin } from "../../InternalPoster/InternalPosterPlugin";
export const AlertAction = automodAction({ export const AlertAction = automodAction({
configType: t.type({ configType: t.type({
@ -38,7 +36,7 @@ export const AlertAction = automodAction({
const channel = pluginData.guild.channels.cache.get(actionConfig.channel as Snowflake); const channel = pluginData.guild.channels.cache.get(actionConfig.channel as Snowflake);
const logs = pluginData.getPlugin(LogsPlugin); const logs = pluginData.getPlugin(LogsPlugin);
if (channel?.isText()) { if (channel?.isTextBased()) {
const text = actionConfig.text; const text = actionConfig.text;
const theMessageLink = const theMessageLink =
contexts[0].message && messageLink(pluginData.guild.id, contexts[0].message.channel_id, contexts[0].message.id); contexts[0].message && messageLink(pluginData.guild.id, contexts[0].message.channel_id, contexts[0].message.id);
@ -96,7 +94,7 @@ export const AlertAction = automodAction({
const chunks = chunkMessageLines(rendered); const chunks = chunkMessageLines(rendered);
for (const chunk of chunks) { for (const chunk of chunks) {
await poster.sendMessage(channel, { await poster.sendMessage(channel, {
content: rendered, content: chunk,
allowedMentions: erisAllowedMentionsToDjsMentionOptions(actionConfig.allowed_mentions), allowedMentions: erisAllowedMentionsToDjsMentionOptions(actionConfig.allowed_mentions),
}); });
} }

View file

@ -1,4 +1,4 @@
import { ThreadChannel } from "discord.js"; import { AnyThreadChannel } from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { noop } from "../../../utils"; import { noop } from "../../../utils";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
@ -11,7 +11,7 @@ export const ArchiveThreadAction = automodAction({
const threads = contexts const threads = contexts
.filter((c) => c.message?.channel_id) .filter((c) => c.message?.channel_id)
.map((c) => pluginData.guild.channels.cache.get(c.message!.channel_id)) .map((c) => pluginData.guild.channels.cache.get(c.message!.channel_id))
.filter((c): c is ThreadChannel => c?.isThread() ?? false); .filter((c): c is AnyThreadChannel => c?.isThread() ?? false);
for (const thread of threads) { for (const thread of threads) {
await thread.setArchived().catch(noop); await thread.setArchived().catch(noop);

View file

@ -1,5 +1,4 @@
import * as t from "io-ts"; import * as t from "io-ts";
import { LogType } from "../../../data/LogType";
import { nonNullish, unique } from "../../../utils"; import { nonNullish, unique } from "../../../utils";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";

View file

@ -1,20 +1,72 @@
import { Permissions, PermissionString } from "discord.js"; import { PermissionsBitField, PermissionsString } from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { automodAction } from "../helpers";
import { tNullable, isValidSnowflake, tPartialDictionary } from "../../../utils";
import { noop } from "knub/dist/utils";
import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter"; import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter";
import { isValidSnowflake, noop, tNullable, tPartialDictionary } from "../../../utils";
import { import {
guildToTemplateSafeGuild, guildToTemplateSafeGuild,
savedMessageToTemplateSafeSavedMessage, savedMessageToTemplateSafeSavedMessage,
userToTemplateSafeUser, userToTemplateSafeUser,
} from "../../../utils/templateSafeObjects"; } from "../../../utils/templateSafeObjects";
import { automodAction } from "../helpers";
type LegacyPermMap = Record<string, keyof (typeof PermissionsBitField)["Flags"]>;
const legacyPermMap = {
CREATE_INSTANT_INVITE: "CreateInstantInvite",
KICK_MEMBERS: "KickMembers",
BAN_MEMBERS: "BanMembers",
ADMINISTRATOR: "Administrator",
MANAGE_CHANNELS: "ManageChannels",
MANAGE_GUILD: "ManageGuild",
ADD_REACTIONS: "AddReactions",
VIEW_AUDIT_LOG: "ViewAuditLog",
PRIORITY_SPEAKER: "PrioritySpeaker",
STREAM: "Stream",
VIEW_CHANNEL: "ViewChannel",
SEND_MESSAGES: "SendMessages",
SEND_TTSMESSAGES: "SendTTSMessages",
MANAGE_MESSAGES: "ManageMessages",
EMBED_LINKS: "EmbedLinks",
ATTACH_FILES: "AttachFiles",
READ_MESSAGE_HISTORY: "ReadMessageHistory",
MENTION_EVERYONE: "MentionEveryone",
USE_EXTERNAL_EMOJIS: "UseExternalEmojis",
VIEW_GUILD_INSIGHTS: "ViewGuildInsights",
CONNECT: "Connect",
SPEAK: "Speak",
MUTE_MEMBERS: "MuteMembers",
DEAFEN_MEMBERS: "DeafenMembers",
MOVE_MEMBERS: "MoveMembers",
USE_VAD: "UseVAD",
CHANGE_NICKNAME: "ChangeNickname",
MANAGE_NICKNAMES: "ManageNicknames",
MANAGE_ROLES: "ManageRoles",
MANAGE_WEBHOOKS: "ManageWebhooks",
MANAGE_EMOJIS_AND_STICKERS: "ManageEmojisAndStickers",
USE_APPLICATION_COMMANDS: "UseApplicationCommands",
REQUEST_TO_SPEAK: "RequestToSpeak",
MANAGE_EVENTS: "ManageEvents",
MANAGE_THREADS: "ManageThreads",
CREATE_PUBLIC_THREADS: "CreatePublicThreads",
CREATE_PRIVATE_THREADS: "CreatePrivateThreads",
USE_EXTERNAL_STICKERS: "UseExternalStickers",
SEND_MESSAGES_IN_THREADS: "SendMessagesInThreads",
USE_EMBEDDED_ACTIVITIES: "UseEmbeddedActivities",
MODERATE_MEMBERS: "ModerateMembers",
} satisfies LegacyPermMap;
const realToLegacyMap = Object.entries(legacyPermMap).reduce((map, pair) => {
map[pair[1]] = pair[0];
return map;
}, {}) as Record<keyof typeof PermissionsBitField.Flags, keyof typeof legacyPermMap>;
export const ChangePermsAction = automodAction({ export const ChangePermsAction = automodAction({
configType: t.type({ configType: t.type({
target: t.string, target: t.string,
channel: tNullable(t.string), channel: tNullable(t.string),
perms: tPartialDictionary(t.keyof(Permissions.FLAGS), tNullable(t.boolean)), perms: tPartialDictionary(
t.union([t.keyof(PermissionsBitField.Flags), t.keyof(legacyPermMap)]),
tNullable(t.boolean),
),
}), }),
defaultConfig: {}, defaultConfig: {},
@ -52,13 +104,15 @@ export const ChangePermsAction = automodAction({
const channel = pluginData.guild.channels.resolve(channelId); const channel = pluginData.guild.channels.resolve(channelId);
if (!channel || channel.isThread()) return; if (!channel || channel.isThread()) return;
const overwrite = channel.permissionOverwrites.cache.find((pw) => pw.id === target); const overwrite = channel.permissionOverwrites.cache.find((pw) => pw.id === target);
const allow = new Permissions(overwrite?.allow ?? 0n).serialize(); const allow = new PermissionsBitField(overwrite?.allow ?? 0n).serialize();
const deny = new Permissions(overwrite?.deny ?? 0n).serialize(); const deny = new PermissionsBitField(overwrite?.deny ?? 0n).serialize();
const newPerms: Partial<Record<PermissionString, boolean | null>> = {}; const newPerms: Partial<Record<PermissionsString, boolean | null>> = {};
for (const key in allow) { for (const key in allow) {
if (typeof actionConfig.perms[key] !== "undefined") { const legacyKey = realToLegacyMap[key];
newPerms[key] = actionConfig.perms[key]; const configEntry = actionConfig.perms[key] ?? actionConfig.perms[legacyKey];
if (typeof configEntry !== "undefined") {
newPerms[key] = configEntry;
continue; continue;
} }
if (allow[key]) { if (allow[key]) {
@ -86,11 +140,12 @@ export const ChangePermsAction = automodAction({
if (!role) return; if (!role) return;
const perms = new Permissions(role.permissions).serialize(); const perms = new PermissionsBitField(role.permissions).serialize();
for (const key in actionConfig.perms) { for (const key in actionConfig.perms) {
perms[key] = actionConfig.perms[key]; const realKey = legacyPermMap[key] ?? key;
perms[realKey] = actionConfig.perms[key];
} }
const permsArray = <PermissionString[]>Object.keys(perms).filter((key) => perms[key]); const permsArray = <PermissionsString[]>Object.keys(perms).filter((key) => perms[key]);
await role.setPermissions(new Permissions(permsArray)).catch(noop); await role.setPermissions(new PermissionsBitField(permsArray)).catch(noop);
}, },
}); });

View file

@ -1,4 +1,4 @@
import { Snowflake, TextChannel } from "discord.js"; import { GuildTextBasedChannel, Snowflake } from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { LogType } from "../../../data/LogType"; import { LogType } from "../../../data/LogType";
import { noop } from "../../../utils"; import { noop } from "../../../utils";
@ -32,7 +32,7 @@ export const CleanAction = automodAction({
pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, id); pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, id);
} }
const channel = pluginData.guild.channels.cache.get(channelId as Snowflake) as TextChannel; const channel = pluginData.guild.channels.cache.get(channelId as Snowflake) as GuildTextBasedChannel;
await channel.bulkDelete(messageIds as Snowflake[]).catch(noop); await channel.bulkDelete(messageIds as Snowflake[]).catch(noop);
} }
}, },

View file

@ -1,9 +1,7 @@
import * as t from "io-ts"; import * as t from "io-ts";
import { LogType } from "../../../data/LogType"; import { isTruthy, unique } from "../../../utils";
import { isTruthy, stripObjectToScalars, unique } from "../../../utils";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
export const LogAction = automodAction({ export const LogAction = automodAction({
configType: t.boolean, configType: t.boolean,

View file

@ -1,5 +1,4 @@
import * as t from "io-ts"; import * as t from "io-ts";
import { LogType } from "../../../data/LogType";
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError";
import { convertDelayStringToMS, nonNullish, tDelayString, tNullable, unique } from "../../../utils"; import { convertDelayStringToMS, nonNullish, tDelayString, tNullable, unique } from "../../../utils";
import { CaseArgs } from "../../Cases/types"; import { CaseArgs } from "../../Cases/types";

View file

@ -1,6 +1,5 @@
import { Permissions, Snowflake } from "discord.js"; import { PermissionFlagsBits, Snowflake } from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { LogType } from "../../../data/LogType";
import { nonNullish, unique } from "../../../utils"; import { nonNullish, unique } from "../../../utils";
import { canAssignRole } from "../../../utils/canAssignRole"; import { canAssignRole } from "../../../utils/canAssignRole";
import { getMissingPermissions } from "../../../utils/getMissingPermissions"; import { getMissingPermissions } from "../../../utils/getMissingPermissions";
@ -10,7 +9,7 @@ import { LogsPlugin } from "../../Logs/LogsPlugin";
import { ignoreRoleChange } from "../functions/ignoredRoleChanges"; import { ignoreRoleChange } from "../functions/ignoredRoleChanges";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
const p = Permissions.FLAGS; const p = PermissionFlagsBits;
export const RemoveRolesAction = automodAction({ export const RemoveRolesAction = automodAction({
configType: t.array(t.string), configType: t.array(t.string),
@ -21,7 +20,7 @@ export const RemoveRolesAction = automodAction({
const members = unique(contexts.map((c) => c.member).filter(nonNullish)); const members = unique(contexts.map((c) => c.member).filter(nonNullish));
const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!; const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!;
const missingPermissions = getMissingPermissions(me.permissions, p.MANAGE_ROLES); const missingPermissions = getMissingPermissions(me.permissions, p.ManageRoles);
if (missingPermissions) { if (missingPermissions) {
const logs = pluginData.getPlugin(LogsPlugin); const logs = pluginData.getPlugin(LogsPlugin);
logs.logBotAlert({ logs.logBotAlert({

View file

@ -1,6 +1,5 @@
import { MessageOptions, Permissions, Snowflake, TextChannel, ThreadChannel, User } from "discord.js"; import { GuildTextBasedChannel, MessageCreateOptions, PermissionsBitField, Snowflake, User } from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter"; import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter";
import { import {
convertDelayStringToMS, convertDelayStringToMS,
@ -14,10 +13,11 @@ import {
verboseChannelMention, verboseChannelMention,
} from "../../../utils"; } from "../../../utils";
import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions"; import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions";
import { messageIsEmpty } from "../../../utils/messageIsEmpty";
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
import { AutomodContext } from "../types"; import { AutomodContext } from "../types";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { messageIsEmpty } from "../../../utils/messageIsEmpty";
export const ReplyAction = automodAction({ export const ReplyAction = automodAction({
configType: t.union([ configType: t.union([
@ -36,7 +36,7 @@ export const ReplyAction = automodAction({
.filter((c) => c.message?.channel_id) .filter((c) => c.message?.channel_id)
.filter((c) => { .filter((c) => {
const channel = pluginData.guild.channels.cache.get(c.message!.channel_id as Snowflake); const channel = pluginData.guild.channels.cache.get(c.message!.channel_id as Snowflake);
return channel?.isText(); return channel?.isTextBased();
}); });
const contextsByChannelId = contextsWithTextChannels.reduce((map: Map<string, AutomodContext[]>, context) => { const contextsByChannelId = contextsWithTextChannels.reduce((map: Map<string, AutomodContext[]>, context) => {
@ -63,16 +63,16 @@ export const ReplyAction = automodAction({
const formatted = const formatted =
typeof actionConfig === "string" typeof actionConfig === "string"
? await renderReplyText(actionConfig) ? await renderReplyText(actionConfig)
: ((await renderRecursively(actionConfig.text, renderReplyText)) as MessageOptions); : ((await renderRecursively(actionConfig.text, renderReplyText)) as MessageCreateOptions);
if (formatted) { if (formatted) {
const channel = pluginData.guild.channels.cache.get(channelId as Snowflake) as TextChannel; const channel = pluginData.guild.channels.cache.get(channelId as Snowflake) as GuildTextBasedChannel;
// Check for basic Send Messages and View Channel permissions // Check for basic Send Messages and View Channel permissions
if ( if (
!hasDiscordPermissions( !hasDiscordPermissions(
channel.permissionsFor(pluginData.client.user!.id), channel.permissionsFor(pluginData.client.user!.id),
Permissions.FLAGS.SEND_MESSAGES | Permissions.FLAGS.VIEW_CHANNEL, PermissionsBitField.Flags.SendMessages | PermissionsBitField.Flags.ViewChannel,
) )
) { ) {
pluginData.getPlugin(LogsPlugin).logBotAlert({ pluginData.getPlugin(LogsPlugin).logBotAlert({
@ -84,7 +84,10 @@ export const ReplyAction = automodAction({
// If the message is an embed, check for embed permissions // If the message is an embed, check for embed permissions
if ( if (
typeof formatted !== "string" && typeof formatted !== "string" &&
!hasDiscordPermissions(channel.permissionsFor(pluginData.client.user!.id), Permissions.FLAGS.EMBED_LINKS) !hasDiscordPermissions(
channel.permissionsFor(pluginData.client.user!.id),
PermissionsBitField.Flags.EmbedLinks,
)
) { ) {
pluginData.getPlugin(LogsPlugin).logBotAlert({ pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Missing permissions to reply **with an embed** in ${verboseChannelMention( body: `Missing permissions to reply **with an embed** in ${verboseChannelMention(
@ -96,7 +99,7 @@ export const ReplyAction = automodAction({
const messageContent = validateAndParseMessageContent(formatted); const messageContent = validateAndParseMessageContent(formatted);
const messageOpts: MessageOptions = { const messageOpts: MessageCreateOptions = {
...messageContent, ...messageContent,
allowedMentions: { allowedMentions: {
users: [user.id], users: [user.id],
@ -118,7 +121,7 @@ export const ReplyAction = automodAction({
if (typeof actionConfig === "object" && actionConfig.auto_delete) { if (typeof actionConfig === "object" && actionConfig.auto_delete) {
const delay = convertDelayStringToMS(String(actionConfig.auto_delete))!; const delay = convertDelayStringToMS(String(actionConfig.auto_delete))!;
setTimeout(() => !replyMsg.deleted && replyMsg.delete().catch(noop), delay); setTimeout(() => replyMsg.deletable && replyMsg.delete().catch(noop), delay);
} }
} }
} }

View file

@ -1,8 +1,7 @@
import * as t from "io-ts"; import * as t from "io-ts";
import { LogType } from "../../../data/LogType";
import { CountersPlugin } from "../../Counters/CountersPlugin"; import { CountersPlugin } from "../../Counters/CountersPlugin";
import { automodAction } from "../helpers";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
export const SetCounterAction = automodAction({ export const SetCounterAction = automodAction({
configType: t.type({ configType: t.type({

View file

@ -1,10 +1,8 @@
import { Snowflake, TextChannel } from "discord.js"; import { ChannelType, GuildTextBasedChannel, Snowflake } from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { ChannelTypeStrings } from "src/types";
import { LogType } from "../../../data/LogType";
import { convertDelayStringToMS, isDiscordAPIError, tDelayString, tNullable } from "../../../utils"; import { convertDelayStringToMS, isDiscordAPIError, tDelayString, tNullable } from "../../../utils";
import { automodAction } from "../helpers";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers";
export const SetSlowmodeAction = automodAction({ export const SetSlowmodeAction = automodAction({
configType: t.type({ configType: t.type({
@ -23,29 +21,27 @@ export const SetSlowmodeAction = automodAction({
const channel = pluginData.guild.channels.cache.get(channelId as Snowflake); const channel = pluginData.guild.channels.cache.get(channelId as Snowflake);
// Only text channels and text channels within categories support slowmodes // Only text channels and text channels within categories support slowmodes
if (!channel || !(channel.type === ChannelTypeStrings.TEXT || ChannelTypeStrings.CATEGORY)) { if (!channel || (!channel.isTextBased() && channel.type !== ChannelType.GuildCategory)) {
continue; continue;
} }
const channelsToSlowmode: TextChannel[] = []; const channelsToSlowmode: GuildTextBasedChannel[] = [];
if (channel.type === ChannelTypeStrings.CATEGORY) { if (channel.type === ChannelType.GuildCategory) {
// Find all text channels within the category // Find all text channels within the category
for (const ch of pluginData.guild.channels.cache.values()) { for (const ch of pluginData.guild.channels.cache.values()) {
if (ch.parentId === channel.id && ch.type === ChannelTypeStrings.TEXT) { if (ch.parentId === channel.id && ch.type === ChannelType.GuildText) {
channelsToSlowmode.push(ch as TextChannel); channelsToSlowmode.push(ch);
} }
} }
} else { } else {
channelsToSlowmode.push(channel as TextChannel); channelsToSlowmode.push(channel);
} }
const slowmodeSeconds = Math.ceil(slowmodeMs / 1000); const slowmodeSeconds = Math.ceil(slowmodeMs / 1000);
try { try {
for (const chan of channelsToSlowmode) { for (const chan of channelsToSlowmode) {
await chan.edit({ await chan.setRateLimitPerUser(slowmodeSeconds);
rateLimitPerUser: slowmodeSeconds,
});
} }
} catch (e) { } catch (e) {
// Check for invalid form body -> indicates duration was too large // Check for invalid form body -> indicates duration was too large

View file

@ -1,8 +1,12 @@
import { GuildFeature, ThreadAutoArchiveDuration } from "discord-api-types/v9"; import {
import { TextChannel } from "discord.js"; ChannelType,
GuildFeature,
GuildTextThreadCreateOptions,
ThreadAutoArchiveDuration,
ThreadChannel,
} from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter"; import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter";
import { ChannelTypeStrings } from "../../../types";
import { convertDelayStringToMS, MINUTES, noop, tDelayString, tNullable } from "../../../utils"; import { convertDelayStringToMS, MINUTES, noop, tDelayString, tNullable } from "../../../utils";
import { savedMessageToTemplateSafeSavedMessage, userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { savedMessageToTemplateSafeSavedMessage, userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
@ -32,12 +36,11 @@ export const StartThreadAction = automodAction({
const threads = contexts.filter((c) => { const threads = contexts.filter((c) => {
if (!c.message || !c.user) return false; if (!c.message || !c.user) return false;
const channel = pluginData.guild.channels.cache.get(c.message.channel_id); const channel = pluginData.guild.channels.cache.get(c.message.channel_id);
if (channel?.type !== ChannelTypeStrings.TEXT || !channel.isText()) return false; // for some reason the typing here for channel.type defaults to ThreadChannelTypes (?) if (channel?.type !== ChannelType.GuildText || !channel.isTextBased()) return false; // for some reason the typing here for channel.type defaults to ThreadChannelTypes (?)
// check against max threads per channel // check against max threads per channel
if (actionConfig.limit_per_channel && actionConfig.limit_per_channel > 0) { if (actionConfig.limit_per_channel && actionConfig.limit_per_channel > 0) {
const threadCount = channel.threads.cache.filter( const threadCount = channel.threads.cache.filter(
(tr) => (tr) => tr.ownerId === pluginData.client.user!.id && !tr.archived && tr.parentId === channel.id,
tr.ownerId === pluginData.client.user!.id && !tr.deleted && !tr.archived && tr.parentId === channel.id,
).size; ).size;
if (threadCount >= actionConfig.limit_per_channel) return false; if (threadCount >= actionConfig.limit_per_channel) return false;
} }
@ -53,7 +56,9 @@ export const StartThreadAction = automodAction({
: ThreadAutoArchiveDuration.OneHour; : ThreadAutoArchiveDuration.OneHour;
for (const threadContext of threads) { for (const threadContext of threads) {
const channel = pluginData.guild.channels.cache.get(threadContext.message!.channel_id) as TextChannel; const channel = pluginData.guild.channels.cache.get(threadContext.message!.channel_id);
if (!channel || !("threads" in channel) || channel.type === ChannelType.GuildForum) continue;
const renderThreadName = async (str: string) => const renderThreadName = async (str: string) =>
renderTemplate( renderTemplate(
str, str,
@ -63,20 +68,35 @@ export const StartThreadAction = automodAction({
}), }),
); );
const threadName = await renderThreadName(actionConfig.name ?? "{user.tag}s thread"); const threadName = await renderThreadName(actionConfig.name ?? "{user.tag}s thread");
const thread = await channel.threads const threadOptions: GuildTextThreadCreateOptions<unknown> = {
.create({ name: threadName,
name: threadName, autoArchiveDuration: autoArchive,
autoArchiveDuration: autoArchive, startMessage:
type: !actionConfig.private && guild.features.includes(GuildFeature.PrivateThreads)
actionConfig.private && guild.features.includes(GuildFeature.PrivateThreads) ? threadContext.message!.id
? ChannelTypeStrings.PRIVATE_THREAD : undefined,
: ChannelTypeStrings.PUBLIC_THREAD, };
startMessage:
!actionConfig.private && guild.features.includes(GuildFeature.PrivateThreads) let thread: ThreadChannel | undefined;
? threadContext.message!.id if (channel.type === ChannelType.GuildNews) {
: undefined, thread = await channel.threads
}) .create({
.catch(noop); ...threadOptions,
type: ChannelType.AnnouncementThread,
})
.catch(() => undefined);
} else {
thread = await channel.threads
.create({
...threadOptions,
type: actionConfig.private ? ChannelType.PrivateThread : ChannelType.PublicThread,
startMessage:
!actionConfig.private && guild.features.includes(GuildFeature.PrivateThreads)
? threadContext.message!.id
: undefined,
})
.catch(() => undefined);
}
if (actionConfig.slowmode && thread) { if (actionConfig.slowmode && thread) {
const dur = Math.ceil(Math.max(convertDelayStringToMS(actionConfig.slowmode) ?? 0, 0) / 1000); const dur = Math.ceil(Math.max(convertDelayStringToMS(actionConfig.slowmode) ?? 0, 0) / 1000);
if (dur > 0) { if (dur > 0) {

View file

@ -1,9 +1,9 @@
import { typedGuildCommand } from "knub"; import { guildPluginMessageCommand } from "knub";
import { sendSuccessMessage } from "../../../pluginUtils"; import { sendSuccessMessage } from "../../../pluginUtils";
import { setAntiraidLevel } from "../functions/setAntiraidLevel"; import { setAntiraidLevel } from "../functions/setAntiraidLevel";
import { AutomodPluginType } from "../types"; import { AutomodPluginType } from "../types";
export const AntiraidClearCmd = typedGuildCommand<AutomodPluginType>()({ export const AntiraidClearCmd = guildPluginMessageCommand<AutomodPluginType>()({
trigger: ["antiraid clear", "antiraid reset", "antiraid none", "antiraid off"], trigger: ["antiraid clear", "antiraid reset", "antiraid none", "antiraid off"],
permission: "can_set_antiraid", permission: "can_set_antiraid",

View file

@ -1,10 +1,10 @@
import { typedGuildCommand } from "knub"; import { guildPluginMessageCommand } from "knub";
import { commandTypeHelpers as ct } from "../../../commandTypes"; import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
import { setAntiraidLevel } from "../functions/setAntiraidLevel"; import { setAntiraidLevel } from "../functions/setAntiraidLevel";
import { AutomodPluginType } from "../types"; import { AutomodPluginType } from "../types";
export const SetAntiraidCmd = typedGuildCommand<AutomodPluginType>()({ export const SetAntiraidCmd = guildPluginMessageCommand<AutomodPluginType>()({
trigger: "antiraid", trigger: "antiraid",
permission: "can_set_antiraid", permission: "can_set_antiraid",

View file

@ -1,7 +1,7 @@
import { typedGuildCommand } from "knub"; import { guildPluginMessageCommand } from "knub";
import { AutomodPluginType } from "../types"; import { AutomodPluginType } from "../types";
export const ViewAntiraidCmd = typedGuildCommand<AutomodPluginType>()({ export const ViewAntiraidCmd = guildPluginMessageCommand<AutomodPluginType>()({
trigger: "antiraid", trigger: "antiraid",
permission: "can_view_antiraid", permission: "can_view_antiraid",

View file

@ -1,9 +1,9 @@
import { typedGuildEventListener } from "knub"; import { guildPluginEventListener } from "knub";
import { RecentActionType } from "../constants"; import { RecentActionType } from "../constants";
import { runAutomod } from "../functions/runAutomod"; import { runAutomod } from "../functions/runAutomod";
import { AutomodContext, AutomodPluginType } from "../types"; import { AutomodContext, AutomodPluginType } from "../types";
export const RunAutomodOnJoinEvt = typedGuildEventListener<AutomodPluginType>()({ export const RunAutomodOnJoinEvt = guildPluginEventListener<AutomodPluginType>()({
event: "guildMemberAdd", event: "guildMemberAdd",
listener({ pluginData, args: { member } }) { listener({ pluginData, args: { member } }) {
const context: AutomodContext = { const context: AutomodContext = {
@ -26,7 +26,7 @@ export const RunAutomodOnJoinEvt = typedGuildEventListener<AutomodPluginType>()(
}, },
}); });
export const RunAutomodOnLeaveEvt = typedGuildEventListener<AutomodPluginType>()({ export const RunAutomodOnLeaveEvt = guildPluginEventListener<AutomodPluginType>()({
event: "guildMemberRemove", event: "guildMemberRemove",
listener({ pluginData, args: { member } }) { listener({ pluginData, args: { member } }) {
const context: AutomodContext = { const context: AutomodContext = {

View file

@ -1,10 +1,10 @@
import { typedGuildEventListener } from "knub"; import { guildPluginEventListener } from "knub";
import diff from "lodash.difference"; import diff from "lodash.difference";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { runAutomod } from "../functions/runAutomod"; import { runAutomod } from "../functions/runAutomod";
import { AutomodContext, AutomodPluginType } from "../types"; import { AutomodContext, AutomodPluginType } from "../types";
export const RunAutomodOnMemberUpdate = typedGuildEventListener<AutomodPluginType>()({ export const RunAutomodOnMemberUpdate = guildPluginEventListener<AutomodPluginType>()({
event: "guildMemberUpdate", event: "guildMemberUpdate",
listener({ pluginData, args: { oldMember, newMember } }) { listener({ pluginData, args: { oldMember, newMember } }) {
if (!oldMember) return; if (!oldMember) return;

View file

@ -14,7 +14,6 @@ export async function runAutomodOnCounterTrigger(
) { ) {
const user = userId ? await resolveUser(pluginData.client, userId) : undefined; const user = userId ? await resolveUser(pluginData.client, userId) : undefined;
const member = (userId && (await resolveMember(pluginData.client, pluginData.guild, userId))) || undefined; const member = (userId && (await resolveMember(pluginData.client, pluginData.guild, userId))) || undefined;
const prettyCounterName = pluginData.getPlugin(CountersPlugin).getPrettyNameForCounter(counterName); const prettyCounterName = pluginData.getPlugin(CountersPlugin).getPrettyNameForCounter(counterName);
const prettyTriggerName = pluginData const prettyTriggerName = pluginData
.getPlugin(CountersPlugin) .getPlugin(CountersPlugin)

View file

@ -1,13 +1,12 @@
import { Snowflake } from "discord.js";
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { performance } from "perf_hooks";
import { SavedMessage } from "../../../data/entities/SavedMessage"; import { SavedMessage } from "../../../data/entities/SavedMessage";
import { profilingEnabled } from "../../../utils/easyProfiler";
import { addRecentActionsFromMessage } from "../functions/addRecentActionsFromMessage"; import { addRecentActionsFromMessage } from "../functions/addRecentActionsFromMessage";
import { clearRecentActionsForMessage } from "../functions/clearRecentActionsForMessage"; import { clearRecentActionsForMessage } from "../functions/clearRecentActionsForMessage";
import { runAutomod } from "../functions/runAutomod"; import { runAutomod } from "../functions/runAutomod";
import { AutomodContext, AutomodPluginType } from "../types"; import { AutomodContext, AutomodPluginType } from "../types";
import { performance } from "perf_hooks";
import { profilingEnabled } from "../../../utils/easyProfiler";
export async function runAutomodOnMessage( export async function runAutomodOnMessage(
pluginData: GuildPluginData<AutomodPluginType>, pluginData: GuildPluginData<AutomodPluginType>,

View file

@ -1,9 +1,9 @@
import { typedGuildEventListener } from "knub"; import { guildPluginEventListener } from "knub";
import { RecentActionType } from "../constants"; import { RecentActionType } from "../constants";
import { runAutomod } from "../functions/runAutomod"; import { runAutomod } from "../functions/runAutomod";
import { AutomodContext, AutomodPluginType } from "../types"; import { AutomodContext, AutomodPluginType } from "../types";
export const RunAutomodOnThreadCreate = typedGuildEventListener<AutomodPluginType>()({ export const RunAutomodOnThreadCreate = guildPluginEventListener<AutomodPluginType>()({
event: "threadCreate", event: "threadCreate",
async listener({ pluginData, args: { thread } }) { async listener({ pluginData, args: { thread } }) {
const user = thread.ownerId const user = thread.ownerId
@ -32,7 +32,7 @@ export const RunAutomodOnThreadCreate = typedGuildEventListener<AutomodPluginTyp
}, },
}); });
export const RunAutomodOnThreadDelete = typedGuildEventListener<AutomodPluginType>()({ export const RunAutomodOnThreadDelete = guildPluginEventListener<AutomodPluginType>()({
event: "threadDelete", event: "threadDelete",
async listener({ pluginData, args: { thread } }) { async listener({ pluginData, args: { thread } }) {
const user = thread.ownerId const user = thread.ownerId
@ -54,7 +54,7 @@ export const RunAutomodOnThreadDelete = typedGuildEventListener<AutomodPluginTyp
}, },
}); });
export const RunAutomodOnThreadUpdate = typedGuildEventListener<AutomodPluginType>()({ export const RunAutomodOnThreadUpdate = guildPluginEventListener<AutomodPluginType>()({
event: "threadUpdate", event: "threadUpdate",
async listener({ pluginData, args: { oldThread, newThread: thread } }) { async listener({ pluginData, args: { oldThread, newThread: thread } }) {
const user = thread.ownerId const user = thread.ownerId

View file

@ -1,7 +1,7 @@
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { startProfiling } from "../../../utils/easyProfiler";
import { RECENT_ACTION_EXPIRY_TIME } from "../constants"; import { RECENT_ACTION_EXPIRY_TIME } from "../constants";
import { AutomodPluginType } from "../types"; import { AutomodPluginType } from "../types";
import { startProfiling } from "../../../utils/easyProfiler";
export function clearOldRecentActions(pluginData: GuildPluginData<AutomodPluginType>) { export function clearOldRecentActions(pluginData: GuildPluginData<AutomodPluginType>) {
const stopProfiling = startProfiling(pluginData.getKnubInstance().profiler, "automod:fns:clearOldRecentActions"); const stopProfiling = startProfiling(pluginData.getKnubInstance().profiler, "automod:fns:clearOldRecentActions");

View file

@ -1,7 +1,7 @@
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { startProfiling } from "../../../utils/easyProfiler";
import { RECENT_SPAM_EXPIRY_TIME } from "../constants"; import { RECENT_SPAM_EXPIRY_TIME } from "../constants";
import { AutomodPluginType } from "../types"; import { AutomodPluginType } from "../types";
import { startProfiling } from "../../../utils/easyProfiler";
export function clearOldRecentSpam(pluginData: GuildPluginData<AutomodPluginType>) { export function clearOldRecentSpam(pluginData: GuildPluginData<AutomodPluginType>) {
const stopProfiling = startProfiling(pluginData.getKnubInstance().profiler, "automod:fns:clearOldRecentSpam"); const stopProfiling = startProfiling(pluginData.getKnubInstance().profiler, "automod:fns:clearOldRecentSpam");

View file

@ -1,6 +1,6 @@
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { AutomodContext, AutomodPluginType } from "../types";
import { startProfiling } from "../../../utils/easyProfiler"; import { startProfiling } from "../../../utils/easyProfiler";
import { AutomodContext, AutomodPluginType } from "../types";
export function clearRecentActionsForMessage(pluginData: GuildPluginData<AutomodPluginType>, context: AutomodContext) { export function clearRecentActionsForMessage(pluginData: GuildPluginData<AutomodPluginType>, context: AutomodContext) {
const stopProfiling = startProfiling( const stopProfiling = startProfiling(

View file

@ -1,7 +1,7 @@
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { startProfiling } from "../../../utils/easyProfiler";
import { RecentActionType } from "../constants"; import { RecentActionType } from "../constants";
import { AutomodPluginType } from "../types"; import { AutomodPluginType } from "../types";
import { startProfiling } from "../../../utils/easyProfiler";
export function findRecentSpam( export function findRecentSpam(
pluginData: GuildPluginData<AutomodPluginType>, pluginData: GuildPluginData<AutomodPluginType>,

View file

@ -1,10 +1,10 @@
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { SavedMessage } from "../../../data/entities/SavedMessage"; import { SavedMessage } from "../../../data/entities/SavedMessage";
import { startProfiling } from "../../../utils/easyProfiler";
import { RecentActionType } from "../constants"; import { RecentActionType } from "../constants";
import { AutomodPluginType } from "../types"; import { AutomodPluginType } from "../types";
import { getMatchingRecentActions } from "./getMatchingRecentActions"; import { getMatchingRecentActions } from "./getMatchingRecentActions";
import { startProfiling } from "../../../utils/easyProfiler";
export function getMatchingMessageRecentActions( export function getMatchingMessageRecentActions(
pluginData: GuildPluginData<AutomodPluginType>, pluginData: GuildPluginData<AutomodPluginType>,

View file

@ -1,7 +1,7 @@
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { startProfiling } from "../../../utils/easyProfiler";
import { RecentActionType } from "../constants"; import { RecentActionType } from "../constants";
import { AutomodPluginType } from "../types"; import { AutomodPluginType } from "../types";
import { startProfiling } from "../../../utils/easyProfiler";
export function getMatchingRecentActions( export function getMatchingRecentActions(
pluginData: GuildPluginData<AutomodPluginType>, pluginData: GuildPluginData<AutomodPluginType>,

View file

@ -1,4 +1,4 @@
import { Snowflake, TextChannel } from "discord.js"; import { ActivityType, Snowflake } from "discord.js";
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { messageSummary, verboseChannelMention } from "../../../utils"; import { messageSummary, verboseChannelMention } from "../../../utils";
import { AutomodContext, AutomodPluginType } from "../types"; import { AutomodContext, AutomodPluginType } from "../types";
@ -11,13 +11,13 @@ export function getTextMatchPartialSummary(
) { ) {
if (type === "message") { if (type === "message") {
const message = context.message!; const message = context.message!;
const channel = pluginData.guild.channels.cache.get(message.channel_id as Snowflake) as TextChannel; const channel = pluginData.guild.channels.cache.get(message.channel_id as Snowflake);
const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``; const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``;
return `message in ${channelMention}:\n${messageSummary(message)}`; return `message in ${channelMention}:\n${messageSummary(message)}`;
} else if (type === "embed") { } else if (type === "embed") {
const message = context.message!; const message = context.message!;
const channel = pluginData.guild.channels.cache.get(message.channel_id as Snowflake) as TextChannel; const channel = pluginData.guild.channels.cache.get(message.channel_id as Snowflake);
const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``; const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``;
return `message embed in ${channelMention}:\n${messageSummary(message)}`; return `message embed in ${channelMention}:\n${messageSummary(message)}`;
@ -29,6 +29,6 @@ export function getTextMatchPartialSummary(
const visibleName = context.member?.nickname || context.user!.username; const visibleName = context.member?.nickname || context.user!.username;
return `visible name: ${visibleName}`; return `visible name: ${visibleName}`;
} else if (type === "customstatus") { } else if (type === "customstatus") {
return `custom status: ${context.member!.presence?.activities.find((a) => a.type === "CUSTOM")?.name}`; return `custom status: ${context.member!.presence?.activities.find((a) => a.type === ActivityType.Custom)?.name}`;
} }
} }

View file

@ -1,7 +1,8 @@
import { Constants, MessageEmbed } from "discord.js"; import { ActivityType, Embed } from "discord.js";
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { SavedMessage } from "../../../data/entities/SavedMessage"; import { SavedMessage } from "../../../data/entities/SavedMessage";
import { resolveMember } from "../../../utils"; import { resolveMember } from "../../../utils";
import { DeepMutable } from "../../../utils/typeUtils.js";
import { AutomodPluginType } from "../types"; import { AutomodPluginType } from "../types";
type TextTriggerWithMultipleMatchTypes = { type TextTriggerWithMultipleMatchTypes = {
@ -33,7 +34,7 @@ export async function* matchMultipleTextTypesOnMessage(
} }
if (trigger.match_embeds && msg.data.embeds?.length) { if (trigger.match_embeds && msg.data.embeds?.length) {
const copiedEmbed: MessageEmbed = JSON.parse(JSON.stringify(msg.data.embeds[0])); const copiedEmbed: DeepMutable<Embed> = JSON.parse(JSON.stringify(msg.data.embeds[0]));
if (copiedEmbed.video) { if (copiedEmbed.video) {
copiedEmbed.description = ""; // The description is not rendered, hence it doesn't need to be matched copiedEmbed.description = ""; // The description is not rendered, hence it doesn't need to be matched
} }
@ -53,7 +54,7 @@ export async function* matchMultipleTextTypesOnMessage(
} }
for (const activity of member.presence?.activities ?? []) { for (const activity of member.presence?.activities ?? []) {
if (activity.type === Constants.ActivityTypes[4]) { if (activity.type === ActivityType.Custom) {
yield ["customstatus", `${activity.emoji} ${activity.name}`]; yield ["customstatus", `${activity.emoji} ${activity.name}`];
break; break;
} }

View file

@ -1,4 +1,4 @@
import { Snowflake, TextChannel, ThreadChannel } from "discord.js"; import { Snowflake } from "discord.js";
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError";
import { disableUserNotificationStrings, UserNotificationMethod } from "../../../utils"; import { disableUserNotificationStrings, UserNotificationMethod } from "../../../utils";
@ -19,7 +19,7 @@ export function resolveActionContactMethods(
} }
const channel = pluginData.guild.channels.cache.get(actionConfig.notifyChannel as Snowflake); const channel = pluginData.guild.channels.cache.get(actionConfig.notifyChannel as Snowflake);
if (!channel?.isText()) { if (!channel?.isTextBased()) {
throw new RecoverablePluginError(ERRORS.INVALID_USER_NOTIFICATION_CHANNEL); throw new RecoverablePluginError(ERRORS.INVALID_USER_NOTIFICATION_CHANNEL);
} }

View file

@ -1,13 +1,13 @@
import { Snowflake, TextChannel, ThreadChannel } from "discord.js"; import { GuildTextBasedChannel, Snowflake } from "discord.js";
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { performance } from "perf_hooks";
import { calculateBlocking, profilingEnabled } from "../../../utils/easyProfiler";
import { availableActions } from "../actions/availableActions"; import { availableActions } from "../actions/availableActions";
import { CleanAction } from "../actions/clean"; import { CleanAction } from "../actions/clean";
import { AutomodTriggerMatchResult } from "../helpers"; import { AutomodTriggerMatchResult } from "../helpers";
import { availableTriggers } from "../triggers/availableTriggers"; import { availableTriggers } from "../triggers/availableTriggers";
import { AutomodContext, AutomodPluginType } from "../types"; import { AutomodContext, AutomodPluginType } from "../types";
import { checkAndUpdateCooldown } from "./checkAndUpdateCooldown"; import { checkAndUpdateCooldown } from "./checkAndUpdateCooldown";
import { performance } from "perf_hooks";
import { calculateBlocking, profilingEnabled } from "../../../utils/easyProfiler";
export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>, context: AutomodContext) { export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>, context: AutomodContext) {
const userId = context.user?.id || context.member?.id || context.message?.user_id; const userId = context.user?.id || context.member?.id || context.message?.user_id;
@ -18,7 +18,7 @@ export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>,
const channelOrThread = const channelOrThread =
context.channel ?? context.channel ??
(channelIdOrThreadId (channelIdOrThreadId
? (pluginData.guild.channels.cache.get(channelIdOrThreadId as Snowflake) as TextChannel | ThreadChannel) ? (pluginData.guild.channels.cache.get(channelIdOrThreadId as Snowflake) as GuildTextBasedChannel)
: null); : null);
const channelId = channelOrThread?.isThread() ? channelOrThread.parent?.id : channelIdOrThreadId; const channelId = channelOrThread?.isThread() ? channelOrThread.parent?.id : channelIdOrThreadId;
const threadId = channelOrThread?.isThread() ? channelOrThread.id : null; const threadId = channelOrThread?.isThread() ? channelOrThread.id : null;

View file

@ -1,7 +1,5 @@
import { User } from "discord.js"; import { User } from "discord.js";
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { LogType } from "../../../data/LogType";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { runAutomodOnAntiraidLevel } from "../events/runAutomodOnAntiraidLevel"; import { runAutomodOnAntiraidLevel } from "../events/runAutomodOnAntiraidLevel";
import { AutomodPluginType } from "../types"; import { AutomodPluginType } from "../types";

View file

@ -1,6 +1,6 @@
import * as t from "io-ts"; import * as t from "io-ts";
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import { Awaitable } from "knub/dist/utils"; import { Awaitable } from "../../utils/typeUtils";
import { AutomodContext, AutomodPluginType } from "./types"; import { AutomodContext, AutomodPluginType } from "./types";
interface BaseAutomodTriggerMatchResult { interface BaseAutomodTriggerMatchResult {

View file

@ -1,5 +1,6 @@
import { trimPluginDescription } from "../../utils"; import { trimPluginDescription } from "../../utils";
import { ZeppelinGuildPluginBlueprint } from "../ZeppelinPluginBlueprint"; import { ZeppelinGuildPluginBlueprint } from "../ZeppelinPluginBlueprint";
import { ConfigSchema } from "./types";
export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = { export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = {
prettyName: "Automod", prettyName: "Automod",
@ -99,4 +100,5 @@ export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = {
{matchSummary} {matchSummary}
~~~ ~~~
`), `),
configSchema: ConfigSchema,
}; };

View file

@ -1,4 +1,4 @@
import { Snowflake, TextChannel } from "discord.js"; import { Snowflake } from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { verboseChannelMention } from "../../../utils"; import { verboseChannelMention } from "../../../utils";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
@ -22,7 +22,7 @@ export const AnyMessageTrigger = automodTrigger<AnyMessageResultType>()({
}, },
renderMatchInformation({ pluginData, contexts, matchResult }) { renderMatchInformation({ pluginData, contexts, matchResult }) {
const channel = pluginData.guild.channels.cache.get(contexts[0].message!.channel_id as Snowflake) as TextChannel; const channel = pluginData.guild.channels.cache.get(contexts[0].message!.channel_id as Snowflake);
return `Matched message (\`${contexts[0].message!.id}\`) in ${ return `Matched message (\`${contexts[0].message!.id}\`) in ${
channel ? verboseChannelMention(channel) : "Unknown Channel" channel ? verboseChannelMention(channel) : "Unknown Channel"
}`; }`;

View file

@ -11,9 +11,9 @@ import { KickTrigger } from "./kick";
import { LineSpamTrigger } from "./lineSpam"; import { LineSpamTrigger } from "./lineSpam";
import { LinkSpamTrigger } from "./linkSpam"; import { LinkSpamTrigger } from "./linkSpam";
import { MatchAttachmentTypeTrigger } from "./matchAttachmentType"; import { MatchAttachmentTypeTrigger } from "./matchAttachmentType";
import { MatchMimeTypeTrigger } from "./matchMimeType";
import { MatchInvitesTrigger } from "./matchInvites"; import { MatchInvitesTrigger } from "./matchInvites";
import { MatchLinksTrigger } from "./matchLinks"; import { MatchLinksTrigger } from "./matchLinks";
import { MatchMimeTypeTrigger } from "./matchMimeType";
import { MatchRegexTrigger } from "./matchRegex"; import { MatchRegexTrigger } from "./matchRegex";
import { MatchWordsTrigger } from "./matchWords"; import { MatchWordsTrigger } from "./matchWords";
import { MemberJoinTrigger } from "./memberJoin"; import { MemberJoinTrigger } from "./memberJoin";
@ -26,14 +26,14 @@ import { NoteTrigger } from "./note";
import { RoleAddedTrigger } from "./roleAdded"; import { RoleAddedTrigger } from "./roleAdded";
import { RoleRemovedTrigger } from "./roleRemoved"; import { RoleRemovedTrigger } from "./roleRemoved";
import { StickerSpamTrigger } from "./stickerSpam"; import { StickerSpamTrigger } from "./stickerSpam";
import { ThreadArchiveTrigger } from "./threadArchive";
import { ThreadCreateTrigger } from "./threadCreate"; import { ThreadCreateTrigger } from "./threadCreate";
import { ThreadCreateSpamTrigger } from "./threadCreateSpam"; import { ThreadCreateSpamTrigger } from "./threadCreateSpam";
import { ThreadDeleteTrigger } from "./threadDelete"; import { ThreadDeleteTrigger } from "./threadDelete";
import { ThreadUnarchiveTrigger } from "./threadUnarchive";
import { UnbanTrigger } from "./unban"; import { UnbanTrigger } from "./unban";
import { UnmuteTrigger } from "./unmute"; import { UnmuteTrigger } from "./unmute";
import { WarnTrigger } from "./warn"; import { WarnTrigger } from "./warn";
import { ThreadArchiveTrigger } from "./threadArchive";
import { ThreadUnarchiveTrigger } from "./threadUnarchive";
export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>> = { export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>> = {
any_message: AnyMessageTrigger, any_message: AnyMessageTrigger,

View file

@ -1,4 +1,4 @@
import { Snowflake, TextChannel, Util } from "discord.js"; import { escapeInlineCode, Snowflake } from "discord.js";
import * as t from "io-ts"; import * as t from "io-ts";
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils"; import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
@ -66,12 +66,12 @@ export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({
}, },
renderMatchInformation({ pluginData, contexts, matchResult }) { renderMatchInformation({ pluginData, contexts, matchResult }) {
const channel = pluginData.guild.channels.cache.get(contexts[0].message!.channel_id as Snowflake) as TextChannel; const channel = pluginData.guild.channels.cache.get(contexts[0].message!.channel_id as Snowflake)!;
const prettyChannel = verboseChannelMention(channel); const prettyChannel = verboseChannelMention(channel);
return ( return (
asSingleLine(` asSingleLine(`
Matched attachment type \`${Util.escapeInlineCode(matchResult.extra.matchedType)}\` Matched attachment type \`${escapeInlineCode(matchResult.extra.matchedType)}\`
(${matchResult.extra.mode === "blacklist" ? "blacklisted" : "not in whitelist"}) (${matchResult.extra.mode === "blacklist" ? "blacklisted" : "not in whitelist"})
in message (\`${contexts[0].message!.id}\`) in ${prettyChannel}: in message (\`${contexts[0].message!.id}\`) in ${prettyChannel}:
`) + messageSummary(contexts[0].message!) `) + messageSummary(contexts[0].message!)

Some files were not shown because too many files have changed in this diff Show more