mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-06-15 10:45:01 +00:00
Merge branch 'master' into chore/dependabot
This commit is contained in:
commit
e0f06844db
516 changed files with 33887 additions and 10626 deletions
43
.clabot
43
.clabot
|
@ -1,37 +1,40 @@
|
|||
{
|
||||
"contributors": [
|
||||
"BanTheNons",
|
||||
"CleverSource",
|
||||
"DarkView",
|
||||
"DenverCoder1",
|
||||
"Jernik",
|
||||
"Rstar284",
|
||||
"almeidx",
|
||||
"axisiscool",
|
||||
"BanTheNons",
|
||||
"Benricheson101",
|
||||
"brawaru",
|
||||
"CleverSource",
|
||||
"Dalkskkskk",
|
||||
"DarkView",
|
||||
"DenverCoder1",
|
||||
"dexbiobot",
|
||||
"greenbigfrog",
|
||||
"hawkeye7662",
|
||||
"iamshoXy",
|
||||
"Jernik",
|
||||
"k200-1",
|
||||
"LilyBergonzat",
|
||||
"martinbndr",
|
||||
"metal0",
|
||||
"Obliie",
|
||||
"paolojpa",
|
||||
"roflmaoqwerty",
|
||||
"Rstar284",
|
||||
"rubyowo",
|
||||
"rukogit",
|
||||
"Scraayp",
|
||||
"TheKodeToad",
|
||||
"thewilloftheshadow",
|
||||
"usoka",
|
||||
"vcokltfre",
|
||||
"Dragory",
|
||||
"rubyowo",
|
||||
"Dalkskkskk",
|
||||
"iamshoXy",
|
||||
"Scraayp",
|
||||
"app/dependabot",
|
||||
"dependabot[bot]",
|
||||
"WeebHiroyuki",
|
||||
"zayKenyon",
|
||||
"rukogit",
|
||||
"Obliie",
|
||||
"brawaru",
|
||||
"Benricheson101",
|
||||
"hawkeye7662",
|
||||
"LilyBergonzat",
|
||||
"martinbndr"
|
||||
|
||||
"Dragory",
|
||||
"app/dependabot",
|
||||
"dependabot[bot]"
|
||||
],
|
||||
"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!"
|
||||
}
|
||||
|
|
|
@ -22,8 +22,10 @@ STAFF=
|
|||
DEFAULT_ALLOWED_SERVERS=
|
||||
|
||||
# Only required if relevant feature is used
|
||||
#PHISHERMAN_API_KEY=
|
||||
#FISHFISH_API_KEY=
|
||||
|
||||
#DEFAULT_SUCCESS_EMOJI=
|
||||
#DEFAULT_ERROR_EMOJI=
|
||||
|
||||
# ==========================
|
||||
# DEVELOPMENT
|
||||
|
|
2
.github/workflows/codequality.yml
vendored
2
.github/workflows/codequality.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.16]
|
||||
node-version: [22]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
|||
18
|
||||
22
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:20
|
||||
FROM node:22 AS build
|
||||
|
||||
RUN mkdir /zeppelin
|
||||
RUN chown node:node /zeppelin
|
||||
|
@ -32,3 +32,8 @@ RUN npm run build
|
|||
# Prune dev dependencies
|
||||
WORKDIR /zeppelin
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
FROM node:22-alpine AS main
|
||||
|
||||
USER node
|
||||
COPY --from=build --chown=node:node /zeppelin /zeppelin
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
"description": "",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./dist/*"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "tsc-watch --build --onSuccess \"node start-dev.js\"",
|
||||
"watch-yaml-parse-test": "tsc-watch --build --onSuccess \"node dist/yamlParseTest.js\"",
|
||||
|
@ -26,7 +29,7 @@
|
|||
"migrate-rollback-prod": "npm run migrate-rollback",
|
||||
"migrate-rollback-dev": "npm run build && npm run migrate-rollback",
|
||||
"validate-active-configs": "node --enable-source-maps dist/validateActiveConfigs.js > ../config-errors.txt",
|
||||
"export-config-json-schema": "node --enable-source-maps dist/exportSchemas.js > ../config-schema.json",
|
||||
"export-config-json-schema": "node --enable-source-maps dist/exportSchemas.js ../config-checker/public/config-schema.json",
|
||||
"test": "npm run build && npm run run-tests",
|
||||
"run-tests": "ava",
|
||||
"test-watch": "tsc-watch --build --onSuccess \"npx ava\""
|
||||
|
@ -38,18 +41,18 @@
|
|||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"deep-diff": "^1.0.2",
|
||||
"discord.js": "^14.14.1",
|
||||
"discord.js": "^14.19.3",
|
||||
"dotenv": "^4.0.0",
|
||||
"emoji-regex": "^8.0.0",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"express": "^4.20.0",
|
||||
"fp-ts": "^2.0.1",
|
||||
"humanize-duration": "^3.15.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"knub": "^32.0.0-next.21",
|
||||
"js-yaml": "^4.1.0",
|
||||
"knub": "^32.0.0-next.25",
|
||||
"knub-command-manager": "^9.1.0",
|
||||
"last-commit-log": "^2.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment-timezone": "^0.5.21",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.9.8",
|
||||
|
@ -72,15 +75,14 @@
|
|||
"utf-8-validate": "^5.0.5",
|
||||
"uuid": "^9.0.0",
|
||||
"yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build",
|
||||
"zlib-sync": "^0.1.7",
|
||||
"zod": "^3.7.2"
|
||||
"zod": "^3.25.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.5",
|
||||
"@types/express": "^4.16.1",
|
||||
"@types/jest": "^24.0.15",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/lodash.at": "^4.6.3",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/moment-timezone": "^0.5.6",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/passport": "^1.0.0",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { EventEmitter } from "events";
|
||||
import { CooldownManager } from "knub";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { RegExpWorker, TimeoutError } from "regexp-worker";
|
||||
import { MINUTES, SECONDS } from "./utils.js";
|
||||
import Timeout = NodeJS.Timeout;
|
||||
|
|
|
@ -46,7 +46,7 @@ export class SimpleCache<T = any> {
|
|||
});
|
||||
|
||||
if (this.maxItems && this.store.size > this.maxItems) {
|
||||
const keyToDelete = this.store.keys().next().value;
|
||||
const keyToDelete = this.store.keys().next().value!;
|
||||
this.store.delete(keyToDelete);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,130 +1,135 @@
|
|||
import express from "express";
|
||||
import z from "zod";
|
||||
import { guildPlugins } from "../plugins/availablePlugins.js";
|
||||
import { guildPluginInfo } from "../plugins/pluginInfo.js";
|
||||
import z from "zod/v4";
|
||||
import { availableGuildPlugins } from "../plugins/availablePlugins.js";
|
||||
import { ZeppelinGuildPluginInfo } from "../types.js";
|
||||
import { indentLines } from "../utils.js";
|
||||
import { notFound } from "./responses.js";
|
||||
import { $ZodPipeDef } from "zod/v4/core";
|
||||
|
||||
function isZodObject(schema: z.ZodTypeAny): schema is z.ZodObject<any> {
|
||||
return schema._def.typeName === "ZodObject";
|
||||
function isZodObject(schema: z.ZodType): schema is z.ZodObject<any> {
|
||||
return schema.def.type === "object";
|
||||
}
|
||||
|
||||
function isZodRecord(schema: z.ZodTypeAny): schema is z.ZodRecord<any> {
|
||||
return schema._def.typeName === "ZodRecord";
|
||||
function isZodRecord(schema: z.ZodType): schema is z.ZodRecord<any> {
|
||||
return schema.def.type === "record";
|
||||
}
|
||||
|
||||
function isZodEffects(schema: z.ZodTypeAny): schema is z.ZodEffects<any, any> {
|
||||
return schema._def.typeName === "ZodEffects";
|
||||
function isZodOptional(schema: z.ZodType): schema is z.ZodOptional<any> {
|
||||
return schema.def.type === "optional";
|
||||
}
|
||||
|
||||
function isZodOptional(schema: z.ZodTypeAny): schema is z.ZodOptional<any> {
|
||||
return schema._def.typeName === "ZodOptional";
|
||||
function isZodArray(schema: z.ZodType): schema is z.ZodArray<any> {
|
||||
return schema.def.type === "array";
|
||||
}
|
||||
|
||||
function isZodArray(schema: z.ZodTypeAny): schema is z.ZodArray<any> {
|
||||
return schema._def.typeName === "ZodArray";
|
||||
function isZodUnion(schema: z.ZodType): schema is z.ZodUnion<any> {
|
||||
return schema.def.type === "union";
|
||||
}
|
||||
|
||||
function isZodUnion(schema: z.ZodTypeAny): schema is z.ZodUnion<any> {
|
||||
return schema._def.typeName === "ZodUnion";
|
||||
function isZodNullable(schema: z.ZodType): schema is z.ZodNullable<any> {
|
||||
return schema.def.type === "nullable";
|
||||
}
|
||||
|
||||
function isZodNullable(schema: z.ZodTypeAny): schema is z.ZodNullable<any> {
|
||||
return schema._def.typeName === "ZodNullable";
|
||||
function isZodDefault(schema: z.ZodType): schema is z.ZodDefault<any> {
|
||||
return schema.def.type === "default";
|
||||
}
|
||||
|
||||
function isZodDefault(schema: z.ZodTypeAny): schema is z.ZodDefault<any> {
|
||||
return schema._def.typeName === "ZodDefault";
|
||||
function isZodLiteral(schema: z.ZodType): schema is z.ZodLiteral<any> {
|
||||
return schema.def.type === "literal";
|
||||
}
|
||||
|
||||
function isZodLiteral(schema: z.ZodTypeAny): schema is z.ZodLiteral<any> {
|
||||
return schema._def.typeName === "ZodLiteral";
|
||||
function isZodIntersection(schema: z.ZodType): schema is z.ZodIntersection<any, any> {
|
||||
return schema.def.type === "intersection";
|
||||
}
|
||||
|
||||
function isZodIntersection(schema: z.ZodTypeAny): schema is z.ZodIntersection<any, any> {
|
||||
return schema._def.typeName === "ZodIntersection";
|
||||
}
|
||||
|
||||
function formatZodConfigSchema(schema: z.ZodTypeAny) {
|
||||
function formatZodConfigSchema(schema: z.ZodType) {
|
||||
if (isZodObject(schema)) {
|
||||
return (
|
||||
`{\n` +
|
||||
Object.entries(schema._def.shape())
|
||||
.map(([k, value]) => indentLines(`${k}: ${formatZodConfigSchema(value as z.ZodTypeAny)}`, 2))
|
||||
Object.entries(schema.def.shape)
|
||||
.map(([k, value]) => indentLines(`${k}: ${formatZodConfigSchema(value as z.ZodType)}`, 2))
|
||||
.join("\n") +
|
||||
"\n}"
|
||||
);
|
||||
}
|
||||
if (isZodRecord(schema)) {
|
||||
return "{\n" + indentLines(`[string]: ${formatZodConfigSchema(schema._def.valueType)}`, 2) + "\n}";
|
||||
}
|
||||
if (isZodEffects(schema)) {
|
||||
return formatZodConfigSchema(schema._def.schema);
|
||||
return "{\n" + indentLines(`[string]: ${formatZodConfigSchema(schema.valueType as z.ZodType)}`, 2) + "\n}";
|
||||
}
|
||||
if (isZodOptional(schema)) {
|
||||
return `Optional<${formatZodConfigSchema(schema._def.innerType)}>`;
|
||||
return `Optional<${formatZodConfigSchema(schema.def.innerType)}>`;
|
||||
}
|
||||
if (isZodArray(schema)) {
|
||||
return `Array<${formatZodConfigSchema(schema._def.type)}>`;
|
||||
return `Array<${formatZodConfigSchema(schema.def.element)}>`;
|
||||
}
|
||||
if (isZodUnion(schema)) {
|
||||
return schema._def.options.map((t) => formatZodConfigSchema(t)).join(" | ");
|
||||
return schema.def.options.map((t) => formatZodConfigSchema(t)).join(" | ");
|
||||
}
|
||||
if (isZodNullable(schema)) {
|
||||
return `Nullable<${formatZodConfigSchema(schema._def.innerType)}>`;
|
||||
return `Nullable<${formatZodConfigSchema(schema.def.innerType)}>`;
|
||||
}
|
||||
if (isZodDefault(schema)) {
|
||||
return formatZodConfigSchema(schema._def.innerType);
|
||||
return formatZodConfigSchema(schema.def.innerType);
|
||||
}
|
||||
if (isZodLiteral(schema)) {
|
||||
return schema._def.value;
|
||||
return schema.def.values;
|
||||
}
|
||||
if (isZodIntersection(schema)) {
|
||||
return [formatZodConfigSchema(schema._def.left), formatZodConfigSchema(schema._def.right)].join(" & ");
|
||||
return [
|
||||
formatZodConfigSchema(schema.def.left as z.ZodType),
|
||||
formatZodConfigSchema(schema.def.right as z.ZodType),
|
||||
].join(" & ");
|
||||
}
|
||||
if (schema._def.typeName === "ZodString") {
|
||||
if (schema.def.type === "string") {
|
||||
return "string";
|
||||
}
|
||||
if (schema._def.typeName === "ZodNumber") {
|
||||
if (schema.def.type === "number") {
|
||||
return "number";
|
||||
}
|
||||
if (schema._def.typeName === "ZodBoolean") {
|
||||
if (schema.def.type === "boolean") {
|
||||
return "boolean";
|
||||
}
|
||||
if (schema._def.typeName === "ZodNever") {
|
||||
if (schema.def.type === "never") {
|
||||
return "never";
|
||||
}
|
||||
if (schema.def.type === "pipe") {
|
||||
return formatZodConfigSchema((schema.def as $ZodPipeDef).in as z.ZodType);
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const availableGuildPluginsByName = availableGuildPlugins.reduce<Record<string, ZeppelinGuildPluginInfo>>(
|
||||
(map, obj) => {
|
||||
map[obj.plugin.name] = obj;
|
||||
return map;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
export function initDocs(router: express.Router) {
|
||||
const docsPluginNames = Object.keys(guildPluginInfo).filter((k) => guildPluginInfo[k].showInDocs);
|
||||
const docsPlugins = availableGuildPlugins.filter((obj) => obj.docs.type !== "internal");
|
||||
|
||||
router.get("/docs/plugins", (req: express.Request, res: express.Response) => {
|
||||
res.json(
|
||||
docsPluginNames.map((pluginName) => {
|
||||
const info = guildPluginInfo[pluginName];
|
||||
const thinInfo = info ? { prettyName: info.prettyName, legacy: info.legacy ?? false } : {};
|
||||
return {
|
||||
name: pluginName,
|
||||
info: thinInfo,
|
||||
};
|
||||
}),
|
||||
docsPlugins.map((obj) => ({
|
||||
name: obj.plugin.name,
|
||||
info: {
|
||||
prettyName: obj.docs.prettyName,
|
||||
type: obj.docs.type,
|
||||
},
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
router.get("/docs/plugins/:pluginName", (req: express.Request, res: express.Response) => {
|
||||
const name = req.params.pluginName;
|
||||
const baseInfo = guildPluginInfo[name];
|
||||
if (!baseInfo) {
|
||||
const pluginInfo = availableGuildPluginsByName[req.params.pluginName];
|
||||
if (!pluginInfo) {
|
||||
return notFound(res);
|
||||
}
|
||||
|
||||
const plugin = guildPlugins.find((p) => p.name === name)!;
|
||||
const { configSchema, ...info } = baseInfo;
|
||||
const { configSchema, ...info } = pluginInfo.docs;
|
||||
const formattedConfigSchema = formatZodConfigSchema(configSchema);
|
||||
|
||||
const messageCommands = (plugin.messageCommands || []).map((cmd) => ({
|
||||
const messageCommands = (pluginInfo.plugin.messageCommands || []).map((cmd) => ({
|
||||
trigger: cmd.trigger,
|
||||
permission: cmd.permission,
|
||||
signature: cmd.signature,
|
||||
|
@ -133,10 +138,10 @@ export function initDocs(router: express.Router) {
|
|||
config: cmd.config,
|
||||
}));
|
||||
|
||||
const defaultOptions = plugin.defaultOptions || {};
|
||||
const defaultOptions = pluginInfo.docs.configSchema.safeParse({}).data ?? {};
|
||||
|
||||
res.json({
|
||||
name,
|
||||
name: pluginInfo.plugin.name,
|
||||
info,
|
||||
configSchema: formattedConfigSchema,
|
||||
defaultOptions,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
||||
import express, { Request, Response } from "express";
|
||||
import moment from "moment-timezone";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { GuildCases } from "../../data/GuildCases.js";
|
||||
import { Case } from "../../data/entities/Case.js";
|
||||
import { MINUTES } from "../../utils.js";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
||||
import express, { Request, Response } from "express";
|
||||
import jsYaml from "js-yaml";
|
||||
import { YAMLException } from "js-yaml";
|
||||
import moment from "moment-timezone";
|
||||
import { Queue } from "../../Queue.js";
|
||||
import { validateGuildConfig } from "../../configValidator.js";
|
||||
|
@ -15,8 +15,6 @@ import { ObjectAliasError } from "../../utils/validateNoObjectAliases.js";
|
|||
import { hasGuildPermission, requireGuildPermission } from "../permissions.js";
|
||||
import { clientError, ok, serverError, unauthorized } from "../responses.js";
|
||||
|
||||
const YAMLException = jsYaml.YAMLException;
|
||||
|
||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||
const auditLog = new ApiAuditLog();
|
||||
|
||||
|
|
|
@ -28,10 +28,10 @@ app.use(multer().none());
|
|||
|
||||
const rootRouter = express.Router();
|
||||
|
||||
initAuth(app);
|
||||
initGuildsAPI(app);
|
||||
initArchives(app);
|
||||
initDocs(app);
|
||||
initAuth(rootRouter);
|
||||
initGuildsAPI(rootRouter);
|
||||
initArchives(rootRouter);
|
||||
initDocs(rootRouter);
|
||||
|
||||
// Default route
|
||||
rootRouter.get("/", (req, res) => {
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { ConfigValidationError, GuildPluginBlueprint, PluginConfigManager } from "knub";
|
||||
import moment from "moment-timezone";
|
||||
import { ZodError } from "zod";
|
||||
import { guildPlugins } from "./plugins/availablePlugins.js";
|
||||
import { ZeppelinGuildConfig, zZeppelinGuildConfig } from "./types.js";
|
||||
import { BaseConfig, ConfigValidationError, GuildPluginBlueprint, PluginConfigManager } from "knub";
|
||||
import { z, ZodError } from "zod/v4";
|
||||
import { availableGuildPlugins } from "./plugins/availablePlugins.js";
|
||||
import { zZeppelinGuildConfig } from "./types.js";
|
||||
import { formatZodIssue } from "./utils/formatZodIssue.js";
|
||||
|
||||
const pluginNameToPlugin = new Map<string, GuildPluginBlueprint<any, any>>();
|
||||
for (const plugin of guildPlugins) {
|
||||
pluginNameToPlugin.set(plugin.name, plugin);
|
||||
for (const pluginInfo of availableGuildPlugins) {
|
||||
pluginNameToPlugin.set(pluginInfo.plugin.name, pluginInfo.plugin);
|
||||
}
|
||||
|
||||
export async function validateGuildConfig(config: any): Promise<string | null> {
|
||||
|
@ -16,14 +15,7 @@ export async function validateGuildConfig(config: any): Promise<string | null> {
|
|||
return validationResult.error.issues.map(formatZodIssue).join("\n");
|
||||
}
|
||||
|
||||
const guildConfig = config as ZeppelinGuildConfig;
|
||||
|
||||
if (guildConfig.timezone) {
|
||||
const validTimezones = moment.tz.names();
|
||||
if (!validTimezones.includes(guildConfig.timezone)) {
|
||||
return `Invalid timezone: ${guildConfig.timezone}`;
|
||||
}
|
||||
}
|
||||
const guildConfig = config as BaseConfig;
|
||||
|
||||
if (guildConfig.plugins) {
|
||||
for (const [pluginName, pluginOptions] of Object.entries(guildConfig.plugins)) {
|
||||
|
@ -36,15 +28,21 @@ export async function validateGuildConfig(config: any): Promise<string | null> {
|
|||
}
|
||||
|
||||
const plugin = pluginNameToPlugin.get(pluginName)!;
|
||||
const configManager = new PluginConfigManager(plugin.defaultOptions || { config: {} }, pluginOptions, {
|
||||
const configManager = new PluginConfigManager(
|
||||
pluginOptions,
|
||||
{
|
||||
configSchema: plugin.configSchema,
|
||||
defaultOverrides: plugin.defaultOverrides ?? [],
|
||||
levels: {},
|
||||
parser: plugin.configParser,
|
||||
});
|
||||
customOverrideCriteriaFunctions: plugin.customOverrideCriteriaFunctions,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await configManager.init();
|
||||
} catch (err) {
|
||||
if (err instanceof ZodError) {
|
||||
return `${pluginName}: ${err.issues.map(formatZodIssue).join("\n")}`;
|
||||
return `${pluginName}:\n${z.prettifyError(err)}`;
|
||||
}
|
||||
if (err instanceof ConfigValidationError) {
|
||||
return `${pluginName}: ${err.message}`;
|
||||
|
|
173
backend/src/data/FishFish.ts
Normal file
173
backend/src/data/FishFish.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
import z from "zod/v4";
|
||||
import { env } from "../env.js";
|
||||
import { HOURS, MINUTES, SECONDS } from "../utils.js";
|
||||
|
||||
const API_ROOT = "https://api.fishfish.gg/v1";
|
||||
|
||||
const zDomainCategory = z.literal(["safe", "malware", "phishing"]);
|
||||
|
||||
const zDomain = z.object({
|
||||
name: z.string(),
|
||||
category: zDomainCategory,
|
||||
description: z.string(),
|
||||
added: z.number(),
|
||||
checked: z.number(),
|
||||
});
|
||||
export type FishFishDomain = z.output<typeof zDomain>;
|
||||
|
||||
const FULL_REFRESH_INTERVAL = 6 * HOURS;
|
||||
const domains = new Map<string, FishFishDomain>();
|
||||
|
||||
let sessionTokenPromise: Promise<string> | null = null;
|
||||
|
||||
const WS_RECONNECT_DELAY = 30 * SECONDS;
|
||||
let updatesWs: WebSocket | null = null;
|
||||
|
||||
export class FishFishError extends Error {}
|
||||
|
||||
const zTokenResponse = z.object({
|
||||
expires: z.number(),
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
async function getSessionToken(): Promise<string> {
|
||||
if (sessionTokenPromise) {
|
||||
return sessionTokenPromise;
|
||||
}
|
||||
|
||||
const apiKey = env.FISHFISH_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new FishFishError("FISHFISH_API_KEY is missing");
|
||||
}
|
||||
|
||||
sessionTokenPromise = (async () => {
|
||||
const response = await fetch(`${API_ROOT}/users/@me/tokens`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: apiKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new FishFishError(`Failed to get session token: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const parseResult = zTokenResponse.safeParse(await response.json());
|
||||
if (!parseResult.success) {
|
||||
throw new FishFishError(`Parse error when fetching session token: ${parseResult.error.message}`);
|
||||
}
|
||||
|
||||
const timeUntilExpiry = Date.now() - parseResult.data.expires * 1000;
|
||||
setTimeout(() => {
|
||||
sessionTokenPromise = null;
|
||||
}, timeUntilExpiry - 1 * MINUTES); // Subtract a minute to ensure we refresh before expiry
|
||||
|
||||
return parseResult.data.token;
|
||||
})();
|
||||
sessionTokenPromise.catch((err) => {
|
||||
sessionTokenPromise = null;
|
||||
throw err;
|
||||
});
|
||||
|
||||
return sessionTokenPromise;
|
||||
}
|
||||
|
||||
async function fishFishApiCall(method: string, path: string, query: Record<string, string> = {}): Promise<unknown> {
|
||||
const sessionToken = await getSessionToken();
|
||||
const queryParams = new URLSearchParams(query);
|
||||
const response = await fetch(`https://api.fishfish.gg/v1/${path}?${queryParams}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: sessionToken,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new FishFishError(`FishFish API call failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function subscribeToFishFishUpdates(): Promise<void> {
|
||||
if (updatesWs) {
|
||||
return;
|
||||
}
|
||||
const sessionToken = await getSessionToken();
|
||||
console.log("[FISHFISH] Connecting to WebSocket for real-time updates");
|
||||
updatesWs = new WebSocket("wss://api.fishfish.gg/v1/stream", {
|
||||
headers: {
|
||||
Authorization: sessionToken,
|
||||
},
|
||||
});
|
||||
updatesWs.addEventListener("open", () => {
|
||||
console.log("[FISHFISH] WebSocket connection established");
|
||||
});
|
||||
updatesWs.addEventListener("message", (event) => {
|
||||
console.log("[FISHFISH] ws update:", event.data);
|
||||
});
|
||||
updatesWs.addEventListener("error", (error) => {
|
||||
console.error(`[FISHFISH] WebSocket error: ${error.message}`);
|
||||
});
|
||||
updatesWs.addEventListener("close", () => {
|
||||
console.log("[FISHFISH] WebSocket connection closed, reconnecting after delay");
|
||||
updatesWs = null;
|
||||
setTimeout(() => {
|
||||
subscribeToFishFishUpdates();
|
||||
}, WS_RECONNECT_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshFishFishDomains() {
|
||||
const rawData = await fishFishApiCall("GET", "domains", { full: "true" });
|
||||
const parseResult = z.array(zDomain).safeParse(rawData);
|
||||
if (!parseResult.success) {
|
||||
throw new FishFishError(`Parse error when refreshing domains: ${parseResult.error.message}`);
|
||||
}
|
||||
|
||||
domains.clear();
|
||||
for (const domain of parseResult.data) {
|
||||
domains.set(domain.name, domain);
|
||||
}
|
||||
|
||||
domains.set("malware-link.test.zeppelin.gg", {
|
||||
name: "malware-link.test.zeppelin.gg",
|
||||
category: "malware",
|
||||
description: "",
|
||||
added: Date.now(),
|
||||
checked: Date.now(),
|
||||
});
|
||||
domains.set("phishing-link.test.zeppelin.gg", {
|
||||
name: "phishing-link.test.zeppelin.gg",
|
||||
category: "phishing",
|
||||
description: "",
|
||||
added: Date.now(),
|
||||
checked: Date.now(),
|
||||
});
|
||||
domains.set("safe-link.test.zeppelin.gg", {
|
||||
name: "safe-link.test.zeppelin.gg",
|
||||
category: "safe",
|
||||
description: "",
|
||||
added: Date.now(),
|
||||
checked: Date.now(),
|
||||
});
|
||||
|
||||
console.log("[FISHFISH] Refreshed FishFish domains, total count:", domains.size);
|
||||
}
|
||||
|
||||
export async function initFishFish() {
|
||||
if (!env.FISHFISH_API_KEY) {
|
||||
console.warn("[FISHFISH] FISHFISH_API_KEY is not set, FishFish functionality will be disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
await refreshFishFishDomains();
|
||||
void subscribeToFishFishUpdates();
|
||||
setInterval(() => refreshFishFishDomains(), FULL_REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
export function getFishFishDomain(domain: string): FishFishDomain | undefined {
|
||||
return domains.get(domain.toLowerCase());
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { In, InsertResult, Repository } from "typeorm";
|
||||
import { FindOptionsWhere, In, InsertResult, Repository } from "typeorm";
|
||||
import { Queue } from "../Queue.js";
|
||||
import { chunkArray } from "../utils.js";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository.js";
|
||||
|
@ -73,34 +73,69 @@ export class GuildCases extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async getByUserId(userId: string): Promise<Case[]> {
|
||||
async getByUserId(
|
||||
userId: string,
|
||||
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "user_id"> = {},
|
||||
): Promise<Case[]> {
|
||||
return this.cases.find({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
...filters,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getRecentByUserId(userId: string, count: number, skip = 0): Promise<Case[]> {
|
||||
return this.cases.find({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
skip,
|
||||
take: count,
|
||||
order: {
|
||||
case_number: "DESC",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getTotalCasesByModId(modId: string): Promise<number> {
|
||||
async getTotalCasesByModId(
|
||||
modId: string,
|
||||
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "mod_id" | "is_hidden"> = {},
|
||||
): Promise<number> {
|
||||
return this.cases.count({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
mod_id: modId,
|
||||
is_hidden: false,
|
||||
...filters,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getRecentByModId(modId: string, count: number, skip = 0): Promise<Case[]> {
|
||||
return this.cases.find({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
async getRecentByModId(
|
||||
modId: string,
|
||||
count: number,
|
||||
skip = 0,
|
||||
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "mod_id"> = {},
|
||||
): Promise<Case[]> {
|
||||
const where: FindOptionsWhere<Case> = {
|
||||
guild_id: this.guildId,
|
||||
mod_id: modId,
|
||||
is_hidden: false,
|
||||
},
|
||||
...filters,
|
||||
};
|
||||
|
||||
if (where.is_hidden === true) {
|
||||
delete where.is_hidden;
|
||||
}
|
||||
|
||||
return this.cases.find({
|
||||
relations: this.getRelations(),
|
||||
where,
|
||||
skip,
|
||||
take: count,
|
||||
order: {
|
||||
|
|
|
@ -1,253 +0,0 @@
|
|||
import crypto from "crypto";
|
||||
import moment from "moment-timezone";
|
||||
import { Repository } from "typeorm";
|
||||
import { env } from "../env.js";
|
||||
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils.js";
|
||||
import { dataSource } from "./dataSource.js";
|
||||
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry.js";
|
||||
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry.js";
|
||||
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman.js";
|
||||
|
||||
const API_URL = "https://api.phisherman.gg";
|
||||
const MASTER_API_KEY = env.PHISHERMAN_API_KEY;
|
||||
|
||||
let caughtDomainTrackingMap: Map<string, Map<string, number[]>> = new Map();
|
||||
|
||||
const pendingApiRequests: Map<string, Promise<unknown>> = new Map();
|
||||
const pendingDomainInfoChecks: Map<string, Promise<PhishermanDomainInfo | null>> = new Map();
|
||||
|
||||
type MemoryCacheEntry = {
|
||||
info: PhishermanDomainInfo | null;
|
||||
expires: number;
|
||||
};
|
||||
const memoryCache: Map<string, MemoryCacheEntry> = new Map();
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of memoryCache.entries()) {
|
||||
if (entry.expires <= now) {
|
||||
memoryCache.delete(key);
|
||||
}
|
||||
}
|
||||
}, 2 * MINUTES);
|
||||
|
||||
const UNKNOWN_DOMAIN_CACHE_LIFETIME = 2 * MINUTES;
|
||||
const DETECTED_DOMAIN_CACHE_LIFETIME = 15 * MINUTES;
|
||||
const SAFE_DOMAIN_CACHE_LIFETIME = 7 * DAYS;
|
||||
|
||||
const KEY_VALIDITY_LIFETIME = 24 * HOURS;
|
||||
|
||||
let cacheRepository: Repository<PhishermanCacheEntry> | null = null;
|
||||
function getCacheRepository(): Repository<PhishermanCacheEntry> {
|
||||
if (cacheRepository == null) {
|
||||
cacheRepository = dataSource.getRepository(PhishermanCacheEntry);
|
||||
}
|
||||
return cacheRepository;
|
||||
}
|
||||
|
||||
let keyCacheRepository: Repository<PhishermanKeyCacheEntry> | null = null;
|
||||
function getKeyCacheRepository(): Repository<PhishermanKeyCacheEntry> {
|
||||
if (keyCacheRepository == null) {
|
||||
keyCacheRepository = dataSource.getRepository(PhishermanKeyCacheEntry);
|
||||
}
|
||||
return keyCacheRepository;
|
||||
}
|
||||
|
||||
class PhishermanApiError extends Error {
|
||||
method: string;
|
||||
url: string;
|
||||
status: number;
|
||||
|
||||
constructor(method: string, url: string, status: number, message: string) {
|
||||
super(message);
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Error ${this.status} in ${this.method} ${this.url}: ${this.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasPhishermanMasterAPIKey() {
|
||||
return MASTER_API_KEY != null && MASTER_API_KEY !== "";
|
||||
}
|
||||
|
||||
export function phishermanDomainIsSafe(info: PhishermanDomainInfo): boolean {
|
||||
return info.classification === "safe";
|
||||
}
|
||||
|
||||
const leadingSlashRegex = /^\/+/g;
|
||||
function trimLeadingSlash(str: string): string {
|
||||
return str.replace(leadingSlashRegex, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an arbitrary API call to the Phisherman API
|
||||
*/
|
||||
async function apiCall<T>(
|
||||
method: "GET" | "POST",
|
||||
resource: string,
|
||||
payload?: Record<string, unknown> | null,
|
||||
): Promise<T> {
|
||||
if (!hasPhishermanMasterAPIKey()) {
|
||||
throw new Error("Phisherman master API key missing");
|
||||
}
|
||||
|
||||
const url = `${API_URL}/${trimLeadingSlash(resource)}`;
|
||||
const key = `${method} ${url}`;
|
||||
|
||||
if (pendingApiRequests.has(key)) {
|
||||
return pendingApiRequests.get(key)! as Promise<T>;
|
||||
}
|
||||
|
||||
let requestPromise = (async () => {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${MASTER_API_KEY}`,
|
||||
}),
|
||||
body: payload ? JSON.stringify(payload) : undefined,
|
||||
});
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!response.ok || (data as any)?.success === false) {
|
||||
throw new PhishermanApiError(method, url, response.status, (data as any)?.message ?? "");
|
||||
}
|
||||
return data;
|
||||
})();
|
||||
requestPromise = requestPromise.finally(() => {
|
||||
pendingApiRequests.delete(key);
|
||||
});
|
||||
pendingApiRequests.set(key, requestPromise);
|
||||
return requestPromise as Promise<T>;
|
||||
}
|
||||
|
||||
type DomainInfoApiCallResult = PhishermanUnknownDomain | PhishermanDomainInfo;
|
||||
async function fetchDomainInfo(domain: string): Promise<PhishermanDomainInfo | null> {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`[PHISHERMAN] Requesting domain information: ${domain}`);
|
||||
const result = await apiCall<Record<string, DomainInfoApiCallResult>>("GET", `/v2/domains/info/${domain}`);
|
||||
const firstKey = Object.keys(result)[0];
|
||||
const domainInfo = firstKey ? result[firstKey] : null;
|
||||
if (!domainInfo) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(`Unexpected Phisherman API response for ${domain}:`, result);
|
||||
return null;
|
||||
}
|
||||
if (domainInfo.classification === "unknown") {
|
||||
return null;
|
||||
}
|
||||
return domainInfo;
|
||||
}
|
||||
|
||||
export async function getPhishermanDomainInfo(domain: string): Promise<PhishermanDomainInfo | null> {
|
||||
if (pendingDomainInfoChecks.has(domain)) {
|
||||
return pendingDomainInfoChecks.get(domain)!;
|
||||
}
|
||||
|
||||
let promise = (async () => {
|
||||
if (memoryCache.has(domain)) {
|
||||
return memoryCache.get(domain)!.info;
|
||||
}
|
||||
|
||||
const dbCache = getCacheRepository();
|
||||
const existingCachedEntry = await dbCache.findOne({
|
||||
where: { domain },
|
||||
});
|
||||
if (existingCachedEntry) {
|
||||
return existingCachedEntry.data;
|
||||
}
|
||||
|
||||
const freshData = await fetchDomainInfo(domain);
|
||||
const expiryTime =
|
||||
freshData === null
|
||||
? UNKNOWN_DOMAIN_CACHE_LIFETIME
|
||||
: phishermanDomainIsSafe(freshData)
|
||||
? SAFE_DOMAIN_CACHE_LIFETIME
|
||||
: DETECTED_DOMAIN_CACHE_LIFETIME;
|
||||
memoryCache.set(domain, {
|
||||
info: freshData,
|
||||
expires: Date.now() + expiryTime,
|
||||
});
|
||||
|
||||
if (freshData) {
|
||||
// Database cache only stores safe/detected domains, not unknown ones
|
||||
await dbCache.insert({
|
||||
domain,
|
||||
data: freshData,
|
||||
expires_at: moment().add(expiryTime, "ms").format(DBDateFormat),
|
||||
});
|
||||
}
|
||||
|
||||
return freshData;
|
||||
})();
|
||||
promise = promise.finally(() => {
|
||||
pendingDomainInfoChecks.delete(domain);
|
||||
});
|
||||
pendingDomainInfoChecks.set(domain, promise);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function phishermanApiKeyIsValid(apiKey: string): Promise<boolean> {
|
||||
if (apiKey === MASTER_API_KEY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const keyCache = getKeyCacheRepository();
|
||||
const hash = crypto.createHash("sha256").update(apiKey).digest("hex");
|
||||
const entry = await keyCache.findOne({
|
||||
where: { hash },
|
||||
});
|
||||
if (entry) {
|
||||
return entry.is_valid;
|
||||
}
|
||||
|
||||
const { valid: isValid } = await apiCall<{ valid: boolean }>("POST", "/zeppelin/check-key", { apiKey });
|
||||
|
||||
await keyCache.insert({
|
||||
hash,
|
||||
is_valid: isValid,
|
||||
expires_at: moment().add(KEY_VALIDITY_LIFETIME, "ms").format(DBDateFormat),
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
export function trackPhishermanCaughtDomain(apiKey: string, domain: string) {
|
||||
if (!caughtDomainTrackingMap.has(apiKey)) {
|
||||
caughtDomainTrackingMap.set(apiKey, new Map());
|
||||
}
|
||||
const apiKeyMap = caughtDomainTrackingMap.get(apiKey)!;
|
||||
if (!apiKeyMap.has(domain)) {
|
||||
apiKeyMap.set(domain, []);
|
||||
}
|
||||
const timestamps = apiKeyMap.get(domain)!;
|
||||
timestamps.push(Date.now());
|
||||
}
|
||||
|
||||
export async function reportTrackedDomainsToPhisherman() {
|
||||
const result = {};
|
||||
for (const [apiKey, domains] of caughtDomainTrackingMap.entries()) {
|
||||
result[apiKey] = {};
|
||||
for (const [domain, timestamps] of domains.entries()) {
|
||||
result[apiKey][domain] = timestamps;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(result).length > 0) {
|
||||
await apiCall("POST", "/v2/phish/caught/bulk", result);
|
||||
caughtDomainTrackingMap = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteStalePhishermanCacheEntries() {
|
||||
await getCacheRepository().createQueryBuilder().where("expires_at <= NOW()").delete().execute();
|
||||
}
|
||||
|
||||
export async function deleteStalePhishermanKeyCacheEntries() {
|
||||
await getKeyCacheRepository().createQueryBuilder().where("expires_at <= NOW()").delete().execute();
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
import { PhishermanDomainInfo } from "../types/phisherman.js";
|
||||
|
||||
@Entity("phisherman_cache")
|
||||
export class PhishermanCacheEntry {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
domain: string;
|
||||
|
||||
@Column("simple-json")
|
||||
data: PhishermanDomainInfo;
|
||||
|
||||
@Column()
|
||||
expires_at: string;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("phisherman_key_cache")
|
||||
export class PhishermanKeyCacheEntry {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
hash: string;
|
||||
|
||||
@Column()
|
||||
is_valid: boolean;
|
||||
|
||||
@Column()
|
||||
expires_at: string;
|
||||
}
|
|
@ -16,7 +16,7 @@ function muteToKey(mute: Mute) {
|
|||
return `${mute.guild_id}/${mute.user_id}`;
|
||||
}
|
||||
|
||||
async function broadcastExpiredMute(guildId: string, userId: string, tries = 0) {
|
||||
async function broadcastExpiredMute(guildId: string, userId: string, tries = 0): Promise<void> {
|
||||
const mute = await getMutesRepository().findMute(guildId, userId);
|
||||
if (!mute) {
|
||||
// Mute was already cleared
|
||||
|
@ -27,7 +27,7 @@ async function broadcastExpiredMute(guildId: string, userId: string, tries = 0)
|
|||
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 there are no listeners registered for the server yet, try again in a bit
|
||||
if (tries < MAX_TRIES_PER_SERVER) {
|
||||
|
@ -42,7 +42,7 @@ async function broadcastExpiredMute(guildId: string, userId: string, tries = 0)
|
|||
}
|
||||
|
||||
function broadcastTimeoutMuteToRenew(mute: Mute, tries = 0) {
|
||||
console.log(`[EXPIRING MUTES LOOP] Broadcasting timeout mute to renew: ${mute.guild_id}/${mute.user_id}`);
|
||||
// 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) {
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
// tslint:disable:no-console
|
||||
|
||||
import { MINUTES } from "../../utils.js";
|
||||
import {
|
||||
deleteStalePhishermanCacheEntries,
|
||||
deleteStalePhishermanKeyCacheEntries,
|
||||
reportTrackedDomainsToPhisherman,
|
||||
} from "../Phisherman.js";
|
||||
|
||||
const CACHE_CLEANUP_LOOP_INTERVAL = 15 * MINUTES;
|
||||
const REPORT_LOOP_INTERVAL = 15 * MINUTES;
|
||||
|
||||
export async function runPhishermanCacheCleanupLoop() {
|
||||
console.log("[PHISHERMAN] Deleting stale cache entries");
|
||||
await deleteStalePhishermanCacheEntries().catch((err) => console.warn(err));
|
||||
|
||||
console.log("[PHISHERMAN] Deleting stale key cache entries");
|
||||
await deleteStalePhishermanKeyCacheEntries().catch((err) => console.warn(err));
|
||||
|
||||
setTimeout(() => runPhishermanCacheCleanupLoop(), CACHE_CLEANUP_LOOP_INTERVAL);
|
||||
}
|
||||
|
||||
export async function runPhishermanReportingLoop() {
|
||||
console.log("[PHISHERMAN] Reporting tracked domains");
|
||||
await reportTrackedDomainsToPhisherman().catch((err) => console.warn(err));
|
||||
|
||||
setTimeout(() => runPhishermanReportingLoop(), REPORT_LOOP_INTERVAL);
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
export interface PhishermanUnknownDomain {
|
||||
classification: "unknown";
|
||||
}
|
||||
|
||||
export interface PhishermanDomainInfo {
|
||||
status: string;
|
||||
lastChecked: string;
|
||||
verifiedPhish: boolean;
|
||||
classification: "safe" | "malicious";
|
||||
created: string;
|
||||
firstSeen: string | null;
|
||||
lastSeen: string | null;
|
||||
targetedBrand: string;
|
||||
phishCaught: number;
|
||||
details: PhishermanDomainInfoDetails;
|
||||
}
|
||||
|
||||
export interface PhishermanDomainInfoDetails {
|
||||
phishTankId: string | null;
|
||||
urlScanId: string;
|
||||
websiteScreenshot: string;
|
||||
ip_address: string;
|
||||
asn: PhishermanDomainInfoAsn;
|
||||
registry: string;
|
||||
country: string;
|
||||
}
|
||||
|
||||
export interface PhishermanDomainInfoAsn {
|
||||
asn: string;
|
||||
asn_name: string;
|
||||
route: string;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import dotenv from "dotenv";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { rootDir } from "./paths.js";
|
||||
|
||||
const envType = z.object({
|
||||
|
@ -37,6 +37,10 @@ const envType = z.object({
|
|||
.optional(),
|
||||
|
||||
PHISHERMAN_API_KEY: z.string().optional(),
|
||||
FISHFISH_API_KEY: z.string().optional(),
|
||||
|
||||
DEFAULT_SUCCESS_EMOJI: z.string().optional().default("✅"),
|
||||
DEFAULT_ERROR_EMOJI: z.string().optional().default("❌"),
|
||||
|
||||
DB_HOST: z.string().optional(),
|
||||
DB_PORT: z.preprocess((v) => Number(v), z.number()).optional(),
|
||||
|
|
|
@ -1,23 +1,91 @@
|
|||
import { z } from "zod";
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
import { guildPluginInfo } from "./plugins/pluginInfo.js";
|
||||
import fs from "node:fs";
|
||||
import { z } from "zod/v4";
|
||||
import { availableGuildPlugins } from "./plugins/availablePlugins.js";
|
||||
import { zZeppelinGuildConfig } from "./types.js";
|
||||
import { deepPartial } from "./utils/zodDeepPartial.js";
|
||||
|
||||
const pluginSchemaMap = Object.entries(guildPluginInfo).reduce((map, [pluginName, pluginInfo]) => {
|
||||
if (pluginInfo.configSchema) {
|
||||
map[pluginName] = pluginInfo.configSchema;
|
||||
const basePluginOverrideCriteriaSchema = z.strictObject({
|
||||
channel: z
|
||||
.union([z.string(), z.array(z.string())])
|
||||
.nullable()
|
||||
.optional(),
|
||||
category: z
|
||||
.union([z.string(), z.array(z.string())])
|
||||
.nullable()
|
||||
.optional(),
|
||||
level: z
|
||||
.union([z.string(), z.array(z.string())])
|
||||
.nullable()
|
||||
.optional(),
|
||||
user: z
|
||||
.union([z.string(), z.array(z.string())])
|
||||
.nullable()
|
||||
.optional(),
|
||||
role: z
|
||||
.union([z.string(), z.array(z.string())])
|
||||
.nullable()
|
||||
.optional(),
|
||||
thread: z
|
||||
.union([z.string(), z.array(z.string())])
|
||||
.nullable()
|
||||
.optional(),
|
||||
is_thread: z.boolean().nullable().optional(),
|
||||
thread_type: z.literal(["public", "private"]).nullable().optional(),
|
||||
extra: z.any().optional(),
|
||||
});
|
||||
|
||||
const pluginOverrideCriteriaSchema = basePluginOverrideCriteriaSchema.extend({
|
||||
get zzz_dummy_property_do_not_use() {
|
||||
return pluginOverrideCriteriaSchema.optional();
|
||||
},
|
||||
get all() {
|
||||
return z.array(pluginOverrideCriteriaSchema).optional();
|
||||
},
|
||||
get any() {
|
||||
return z.array(pluginOverrideCriteriaSchema).optional();
|
||||
},
|
||||
get not() {
|
||||
return pluginOverrideCriteriaSchema.optional();
|
||||
},
|
||||
}).meta({
|
||||
id: "overrideCriteria",
|
||||
});
|
||||
|
||||
const outputPath = process.argv[2];
|
||||
if (!outputPath) {
|
||||
console.error("Output path required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const partialConfigs = new Map<any, z.ZodType>();
|
||||
function getPartialConfig(configSchema: z.ZodType) {
|
||||
if (!partialConfigs.has(configSchema)) {
|
||||
partialConfigs.set(configSchema, deepPartial(configSchema));
|
||||
}
|
||||
return partialConfigs.get(configSchema)!;
|
||||
}
|
||||
|
||||
function overrides(configSchema: z.ZodType): z.ZodType {
|
||||
const partialConfig = getPartialConfig(configSchema);
|
||||
return pluginOverrideCriteriaSchema.extend({
|
||||
config: partialConfig,
|
||||
});
|
||||
}
|
||||
|
||||
const pluginSchemaMap = availableGuildPlugins.reduce((map, pluginInfo) => {
|
||||
map[pluginInfo.plugin.name] = z.object({
|
||||
config: pluginInfo.docs.configSchema.optional(),
|
||||
overrides: z.array(overrides(pluginInfo.docs.configSchema)).optional(),
|
||||
});
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const fullSchema = zZeppelinGuildConfig.omit({ plugins: true }).merge(
|
||||
z.strictObject({
|
||||
plugins: z.strictObject(pluginSchemaMap).partial(),
|
||||
}),
|
||||
);
|
||||
const fullSchema = zZeppelinGuildConfig.omit({ plugins: true }).extend({
|
||||
plugins: z.strictObject(pluginSchemaMap).partial().optional(),
|
||||
});
|
||||
|
||||
const jsonSchema = zodToJsonSchema(fullSchema);
|
||||
const jsonSchema = z.toJSONSchema(fullSchema, { io: "input", cycles: "ref" });
|
||||
|
||||
console.log(JSON.stringify(jsonSchema, null, 2));
|
||||
fs.writeFileSync(outputPath, JSON.stringify(jsonSchema, null, 2), { encoding: "utf8" });
|
||||
|
||||
process.exit(0);
|
||||
|
|
34
backend/src/humanizeDuration.ts
Normal file
34
backend/src/humanizeDuration.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import humanizeduration from "humanize-duration";
|
||||
|
||||
export const delayStringMultipliers = {
|
||||
y: 1000 * 60 * 60 * 24 * (365 + 1 / 4 - 1 / 100 + 1 / 400),
|
||||
mo: (1000 * 60 * 60 * 24 * (365 + 1 / 4 - 1 / 100 + 1 / 400)) / 12,
|
||||
w: 1000 * 60 * 60 * 24 * 7,
|
||||
d: 1000 * 60 * 60 * 24,
|
||||
h: 1000 * 60 * 60,
|
||||
m: 1000 * 60,
|
||||
s: 1000,
|
||||
x: 1,
|
||||
};
|
||||
|
||||
export const humanizeDurationShort = humanizeduration.humanizer({
|
||||
language: "shortEn",
|
||||
languages: {
|
||||
shortEn: {
|
||||
y: () => "y",
|
||||
mo: () => "mo",
|
||||
w: () => "w",
|
||||
d: () => "d",
|
||||
h: () => "h",
|
||||
m: () => "m",
|
||||
s: () => "s",
|
||||
ms: () => "ms",
|
||||
},
|
||||
},
|
||||
spacer: "",
|
||||
unitMeasures: delayStringMultipliers,
|
||||
});
|
||||
|
||||
export const humanizeDuration = humanizeduration.humanizer({
|
||||
unitMeasures: delayStringMultipliers,
|
||||
});
|
|
@ -1,18 +0,0 @@
|
|||
import humanizeDuration from "humanize-duration";
|
||||
|
||||
export const humanizeDurationShort = humanizeDuration.humanizer({
|
||||
language: "shortEn",
|
||||
languages: {
|
||||
shortEn: {
|
||||
y: () => "y",
|
||||
mo: () => "mo",
|
||||
w: () => "w",
|
||||
d: () => "d",
|
||||
h: () => "h",
|
||||
m: () => "m",
|
||||
s: () => "s",
|
||||
ms: () => "ms",
|
||||
},
|
||||
},
|
||||
spacer: "",
|
||||
});
|
|
@ -12,7 +12,6 @@ import {
|
|||
TextChannel,
|
||||
ThreadChannel,
|
||||
} from "discord.js";
|
||||
import { EventEmitter } from "events";
|
||||
import { Knub, PluginError, PluginLoadError, PluginNotLoadedError } from "knub";
|
||||
import moment from "moment-timezone";
|
||||
import { performance } from "perf_hooks";
|
||||
|
@ -22,9 +21,9 @@ import { RecoverablePluginError } from "./RecoverablePluginError.js";
|
|||
import { SimpleError } from "./SimpleError.js";
|
||||
import { AllowedGuilds } from "./data/AllowedGuilds.js";
|
||||
import { Configs } from "./data/Configs.js";
|
||||
import { FishFishError, initFishFish } from "./data/FishFish.js";
|
||||
import { GuildLogs } from "./data/GuildLogs.js";
|
||||
import { LogType } from "./data/LogType.js";
|
||||
import { hasPhishermanMasterAPIKey } from "./data/Phisherman.js";
|
||||
import { dataSource } from "./data/dataSource.js";
|
||||
import { connect } from "./data/db.js";
|
||||
import { runExpiredArchiveDeletionLoop } from "./data/loops/expiredArchiveDeletionLoop.js";
|
||||
|
@ -33,14 +32,13 @@ import { runExpiringMutesLoop } from "./data/loops/expiringMutesLoop.js";
|
|||
import { runExpiringTempbansLoop } from "./data/loops/expiringTempbansLoop.js";
|
||||
import { runExpiringVCAlertsLoop } from "./data/loops/expiringVCAlertsLoop.js";
|
||||
import { runMemberCacheDeletionLoop } from "./data/loops/memberCacheDeletionLoop.js";
|
||||
import { runPhishermanCacheCleanupLoop, runPhishermanReportingLoop } from "./data/loops/phishermanLoops.js";
|
||||
import { runSavedMessageCleanupLoop } from "./data/loops/savedMessageCleanupLoop.js";
|
||||
import { runUpcomingRemindersLoop } from "./data/loops/upcomingRemindersLoop.js";
|
||||
import { runUpcomingScheduledPostsLoop } from "./data/loops/upcomingScheduledPostsLoop.js";
|
||||
import { consumeQueryStats } from "./data/queryLogger.js";
|
||||
import { env } from "./env.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { baseGuildPlugins, globalPlugins, guildPlugins } from "./plugins/availablePlugins.js";
|
||||
import { availableGlobalPlugins, availableGuildPlugins } from "./plugins/availablePlugins.js";
|
||||
import { setProfiler } from "./profiler.js";
|
||||
import { logRateLimit } from "./rateLimitStats.js";
|
||||
import { startUptimeCounter } from "./uptime.js";
|
||||
|
@ -143,6 +141,12 @@ function errorHandler(err) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (err instanceof FishFishError) {
|
||||
// FishFish errors are not critical, so we just log them
|
||||
console.error(`[FISHFISH] ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// tslint:disable:no-console
|
||||
console.error(err);
|
||||
|
||||
|
@ -166,10 +170,8 @@ function errorHandler(err) {
|
|||
// tslint:enable:no-console
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
process.on("uncaughtException", errorHandler);
|
||||
process.on("unhandledRejection", errorHandler);
|
||||
}
|
||||
process.on("uncaughtException", errorHandler);
|
||||
process.on("unhandledRejection", errorHandler);
|
||||
|
||||
// Verify required Node.js version
|
||||
const REQUIRED_NODE_VERSION = "16.9.0";
|
||||
|
@ -252,8 +254,8 @@ connect().then(async () => {
|
|||
GatewayIntentBits.GuildVoiceStates,
|
||||
],
|
||||
});
|
||||
// FIXME: TS doesn't see Client as a child of EventEmitter for some reason
|
||||
(client as unknown as EventEmitter).setMaxListeners(200);
|
||||
|
||||
client.setMaxListeners(200);
|
||||
|
||||
const safe429DecayInterval = 5 * SECONDS;
|
||||
const safe429MaxCount = 5;
|
||||
|
@ -275,6 +277,10 @@ connect().then(async () => {
|
|||
});
|
||||
|
||||
client.on("error", (err) => {
|
||||
if (err instanceof PluginLoadError) {
|
||||
errorHandler(err);
|
||||
return;
|
||||
}
|
||||
errorHandler(new DiscordJSError(err.message, (err as any).code, 0));
|
||||
});
|
||||
|
||||
|
@ -282,8 +288,8 @@ connect().then(async () => {
|
|||
const guildConfigs = new Configs();
|
||||
|
||||
const bot = new Knub(client, {
|
||||
guildPlugins,
|
||||
globalPlugins,
|
||||
guildPlugins: availableGuildPlugins.map((obj) => obj.plugin),
|
||||
globalPlugins: availableGlobalPlugins.map((obj) => obj.plugin),
|
||||
|
||||
options: {
|
||||
canLoadGuild(guildId): Promise<boolean> {
|
||||
|
@ -292,7 +298,7 @@ connect().then(async () => {
|
|||
|
||||
/**
|
||||
* Plugins are enabled if they...
|
||||
* - are base plugins, i.e. always enabled, or
|
||||
* - are marked to be autoloaded, or
|
||||
* - are explicitly enabled in the guild config
|
||||
* Dependencies are also automatically loaded by Knub.
|
||||
*/
|
||||
|
@ -302,10 +308,10 @@ connect().then(async () => {
|
|||
}
|
||||
|
||||
const configuredPlugins = ctx.config.plugins;
|
||||
const basePluginNames = baseGuildPlugins.map((p) => p.name);
|
||||
const autoloadPluginNames = availableGuildPlugins.filter((obj) => obj.autoload).map((obj) => obj.plugin.name);
|
||||
|
||||
return Array.from(plugins.keys()).filter((pluginName) => {
|
||||
if (basePluginNames.includes(pluginName)) return true;
|
||||
if (autoloadPluginNames.includes(pluginName)) return true;
|
||||
return configuredPlugins[pluginName] && (configuredPlugins[pluginName] as any).enabled !== false;
|
||||
});
|
||||
},
|
||||
|
@ -323,12 +329,30 @@ connect().then(async () => {
|
|||
if (row) {
|
||||
try {
|
||||
const loaded = loadYamlSafely(row.config);
|
||||
|
||||
if (loaded.success_emoji || loaded.error_emoji) {
|
||||
const deprecatedKeys = [] as string[];
|
||||
const exampleConfig = `plugins:\n common:\n config:\n success_emoji: "👍"\n error_emoji: "👎"`;
|
||||
|
||||
if (loaded.success_emoji) {
|
||||
deprecatedKeys.push("success_emoji");
|
||||
}
|
||||
|
||||
if (loaded.error_emoji) {
|
||||
deprecatedKeys.push("error_emoji");
|
||||
}
|
||||
|
||||
// logger.warn(`Deprecated config properties found in "${key}": ${deprecatedKeys.join(", ")}`);
|
||||
// logger.warn(`You can now configure those emojis in the "common" plugin config\n${exampleConfig}`);
|
||||
}
|
||||
|
||||
// Remove deprecated properties some may still have in their config
|
||||
delete loaded.success_emoji;
|
||||
delete loaded.error_emoji;
|
||||
|
||||
return loaded;
|
||||
} catch (err) {
|
||||
logger.error(`Error while loading config "${key}": ${err.message}`);
|
||||
logger.error(`Error while loading config "${key}"`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
@ -385,6 +409,8 @@ connect().then(async () => {
|
|||
enableProfiling();
|
||||
}
|
||||
|
||||
initFishFish();
|
||||
|
||||
runExpiringMutesLoop();
|
||||
await sleep(10 * SECONDS);
|
||||
runExpiringTempbansLoop();
|
||||
|
@ -402,13 +428,6 @@ connect().then(async () => {
|
|||
runExpiredMemberCacheDeletionLoop();
|
||||
await sleep(10 * SECONDS);
|
||||
runMemberCacheDeletionLoop();
|
||||
|
||||
if (hasPhishermanMasterAPIKey()) {
|
||||
await sleep(10 * SECONDS);
|
||||
runPhishermanCacheCleanupLoop();
|
||||
await sleep(10 * SECONDS);
|
||||
runPhishermanReportingLoop();
|
||||
}
|
||||
});
|
||||
|
||||
let lowestGlobalRemaining = Infinity;
|
||||
|
|
|
@ -9,7 +9,7 @@ export class MigrateUsernamesToNewHistoryTable1556909512501 implements Migration
|
|||
|
||||
const migratedUsernames = new Set();
|
||||
|
||||
await new Promise(async (resolve) => {
|
||||
await new Promise<void>(async (resolve) => {
|
||||
const stream = await queryRunner.stream("SELECT CONCAT(user_id, '-', username) AS `key` FROM username_history");
|
||||
stream.on("data", (row: any) => {
|
||||
migratedUsernames.add(row.key);
|
||||
|
|
|
@ -3,18 +3,35 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
BitField,
|
||||
BitFieldResolvable,
|
||||
ChatInputCommandInteraction,
|
||||
CommandInteraction,
|
||||
GuildMember,
|
||||
InteractionEditReplyOptions,
|
||||
InteractionReplyOptions,
|
||||
InteractionResponse,
|
||||
Message,
|
||||
MessageCreateOptions,
|
||||
MessageMentionOptions,
|
||||
MessageEditOptions,
|
||||
MessageFlags,
|
||||
MessageFlagsString,
|
||||
ModalSubmitInteraction,
|
||||
PermissionsBitField,
|
||||
TextBasedChannel,
|
||||
} from "discord.js";
|
||||
import { AnyPluginData, BasePluginData, CommandContext, ExtendedMatchParams, GuildPluginData, helpers } from "knub";
|
||||
import { logger } from "./logger.js";
|
||||
import {
|
||||
AnyPluginData,
|
||||
BasePluginData,
|
||||
CommandContext,
|
||||
ExtendedMatchParams,
|
||||
GuildPluginData,
|
||||
helpers,
|
||||
PluginConfigManager,
|
||||
} from "knub";
|
||||
import z from "zod/v4";
|
||||
import { isStaff } from "./staff.js";
|
||||
import { TZeppelinKnub } from "./types.js";
|
||||
import { errorMessage, successMessage } from "./utils.js";
|
||||
import { Tail } from "./utils/typeUtils.js";
|
||||
|
||||
const { getMemberLevel } = helpers;
|
||||
|
@ -49,45 +66,117 @@ export async function hasPermission(
|
|||
return helpers.hasPermission(config, permission);
|
||||
}
|
||||
|
||||
export async function sendSuccessMessage(
|
||||
pluginData: AnyPluginData<any>,
|
||||
channel: TextBasedChannel,
|
||||
body: string,
|
||||
allowedMentions?: MessageMentionOptions,
|
||||
): Promise<Message | undefined> {
|
||||
const emoji = pluginData.fullConfig.success_emoji || undefined;
|
||||
const formattedBody = successMessage(body, emoji);
|
||||
const content: MessageCreateOptions = allowedMentions
|
||||
? { content: formattedBody, allowedMentions }
|
||||
: { content: formattedBody };
|
||||
export type GenericCommandSource = Message | CommandInteraction | ModalSubmitInteraction;
|
||||
|
||||
return channel
|
||||
.send({ ...content }) // Force line break
|
||||
.catch((err) => {
|
||||
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}`);
|
||||
return undefined;
|
||||
});
|
||||
export function isContextInteraction(
|
||||
context: GenericCommandSource,
|
||||
): context is CommandInteraction | ModalSubmitInteraction {
|
||||
return context instanceof CommandInteraction || context instanceof ModalSubmitInteraction;
|
||||
}
|
||||
|
||||
export async function sendErrorMessage(
|
||||
pluginData: AnyPluginData<any>,
|
||||
channel: TextBasedChannel,
|
||||
body: string,
|
||||
allowedMentions?: MessageMentionOptions,
|
||||
): Promise<Message | undefined> {
|
||||
const emoji = pluginData.fullConfig.error_emoji || undefined;
|
||||
const formattedBody = errorMessage(body, emoji);
|
||||
const content: MessageCreateOptions = allowedMentions
|
||||
? { content: formattedBody, allowedMentions }
|
||||
: { content: formattedBody };
|
||||
export function isContextMessage(context: GenericCommandSource): context is Message {
|
||||
return context instanceof Message;
|
||||
}
|
||||
|
||||
return channel
|
||||
.send({ ...content }) // Force line break
|
||||
.catch((err) => {
|
||||
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}`);
|
||||
return undefined;
|
||||
export async function getContextChannel(context: GenericCommandSource): Promise<TextBasedChannel | null> {
|
||||
if (isContextInteraction(context)) {
|
||||
return context.channel;
|
||||
}
|
||||
if (context instanceof Message) {
|
||||
return context.channel;
|
||||
}
|
||||
throw new Error("Unknown context type");
|
||||
}
|
||||
|
||||
export function getContextChannelId(context: GenericCommandSource): string | null {
|
||||
return context.channelId;
|
||||
}
|
||||
|
||||
export async function fetchContextChannel(context: GenericCommandSource) {
|
||||
if (!context.guild) {
|
||||
throw new Error("Missing context guild");
|
||||
}
|
||||
const channelId = getContextChannelId(context);
|
||||
if (!channelId) {
|
||||
throw new Error("Missing context channel ID");
|
||||
}
|
||||
return (await context.guild.channels.fetch(channelId))!;
|
||||
}
|
||||
|
||||
function flagsWithEphemeral<TFlags extends string, TType extends number | bigint>(
|
||||
flags: BitFieldResolvable<TFlags, any>,
|
||||
ephemeral: boolean,
|
||||
): BitFieldResolvable<TFlags | Extract<MessageFlagsString, "Ephemeral">, TType | MessageFlags.Ephemeral> {
|
||||
if (!ephemeral) {
|
||||
return flags;
|
||||
}
|
||||
return new BitField(flags).add(MessageFlags.Ephemeral) as any;
|
||||
}
|
||||
|
||||
export type ContextResponseOptions = MessageCreateOptions & InteractionReplyOptions & InteractionEditReplyOptions;
|
||||
export type ContextResponse = Message | InteractionResponse;
|
||||
|
||||
export async function sendContextResponse(
|
||||
context: GenericCommandSource,
|
||||
content: string | ContextResponseOptions,
|
||||
ephemeral = false,
|
||||
): Promise<Message> {
|
||||
if (isContextInteraction(context)) {
|
||||
const options = { ...(typeof content === "string" ? { content: content } : content), fetchReply: true };
|
||||
|
||||
if (context.replied) {
|
||||
return context.followUp({
|
||||
...options,
|
||||
flags: flagsWithEphemeral(options.flags, ephemeral),
|
||||
});
|
||||
}
|
||||
if (context.deferred) {
|
||||
return context.editReply(options);
|
||||
}
|
||||
|
||||
const replyResult = await context.reply({
|
||||
...options,
|
||||
flags: flagsWithEphemeral(options.flags, ephemeral),
|
||||
withResponse: true,
|
||||
});
|
||||
return replyResult.resource!.message!;
|
||||
}
|
||||
|
||||
const contextChannel = await fetchContextChannel(context);
|
||||
if (!contextChannel?.isSendable()) {
|
||||
throw new Error("Context channel does not exist or is not sendable");
|
||||
}
|
||||
|
||||
return contextChannel.send(content);
|
||||
}
|
||||
|
||||
export type ContextResponseEditOptions = MessageEditOptions & InteractionEditReplyOptions;
|
||||
|
||||
export function editContextResponse(
|
||||
response: ContextResponse,
|
||||
content: string | ContextResponseEditOptions,
|
||||
): Promise<ContextResponse> {
|
||||
return response.edit(content);
|
||||
}
|
||||
|
||||
export async function deleteContextResponse(response: ContextResponse): Promise<void> {
|
||||
await response.delete();
|
||||
}
|
||||
|
||||
export async function getConfigForContext<TPluginData extends BasePluginData<any>>(
|
||||
config: PluginConfigManager<TPluginData>,
|
||||
context: GenericCommandSource,
|
||||
): Promise<z.output<TPluginData["_pluginType"]["configSchema"]>> {
|
||||
if (context instanceof ChatInputCommandInteraction) {
|
||||
// TODO: Support for modal interactions (here and Knub)
|
||||
return config.getForInteraction(context);
|
||||
}
|
||||
const channel = await getContextChannel(context);
|
||||
const member = isContextMessage(context) && context.inGuild() ? await resolveMessageMember(context) : null;
|
||||
|
||||
return config.getMatchingConfig({
|
||||
channel,
|
||||
member,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -136,4 +225,6 @@ export function makePublicFn<TPluginData extends BasePluginData<any>, T extends
|
|||
};
|
||||
}
|
||||
|
||||
// ???
|
||||
export function resolveMessageMember(message: Message<true>) {
|
||||
return Promise.resolve(message.member || message.guild.members.fetch(message.author.id));
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PluginOptions, guildPlugin } from "knub";
|
||||
import { guildPlugin } from "knub";
|
||||
import { GuildLogs } from "../../data/GuildLogs.js";
|
||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
||||
import { LogsPlugin } from "../Logs/LogsPlugin.js";
|
||||
|
@ -8,19 +8,11 @@ import { onMessageCreate } from "./util/onMessageCreate.js";
|
|||
import { onMessageDelete } from "./util/onMessageDelete.js";
|
||||
import { onMessageDeleteBulk } from "./util/onMessageDeleteBulk.js";
|
||||
|
||||
const defaultOptions: PluginOptions<AutoDeletePluginType> = {
|
||||
config: {
|
||||
enabled: false,
|
||||
delay: "5s",
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoDeletePlugin = guildPlugin<AutoDeletePluginType>()({
|
||||
name: "auto_delete",
|
||||
|
||||
dependencies: () => [TimeAndDatePlugin, LogsPlugin],
|
||||
configParser: (input) => zAutoDeleteConfig.parse(input),
|
||||
defaultOptions,
|
||||
configSchema: zAutoDeleteConfig,
|
||||
|
||||
beforeLoad(pluginData) {
|
||||
const { state, guild } = pluginData;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { ZeppelinPluginInfo } from "../../types.js";
|
||||
import { ZeppelinPluginDocs } from "../../types.js";
|
||||
import { zAutoDeleteConfig } from "./types.js";
|
||||
|
||||
export const autoDeletePluginInfo: ZeppelinPluginInfo = {
|
||||
showInDocs: true,
|
||||
export const autoDeletePluginDocs: ZeppelinPluginDocs = {
|
||||
type: "stable",
|
||||
configSchema: zAutoDeleteConfig,
|
||||
|
||||
prettyName: "Auto-delete",
|
||||
description: "Allows Zeppelin to auto-delete messages from a channel after a delay",
|
||||
configurationGuide: "Maximum deletion delay is currently 5 minutes",
|
||||
configSchema: zAutoDeleteConfig,
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import { BasePluginType } from "knub";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { GuildLogs } from "../../data/GuildLogs.js";
|
||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
||||
import { SavedMessage } from "../../data/entities/SavedMessage.js";
|
||||
|
@ -14,12 +14,12 @@ export interface IDeletionQueueItem {
|
|||
}
|
||||
|
||||
export const zAutoDeleteConfig = z.strictObject({
|
||||
enabled: z.boolean(),
|
||||
delay: zDelayString,
|
||||
enabled: z.boolean().default(false),
|
||||
delay: zDelayString.default("5s"),
|
||||
});
|
||||
|
||||
export interface AutoDeletePluginType extends BasePluginType {
|
||||
config: z.output<typeof zAutoDeleteConfig>;
|
||||
configSchema: typeof zAutoDeleteConfig;
|
||||
state: {
|
||||
guildSavedMessages: GuildSavedMessages;
|
||||
guildLogs: GuildLogs;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChannelType, PermissionsBitField, Snowflake } from "discord.js";
|
||||
import { PermissionsBitField, Snowflake } from "discord.js";
|
||||
import { GuildPluginData } from "knub";
|
||||
import moment from "moment-timezone";
|
||||
import { LogType } from "../../../data/LogType.js";
|
||||
|
@ -17,8 +17,8 @@ export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePlugi
|
|||
scheduleNextDeletion(pluginData);
|
||||
|
||||
const channel = pluginData.guild.channels.cache.get(itemToDelete.message.channel_id as Snowflake);
|
||||
if (!channel || channel.type === ChannelType.GuildCategory) {
|
||||
// Channel was deleted, ignore
|
||||
if (!channel || !("messages" in channel)) {
|
||||
// Channel does not exist or does not support messages, ignore
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,21 @@
|
|||
import { PluginOptions, guildPlugin } from "knub";
|
||||
import { PluginOverride, guildPlugin } from "knub";
|
||||
import { GuildAutoReactions } from "../../data/GuildAutoReactions.js";
|
||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
||||
import { CommonPlugin } from "../Common/CommonPlugin.js";
|
||||
import { LogsPlugin } from "../Logs/LogsPlugin.js";
|
||||
import { DisableAutoReactionsCmd } from "./commands/DisableAutoReactionsCmd.js";
|
||||
import { NewAutoReactionsCmd } from "./commands/NewAutoReactionsCmd.js";
|
||||
import { AddReactionsEvt } from "./events/AddReactionsEvt.js";
|
||||
import { AutoReactionsPluginType, zAutoReactionsConfig } from "./types.js";
|
||||
|
||||
const defaultOptions: PluginOptions<AutoReactionsPluginType> = {
|
||||
config: {
|
||||
can_manage: false,
|
||||
},
|
||||
overrides: [
|
||||
const defaultOverrides: Array<PluginOverride<AutoReactionsPluginType>> = [
|
||||
{
|
||||
level: ">=100",
|
||||
config: {
|
||||
can_manage: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
];
|
||||
|
||||
export const AutoReactionsPlugin = guildPlugin<AutoReactionsPluginType>()({
|
||||
name: "auto_reactions",
|
||||
|
@ -29,8 +25,8 @@ export const AutoReactionsPlugin = guildPlugin<AutoReactionsPluginType>()({
|
|||
LogsPlugin,
|
||||
],
|
||||
|
||||
configParser: (input) => zAutoReactionsConfig.parse(input),
|
||||
defaultOptions,
|
||||
configSchema: zAutoReactionsConfig,
|
||||
defaultOverrides,
|
||||
|
||||
// prettier-ignore
|
||||
messageCommands: [
|
||||
|
@ -50,4 +46,8 @@ export const AutoReactionsPlugin = guildPlugin<AutoReactionsPluginType>()({
|
|||
state.autoReactions = GuildAutoReactions.getGuildInstance(guild.id);
|
||||
state.cache = new Map();
|
||||
},
|
||||
|
||||
beforeStart(pluginData) {
|
||||
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
|
||||
import { autoReactionsCmd } from "../types.js";
|
||||
|
||||
export const DisableAutoReactionsCmd = autoReactionsCmd({
|
||||
|
@ -14,12 +13,12 @@ export const DisableAutoReactionsCmd = autoReactionsCmd({
|
|||
async run({ message: msg, args, pluginData }) {
|
||||
const autoReaction = await pluginData.state.autoReactions.getForChannel(args.channelId);
|
||||
if (!autoReaction) {
|
||||
sendErrorMessage(pluginData, msg.channel, `Auto-reactions aren't enabled in <#${args.channelId}>`);
|
||||
void pluginData.state.common.sendErrorMessage(msg, `Auto-reactions aren't enabled in <#${args.channelId}>`);
|
||||
return;
|
||||
}
|
||||
|
||||
await pluginData.state.autoReactions.removeFromChannel(args.channelId);
|
||||
pluginData.state.cache.delete(args.channelId);
|
||||
sendSuccessMessage(pluginData, msg.channel, `Auto-reactions disabled in <#${args.channelId}>`);
|
||||
void pluginData.state.common.sendSuccessMessage(msg, `Auto-reactions disabled in <#${args.channelId}>`);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { PermissionsBitField } from "discord.js";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
|
||||
import { canUseEmoji, customEmojiRegex, isEmoji } from "../../../utils.js";
|
||||
import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions.js";
|
||||
import { missingPermissionError } from "../../../utils/missingPermissionError.js";
|
||||
|
@ -25,9 +24,8 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
|
|||
const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!;
|
||||
const missingPermissions = getMissingChannelPermissions(me, args.channel, requiredPermissions);
|
||||
if (missingPermissions) {
|
||||
sendErrorMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
pluginData.state.common.sendErrorMessage(
|
||||
msg,
|
||||
`Cannot set auto-reactions for that channel. ${missingPermissionError(missingPermissions)}`,
|
||||
);
|
||||
return;
|
||||
|
@ -35,7 +33,7 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
|
|||
|
||||
for (const reaction of args.reactions) {
|
||||
if (!isEmoji(reaction)) {
|
||||
sendErrorMessage(pluginData, msg.channel, "One or more of the specified reactions were invalid!");
|
||||
void pluginData.state.common.sendErrorMessage(msg, "One or more of the specified reactions were invalid!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -45,7 +43,10 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
|
|||
if (customEmojiMatch) {
|
||||
// Custom emoji
|
||||
if (!canUseEmoji(pluginData.client, customEmojiMatch[2])) {
|
||||
sendErrorMessage(pluginData, msg.channel, "I can only use regular emojis and custom emojis from this server");
|
||||
pluginData.state.common.sendErrorMessage(
|
||||
msg,
|
||||
"I can only use regular emojis and custom emojis from this server",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -60,6 +61,6 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
|
|||
|
||||
await pluginData.state.autoReactions.set(args.channel.id, finalReactions);
|
||||
pluginData.state.cache.delete(args.channel.id);
|
||||
sendSuccessMessage(pluginData, msg.channel, `Auto-reactions set for <#${args.channel.id}>`);
|
||||
void pluginData.state.common.sendSuccessMessage(msg, `Auto-reactions set for <#${args.channel.id}>`);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { ZeppelinPluginInfo } from "../../types.js";
|
||||
import { ZeppelinPluginDocs } from "../../types.js";
|
||||
import { trimPluginDescription } from "../../utils.js";
|
||||
import { zAutoReactionsConfig } from "./types.js";
|
||||
|
||||
export const autoReactionsInfo: ZeppelinPluginInfo = {
|
||||
showInDocs: true,
|
||||
export const autoReactionsPluginDocs: ZeppelinPluginDocs = {
|
||||
type: "stable",
|
||||
configSchema: zAutoReactionsConfig,
|
||||
|
||||
prettyName: "Auto-reactions",
|
||||
description: trimPluginDescription(`
|
||||
Allows setting up automatic reactions to all new messages on a channel
|
||||
`),
|
||||
configSchema: zAutoReactionsConfig,
|
||||
};
|
|
@ -1,21 +1,23 @@
|
|||
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub";
|
||||
import z from "zod";
|
||||
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub";
|
||||
import z from "zod/v4";
|
||||
import { GuildAutoReactions } from "../../data/GuildAutoReactions.js";
|
||||
import { GuildLogs } from "../../data/GuildLogs.js";
|
||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
||||
import { AutoReaction } from "../../data/entities/AutoReaction.js";
|
||||
import { CommonPlugin } from "../Common/CommonPlugin.js";
|
||||
|
||||
export const zAutoReactionsConfig = z.strictObject({
|
||||
can_manage: z.boolean(),
|
||||
can_manage: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export interface AutoReactionsPluginType extends BasePluginType {
|
||||
config: z.output<typeof zAutoReactionsConfig>;
|
||||
configSchema: typeof zAutoReactionsConfig;
|
||||
state: {
|
||||
logs: GuildLogs;
|
||||
savedMessages: GuildSavedMessages;
|
||||
autoReactions: GuildAutoReactions;
|
||||
cache: Map<string, AutoReaction | null>;
|
||||
common: pluginUtils.PluginPublicInterface<typeof CommonPlugin>;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners.js";
|
|||
import { MINUTES, SECONDS } from "../../utils.js";
|
||||
import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap.js";
|
||||
import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap.js";
|
||||
import { CommonPlugin } from "../Common/CommonPlugin.js";
|
||||
import { CountersPlugin } from "../Counters/CountersPlugin.js";
|
||||
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin.js";
|
||||
import { LogsPlugin } from "../Logs/LogsPlugin.js";
|
||||
|
@ -33,29 +34,6 @@ import { clearOldRecentActions } from "./functions/clearOldRecentActions.js";
|
|||
import { clearOldRecentSpam } from "./functions/clearOldRecentSpam.js";
|
||||
import { AutomodPluginType, zAutomodConfig } from "./types.js";
|
||||
|
||||
const defaultOptions = {
|
||||
config: {
|
||||
rules: {},
|
||||
antiraid_levels: ["low", "medium", "high"],
|
||||
can_set_antiraid: false,
|
||||
can_view_antiraid: false,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
level: ">=50",
|
||||
config: {
|
||||
can_view_antiraid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
level: ">=100",
|
||||
config: {
|
||||
can_set_antiraid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const AutomodPlugin = guildPlugin<AutomodPluginType>()({
|
||||
name: "automod",
|
||||
|
||||
|
@ -70,8 +48,7 @@ export const AutomodPlugin = guildPlugin<AutomodPluginType>()({
|
|||
RoleManagerPlugin,
|
||||
],
|
||||
|
||||
defaultOptions,
|
||||
configParser: (input) => zAutomodConfig.parse(input),
|
||||
configSchema: zAutomodConfig,
|
||||
|
||||
customOverrideCriteriaFunctions: {
|
||||
antiraid_level: (pluginData, matchParams, value) => {
|
||||
|
@ -117,6 +94,10 @@ export const AutomodPlugin = guildPlugin<AutomodPluginType>()({
|
|||
state.cachedAntiraidLevel = await state.antiraidLevels.get();
|
||||
},
|
||||
|
||||
beforeStart(pluginData) {
|
||||
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
|
||||
},
|
||||
|
||||
async afterLoad(pluginData) {
|
||||
const { state } = pluginData;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PermissionFlagsBits, Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { nonNullish, unique, zSnowflake } from "../../../utils.js";
|
||||
import { canAssignRole } from "../../../utils/canAssignRole.js";
|
||||
import { getMissingPermissions } from "../../../utils/getMissingPermissions.js";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { zBoundedCharacters } from "../../../utils.js";
|
||||
import { CountersPlugin } from "../../Counters/CountersPlugin.js";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { LogType } from "../../../data/LogType.js";
|
||||
import {
|
||||
createTypedTemplateSafeValueContainer,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { AnyThreadChannel } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { noop } from "../../../utils.js";
|
||||
import { automodAction } from "../helpers.js";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import {
|
||||
convertDelayStringToMS,
|
||||
nonNullish,
|
||||
|
@ -47,6 +47,7 @@ export const BanAction = automodAction({
|
|||
await modActions.banUserId(
|
||||
userId,
|
||||
reason,
|
||||
reason,
|
||||
{
|
||||
contactMethods,
|
||||
caseArgs,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { nonNullish, unique, zBoundedCharacters } from "../../../utils.js";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||
import { automodAction } from "../helpers.js";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { PermissionsBitField, PermissionsString } from "discord.js";
|
||||
import { U } from "ts-toolbelt";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter.js";
|
||||
import { isValidSnowflake, keys, noop, zBoundedCharacters } from "../../../utils.js";
|
||||
import {
|
||||
|
@ -65,11 +65,17 @@ const permissionNames = keys(PermissionsBitField.Flags) as U.ListOf<keyof typeof
|
|||
const legacyPermissionNames = keys(legacyPermMap) as U.ListOf<keyof typeof legacyPermMap>;
|
||||
const allPermissionNames = [...permissionNames, ...legacyPermissionNames] as const;
|
||||
|
||||
const permissionTypeMap = allPermissionNames.reduce((map, permName) => {
|
||||
map[permName] = z.boolean().nullable();
|
||||
return map;
|
||||
}, {} as Record<typeof allPermissionNames[number], z.ZodNullable<z.ZodBoolean>>);
|
||||
const zPermissionsMap = z.strictObject(permissionTypeMap);
|
||||
|
||||
export const ChangePermsAction = automodAction({
|
||||
configSchema: z.strictObject({
|
||||
target: zBoundedCharacters(1, 2000),
|
||||
channel: zBoundedCharacters(1, 2000).nullable().default(null),
|
||||
perms: z.record(z.enum(allPermissionNames), z.boolean().nullable()),
|
||||
perms: zPermissionsMap.partial(),
|
||||
}),
|
||||
|
||||
async apply({ pluginData, contexts, actionConfig, ruleName }) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GuildTextBasedChannel, Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { LogType } from "../../../data/LogType.js";
|
||||
import { noop } from "../../../utils.js";
|
||||
import { automodAction } from "../helpers.js";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { zBoundedCharacters } from "../../../utils.js";
|
||||
import { automodAction } from "../helpers.js";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils.js";
|
||||
import { CaseArgs } from "../../Cases/types.js";
|
||||
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
|
||||
|
@ -33,7 +33,7 @@ export const KickAction = automodAction({
|
|||
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
||||
for (const member of membersToKick) {
|
||||
if (!member) continue;
|
||||
await modActions.kickMember(member, reason, { contactMethods, caseArgs, isAutomodAction: true });
|
||||
await modActions.kickMember(member, reason, reason, { contactMethods, caseArgs, isAutomodAction: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { isTruthy, unique } from "../../../utils.js";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||
import { automodAction } from "../helpers.js";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError.js";
|
||||
import {
|
||||
convertDelayStringToMS,
|
||||
|
@ -57,6 +57,7 @@ export const MuteAction = automodAction({
|
|||
userId,
|
||||
duration,
|
||||
reason,
|
||||
reason,
|
||||
{ contactMethods, caseArgs, isAutomodAction: true },
|
||||
rolesToRemove,
|
||||
rolesToRestore,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GuildFeature } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodAction } from "../helpers.js";
|
||||
|
||||
export const PauseInvitesAction = automodAction({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PermissionFlagsBits, Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { nonNullish, unique, zSnowflake } from "../../../utils.js";
|
||||
import { canAssignRole } from "../../../utils/canAssignRole.js";
|
||||
import { getMissingPermissions } from "../../../utils/getMissingPermissions.js";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GuildTextBasedChannel, MessageCreateOptions, PermissionsBitField, Snowflake, User } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter.js";
|
||||
import {
|
||||
convertDelayStringToMS,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { MAX_COUNTER_VALUE, MIN_COUNTER_VALUE } from "../../../data/GuildCounters.js";
|
||||
import { zBoundedCharacters } from "../../../utils.js";
|
||||
import { CountersPlugin } from "../../Counters/CountersPlugin.js";
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { ChannelType, GuildTextBasedChannel, Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { convertDelayStringToMS, isDiscordAPIError, zDelayString, zSnowflake } from "../../../utils.js";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||
import { automodAction } from "../helpers.js";
|
||||
|
||||
export const SetSlowmodeAction = automodAction({
|
||||
configSchema: z.strictObject({
|
||||
channels: z.array(zSnowflake),
|
||||
channels: z.array(zSnowflake).nullable().default([]),
|
||||
duration: zDelayString.nullable().default("10s"),
|
||||
}),
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ChannelType, GuildTextThreadCreateOptions, ThreadAutoArchiveDuration, ThreadChannel } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter.js";
|
||||
import { MINUTES, convertDelayStringToMS, noop, zBoundedCharacters, zDelayString } from "../../../utils.js";
|
||||
import { savedMessageToTemplateSafeSavedMessage, userToTemplateSafeUser } from "../../../utils/templateSafeObjects.js";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils.js";
|
||||
import { CaseArgs } from "../../Cases/types.js";
|
||||
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
|
||||
|
@ -33,7 +33,7 @@ export const WarnAction = automodAction({
|
|||
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
||||
for (const member of membersToWarn) {
|
||||
if (!member) continue;
|
||||
await modActions.warnMember(member, reason, { contactMethods, caseArgs, isAutomodAction: true });
|
||||
await modActions.warnMember(member, reason, reason, { contactMethods, caseArgs, isAutomodAction: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { guildPluginMessageCommand } from "knub";
|
||||
import { sendSuccessMessage } from "../../../pluginUtils.js";
|
||||
import { setAntiraidLevel } from "../functions/setAntiraidLevel.js";
|
||||
import { AutomodPluginType } from "../types.js";
|
||||
|
||||
|
@ -9,6 +8,6 @@ export const AntiraidClearCmd = guildPluginMessageCommand<AutomodPluginType>()({
|
|||
|
||||
async run({ pluginData, message }) {
|
||||
await setAntiraidLevel(pluginData, null, message.author);
|
||||
sendSuccessMessage(pluginData, message.channel, "Anti-raid turned **off**");
|
||||
void pluginData.state.common.sendSuccessMessage(message, "Anti-raid turned **off**");
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { guildPluginMessageCommand } from "knub";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
|
||||
import { setAntiraidLevel } from "../functions/setAntiraidLevel.js";
|
||||
import { AutomodPluginType } from "../types.js";
|
||||
|
||||
|
@ -15,11 +14,11 @@ export const SetAntiraidCmd = guildPluginMessageCommand<AutomodPluginType>()({
|
|||
async run({ pluginData, message, args }) {
|
||||
const config = pluginData.config.get();
|
||||
if (!config.antiraid_levels.includes(args.level)) {
|
||||
sendErrorMessage(pluginData, message.channel, "Unknown anti-raid level");
|
||||
pluginData.state.common.sendErrorMessage(message, "Unknown anti-raid level");
|
||||
return;
|
||||
}
|
||||
|
||||
await setAntiraidLevel(pluginData, args.level, message.author);
|
||||
sendSuccessMessage(pluginData, message.channel, `Anti-raid level set to **${args.level}**`);
|
||||
pluginData.state.common.sendSuccessMessage(message, `Anti-raid level set to **${args.level}**`);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { MINUTES, SECONDS } from "../../utils.js";
|
||||
|
||||
export const RECENT_SPAM_EXPIRY_TIME = 10 * SECONDS;
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { ZeppelinPluginInfo } from "../../types.js";
|
||||
import { ZeppelinPluginDocs } from "../../types.js";
|
||||
import { trimPluginDescription } from "../../utils.js";
|
||||
import { zAutomodConfig } from "./types.js";
|
||||
|
||||
export const automodPluginInfo: ZeppelinPluginInfo = {
|
||||
showInDocs: true,
|
||||
prettyName: "Automod",
|
||||
export const automodPluginDocs: ZeppelinPluginDocs = {
|
||||
type: "stable",
|
||||
configSchema: zAutomodConfig,
|
||||
|
||||
prettyName: "Automod",
|
||||
description: trimPluginDescription(`
|
||||
Allows specifying automated actions in response to triggers. Example use cases include word filtering and spam prevention.
|
||||
`),
|
|
@ -1,6 +1,5 @@
|
|||
import { guildPluginEventListener } from "knub";
|
||||
import diff from "lodash/difference.js";
|
||||
import isEqual from "lodash/isEqual.js";
|
||||
import { difference, isEqual } from "lodash-es";
|
||||
import { runAutomod } from "../functions/runAutomod.js";
|
||||
import { AutomodContext, AutomodPluginType } from "../types.js";
|
||||
|
||||
|
@ -15,8 +14,8 @@ export const RunAutomodOnMemberUpdate = guildPluginEventListener<AutomodPluginTy
|
|||
|
||||
if (isEqual(oldRoles, newRoles)) return;
|
||||
|
||||
const addedRoles = diff(newRoles, oldRoles);
|
||||
const removedRoles = diff(oldRoles, newRoles);
|
||||
const addedRoles = difference(newRoles, oldRoles);
|
||||
const removedRoles = difference(oldRoles, newRoles);
|
||||
|
||||
if (addedRoles.length || removedRoles.length) {
|
||||
const context: AutomodContext = {
|
||||
|
|
|
@ -2,8 +2,13 @@ import { GuildPluginData } from "knub";
|
|||
import { convertDelayStringToMS } from "../../../utils.js";
|
||||
import { AutomodContext, AutomodPluginType, TRule } from "../types.js";
|
||||
|
||||
export function applyCooldown(pluginData: GuildPluginData<AutomodPluginType>, rule: TRule, context: AutomodContext) {
|
||||
const cooldownKey = `${rule.name}-${context.user?.id}`;
|
||||
export function applyCooldown(
|
||||
pluginData: GuildPluginData<AutomodPluginType>,
|
||||
rule: TRule,
|
||||
ruleName: string,
|
||||
context: AutomodContext,
|
||||
) {
|
||||
const cooldownKey = `${ruleName}-${context.user?.id}`;
|
||||
|
||||
const cooldownTime = convertDelayStringToMS(rule.cooldown, "s");
|
||||
if (cooldownTime) pluginData.state.cooldownManager.setCooldown(cooldownKey, cooldownTime);
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { AutomodContext, AutomodPluginType, TRule } from "../types.js";
|
||||
|
||||
export function checkCooldown(pluginData: GuildPluginData<AutomodPluginType>, rule: TRule, context: AutomodContext) {
|
||||
const cooldownKey = `${rule.name}-${context.user?.id}`;
|
||||
export function checkCooldown(
|
||||
pluginData: GuildPluginData<AutomodPluginType>,
|
||||
rule: TRule,
|
||||
ruleName: string,
|
||||
context: AutomodContext,
|
||||
) {
|
||||
const cooldownKey = `${ruleName}-${context.user?.id}`;
|
||||
|
||||
return pluginData.state.cooldownManager.isOnCooldown(cooldownKey);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { SavedMessage } from "../../../data/entities/SavedMessage.js";
|
||||
import { humanizeDurationShort } from "../../../humanizeDurationShort.js";
|
||||
import { humanizeDurationShort } from "../../../humanizeDuration.js";
|
||||
import { getBaseUrl } from "../../../pluginUtils.js";
|
||||
import { convertDelayStringToMS, sorter, zDelayString } from "../../../utils.js";
|
||||
import { RecentActionType } from "../constants.js";
|
||||
|
|
|
@ -49,7 +49,7 @@ export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>,
|
|||
}
|
||||
if (!rule.affects_self && userId && userId === pluginData.client.user?.id) continue;
|
||||
|
||||
if (rule.cooldown && checkCooldown(pluginData, rule, context)) {
|
||||
if (rule.cooldown && checkCooldown(pluginData, rule, ruleName, context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -87,7 +87,7 @@ export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>,
|
|||
}
|
||||
|
||||
if (matchResult) {
|
||||
if (rule.cooldown) applyCooldown(pluginData, rule, context);
|
||||
if (rule.cooldown) applyCooldown(pluginData, rule, ruleName, context);
|
||||
|
||||
contexts = [context, ...(matchResult.extraContexts || [])];
|
||||
|
||||
|
@ -164,6 +164,18 @@ export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>,
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Log all automod rules by default
|
||||
if (rule.actions.log == null) {
|
||||
availableActions.log.apply({
|
||||
ruleName,
|
||||
pluginData,
|
||||
contexts,
|
||||
actionConfig: true,
|
||||
matchResult,
|
||||
prettyName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (profilingEnabled()) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import z, { ZodTypeAny } from "zod";
|
||||
import z, { ZodTypeAny } from "zod/v4";
|
||||
import { Awaitable } from "../../utils/typeUtils.js";
|
||||
import { AutomodContext, AutomodPluginType } from "./types.js";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
interface AntiraidLevelTriggerResult {}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { verboseChannelMention } from "../../../utils.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
// tslint:disable-next-line
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
interface ExampleMatchResultType {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { escapeInlineCode, Snowflake } from "discord.js";
|
||||
import { extname } from "path";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
|
@ -9,30 +9,12 @@ interface MatchResultType {
|
|||
mode: "blacklist" | "whitelist";
|
||||
}
|
||||
|
||||
const configSchema = z
|
||||
.strictObject({
|
||||
filetype_blacklist: z.array(z.string().max(32)).max(255).default([]),
|
||||
blacklist_enabled: z.boolean().default(false),
|
||||
filetype_whitelist: z.array(z.string().max(32)).max(255).default([]),
|
||||
const configSchema = z.strictObject({
|
||||
whitelist_enabled: z.boolean().default(false),
|
||||
})
|
||||
.transform((parsed, ctx) => {
|
||||
if (parsed.blacklist_enabled && parsed.whitelist_enabled) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Cannot have both blacklist and whitelist enabled",
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
if (!parsed.blacklist_enabled && !parsed.whitelist_enabled) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Must have either blacklist or whitelist enabled",
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return parsed;
|
||||
});
|
||||
filetype_whitelist: z.array(z.string().max(32)).max(255).default([]),
|
||||
blacklist_enabled: z.boolean().default(false),
|
||||
filetype_blacklist: z.array(z.string().max(32)).max(255).default([]),
|
||||
});
|
||||
|
||||
export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({
|
||||
configSchema,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { getInviteCodesInString, GuildInvite, isGuildInvite, resolveInvite, zSnowflake } from "../../../utils.js";
|
||||
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary.js";
|
||||
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage.js";
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { escapeInlineCode } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { allowTimeout } from "../../../RegExpRunner.js";
|
||||
import { phishermanDomainIsSafe } from "../../../data/Phisherman.js";
|
||||
import { getUrlsInString, zRegex } from "../../../utils.js";
|
||||
import { getFishFishDomain } from "../../../data/FishFish.js";
|
||||
import { getUrlsInString, inputPatternToRegExp, zRegex } from "../../../utils.js";
|
||||
import { mergeRegexes } from "../../../utils/mergeRegexes.js";
|
||||
import { mergeWordsIntoRegex } from "../../../utils/mergeWordsIntoRegex.js";
|
||||
import { PhishermanPlugin } from "../../Phisherman/PhishermanPlugin.js";
|
||||
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary.js";
|
||||
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
@ -40,6 +39,7 @@ const configSchema = z.strictObject({
|
|||
include_verified: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
include_malicious: z.boolean().default(false),
|
||||
only_real_links: z.boolean().default(true),
|
||||
match_messages: z.boolean().default(true),
|
||||
match_embeds: z.boolean().default(true),
|
||||
|
@ -73,7 +73,7 @@ export const MatchLinksTrigger = automodTrigger<MatchResultType>()({
|
|||
|
||||
if (trigger.exclude_regex) {
|
||||
if (!regexCache.has(trigger.exclude_regex)) {
|
||||
const toCache = mergeRegexes(trigger.exclude_regex, "i");
|
||||
const toCache = mergeRegexes(trigger.exclude_regex.map(pattern => inputPatternToRegExp(pattern)), "i");
|
||||
regexCache.set(trigger.exclude_regex, toCache);
|
||||
}
|
||||
const regexes = regexCache.get(trigger.exclude_regex)!;
|
||||
|
@ -88,7 +88,7 @@ export const MatchLinksTrigger = automodTrigger<MatchResultType>()({
|
|||
|
||||
if (trigger.include_regex) {
|
||||
if (!regexCache.has(trigger.include_regex)) {
|
||||
const toCache = mergeRegexes(trigger.include_regex, "i");
|
||||
const toCache = mergeRegexes(trigger.include_regex.map(pattern => inputPatternToRegExp(pattern)), "i");
|
||||
regexCache.set(trigger.include_regex, toCache);
|
||||
}
|
||||
const regexes = regexCache.get(trigger.include_regex)!;
|
||||
|
@ -155,26 +155,22 @@ export const MatchLinksTrigger = automodTrigger<MatchResultType>()({
|
|||
}
|
||||
}
|
||||
|
||||
if (trigger.phisherman) {
|
||||
const phishermanResult = await pluginData.getPlugin(PhishermanPlugin).getDomainInfo(normalizedHostname);
|
||||
if (phishermanResult != null && !phishermanDomainIsSafe(phishermanResult)) {
|
||||
if (
|
||||
(trigger.phisherman.include_suspected && !phishermanResult.verifiedPhish) ||
|
||||
(trigger.phisherman.include_verified && phishermanResult.verifiedPhish)
|
||||
) {
|
||||
const suspectedVerified = phishermanResult.verifiedPhish ? "verified" : "suspected";
|
||||
const includeMalicious =
|
||||
trigger.include_malicious || trigger.phisherman?.include_suspected || trigger.phisherman?.include_verified;
|
||||
if (includeMalicious) {
|
||||
const domainInfo = getFishFishDomain(normalizedHostname);
|
||||
if (domainInfo && domainInfo.category !== "safe") {
|
||||
return {
|
||||
extra: {
|
||||
type,
|
||||
link: link.input,
|
||||
details: `using Phisherman (${suspectedVerified})`,
|
||||
details: `(known ${domainInfo.category} domain)`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { escapeInlineCode } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
|
@ -8,30 +8,12 @@ interface MatchResultType {
|
|||
mode: "blacklist" | "whitelist";
|
||||
}
|
||||
|
||||
const configSchema = z
|
||||
.strictObject({
|
||||
mime_type_blacklist: z.array(z.string().max(255)).max(255).default([]),
|
||||
blacklist_enabled: z.boolean().default(false),
|
||||
mime_type_whitelist: z.array(z.string().max(255)).max(255).default([]),
|
||||
const configSchema = z.strictObject({
|
||||
whitelist_enabled: z.boolean().default(false),
|
||||
})
|
||||
.transform((parsed, ctx) => {
|
||||
if (parsed.blacklist_enabled && parsed.whitelist_enabled) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Cannot have both blacklist and whitelist enabled",
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
if (!parsed.blacklist_enabled && !parsed.whitelist_enabled) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Must have either blacklist or whitelist enabled",
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return parsed;
|
||||
});
|
||||
mime_type_whitelist: z.array(z.string().max(32)).max(255).default([]),
|
||||
blacklist_enabled: z.boolean().default(false),
|
||||
mime_type_blacklist: z.array(z.string().max(32)).max(255).default([]),
|
||||
});
|
||||
|
||||
export const MatchMimeTypeTrigger = automodTrigger<MatchResultType>()({
|
||||
configSchema,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { allowTimeout } from "../../../RegExpRunner.js";
|
||||
import { zRegex } from "../../../utils.js";
|
||||
import { inputPatternToRegExp, zRegex } from "../../../utils.js";
|
||||
import { mergeRegexes } from "../../../utils/mergeRegexes.js";
|
||||
import { normalizeText } from "../../../utils/normalizeText.js";
|
||||
import { stripMarkdown } from "../../../utils/stripMarkdown.js";
|
||||
|
@ -38,7 +38,7 @@ export const MatchRegexTrigger = automodTrigger<MatchResultType>()({
|
|||
|
||||
if (!regexCache.has(trigger)) {
|
||||
const flags = trigger.case_sensitive ? "" : "i";
|
||||
const toCache = mergeRegexes(trigger.patterns, flags);
|
||||
const toCache = mergeRegexes(trigger.patterns.map(pattern => inputPatternToRegExp(pattern)), flags);
|
||||
regexCache.set(trigger, toCache);
|
||||
}
|
||||
const regexes = regexCache.get(trigger)!;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import escapeStringRegexp from "escape-string-regexp";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { normalizeText } from "../../../utils/normalizeText.js";
|
||||
import { stripMarkdown } from "../../../utils/stripMarkdown.js";
|
||||
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary.js";
|
||||
|
@ -19,7 +19,7 @@ const configSchema = z.strictObject({
|
|||
only_full_words: z.boolean().default(true),
|
||||
normalize: z.boolean().default(false),
|
||||
loose_matching: z.boolean().default(false),
|
||||
loose_matching_threshold: z.number().int().default(4),
|
||||
loose_matching_threshold: z.number().int().default(1),
|
||||
strip_markdown: z.boolean().default(false),
|
||||
match_messages: z.boolean().default(true),
|
||||
match_embeds: z.boolean().default(false),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { convertDelayStringToMS, zDelayString } from "../../../utils.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { convertDelayStringToMS, zDelayString } from "../../../utils.js";
|
||||
import { RecentActionType } from "../constants.js";
|
||||
import { findRecentSpam } from "../functions/findRecentSpam.js";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
const configSchema = z.strictObject({});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { renderUsername, zSnowflake } from "../../../utils.js";
|
||||
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { renderUsername, zSnowflake } from "../../../utils.js";
|
||||
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { User, escapeBold, type Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { renderUsername } from "../../../utils.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { User, escapeBold, type Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { renderUsername } from "../../../utils.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { convertDelayStringToMS, zDelayString } from "../../../utils.js";
|
||||
import { RecentActionType } from "../constants.js";
|
||||
import { findRecentSpam } from "../functions/findRecentSpam.js";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { User, escapeBold, type Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { renderUsername } from "../../../utils.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { User, escapeBold, type Snowflake } from "discord.js";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { renderUsername } from "../../../utils.js";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { automodTrigger } from "../helpers.js";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { GuildMember, GuildTextBasedChannel, PartialGuildMember, ThreadChannel, User } from "discord.js";
|
||||
import { BasePluginType, CooldownManager } from "knub";
|
||||
import z from "zod";
|
||||
import { BasePluginType, CooldownManager, pluginUtils } from "knub";
|
||||
import z from "zod/v4";
|
||||
import { Queue } from "../../Queue.js";
|
||||
import { RegExpRunner } from "../../RegExpRunner.js";
|
||||
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels.js";
|
||||
|
@ -9,6 +9,7 @@ import { GuildLogs } from "../../data/GuildLogs.js";
|
|||
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
||||
import { SavedMessage } from "../../data/entities/SavedMessage.js";
|
||||
import { entries, zBoundedRecord, zDelayString } from "../../utils.js";
|
||||
import { CommonPlugin } from "../Common/CommonPlugin.js";
|
||||
import { CounterEvents } from "../Counters/types.js";
|
||||
import { ModActionType, ModActionsEvents } from "../ModActions/types.js";
|
||||
import { MutesEvents } from "../Mutes/types.js";
|
||||
|
@ -45,22 +46,6 @@ const zActionsMap = z
|
|||
|
||||
const zRule = z.strictObject({
|
||||
enabled: z.boolean().default(true),
|
||||
// Typed as "never" because you are not expected to supply this directly.
|
||||
// The transform instead picks it up from the property key and the output type is a string.
|
||||
name: z
|
||||
.never()
|
||||
.optional()
|
||||
.transform((_, ctx) => {
|
||||
const ruleName = String(ctx.path[ctx.path.length - 2]).trim();
|
||||
if (!ruleName) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Automod rules must have names",
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return ruleName;
|
||||
}),
|
||||
pretty_name: z.string().optional(),
|
||||
presets: z.array(z.string().max(100)).max(25).default([]),
|
||||
affects_bots: z.boolean().default(false),
|
||||
|
@ -68,21 +53,19 @@ const zRule = z.strictObject({
|
|||
cooldown: zDelayString.nullable().default(null),
|
||||
allow_further_rules: z.boolean().default(false),
|
||||
triggers: z.array(zTriggersMap),
|
||||
actions: zActionsMap.refine((v) => !(v.clean && v.start_thread), {
|
||||
message: "Cannot have both clean and start_thread active at the same time",
|
||||
}),
|
||||
actions: zActionsMap,
|
||||
});
|
||||
export type TRule = z.infer<typeof zRule>;
|
||||
|
||||
export const zAutomodConfig = z.strictObject({
|
||||
rules: zBoundedRecord(z.record(z.string().max(100), zRule), 0, 255),
|
||||
antiraid_levels: z.array(z.string().max(100)).max(10),
|
||||
can_set_antiraid: z.boolean(),
|
||||
can_view_antiraid: z.boolean(),
|
||||
rules: zBoundedRecord(z.record(z.string().max(100), zRule), 0, 255).default({}),
|
||||
antiraid_levels: z.array(z.string().max(100)).max(10).default(["low", "medium", "high"]),
|
||||
can_set_antiraid: z.boolean().default(false),
|
||||
can_view_antiraid: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export interface AutomodPluginType extends BasePluginType {
|
||||
config: z.output<typeof zAutomodConfig>;
|
||||
configSchema: typeof zAutomodConfig;
|
||||
|
||||
customOverrideCriteria: {
|
||||
antiraid_level?: string;
|
||||
|
@ -140,6 +123,8 @@ export interface AutomodPluginType extends BasePluginType {
|
|||
|
||||
modActionsListeners: Map<keyof ModActionsEvents, any>;
|
||||
mutesListeners: Map<keyof MutesEvents, any>;
|
||||
|
||||
common: pluginUtils.PluginPublicInterface<typeof CommonPlugin>;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import { AllowedGuilds } from "../../data/AllowedGuilds.js";
|
|||
import { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments.js";
|
||||
import { Configs } from "../../data/Configs.js";
|
||||
import { GuildArchives } from "../../data/GuildArchives.js";
|
||||
import { sendSuccessMessage } from "../../pluginUtils.js";
|
||||
import { getActiveReload, resetActiveReload } from "./activeReload.js";
|
||||
import { AddDashboardUserCmd } from "./commands/AddDashboardUserCmd.js";
|
||||
import { AddServerFromInviteCmd } from "./commands/AddServerFromInviteCmd.js";
|
||||
|
@ -24,21 +23,9 @@ import { RestPerformanceCmd } from "./commands/RestPerformanceCmd.js";
|
|||
import { ServersCmd } from "./commands/ServersCmd.js";
|
||||
import { BotControlPluginType, zBotControlConfig } from "./types.js";
|
||||
|
||||
const defaultOptions = {
|
||||
config: {
|
||||
can_use: false,
|
||||
can_eligible: false,
|
||||
can_performance: false,
|
||||
can_add_server_from_invite: false,
|
||||
can_list_dashboard_perms: false,
|
||||
update_cmd: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const BotControlPlugin = globalPlugin<BotControlPluginType>()({
|
||||
name: "bot_control",
|
||||
configParser: (input) => zBotControlConfig.parse(input),
|
||||
defaultOptions,
|
||||
configSchema: zBotControlConfig,
|
||||
|
||||
// prettier-ignore
|
||||
messageCommands: [
|
||||
|
@ -77,7 +64,7 @@ export const BotControlPlugin = globalPlugin<BotControlPluginType>()({
|
|||
if (guild) {
|
||||
const channel = guild.channels.cache.get(channelId as Snowflake);
|
||||
if (channel instanceof TextChannel) {
|
||||
sendSuccessMessage(pluginData, channel, "Global plugins reloaded!");
|
||||
void channel.send("Global plugins reloaded!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { isStaffPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
|
||||
import { isStaffPreFilter } from "../../../pluginUtils.js";
|
||||
import { renderUsername } from "../../../utils.js";
|
||||
import { botControlCmd } from "../types.js";
|
||||
|
||||
|
@ -19,7 +19,7 @@ export const AddDashboardUserCmd = botControlCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const guild = await pluginData.state.allowedGuilds.find(args.guildId);
|
||||
if (!guild) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Server is not using Zeppelin");
|
||||
void msg.channel.send("Server is not using Zeppelin");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -36,10 +36,7 @@ export const AddDashboardUserCmd = botControlCmd({
|
|||
}
|
||||
|
||||
const userNameList = args.users.map((user) => `<@!${user.id}> (**${renderUsername(user)}**, \`${user.id}\`)`);
|
||||
sendSuccessMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`The following users were given dashboard access for **${guild.name}**:\n\n${userNameList}`,
|
||||
);
|
||||
|
||||
msg.channel.send(`The following users were given dashboard access for **${guild.name}**:\n\n${userNameList}`);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
||||
import moment from "moment-timezone";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
|
||||
import { DBDateFormat, isGuildInvite, resolveInvite } from "../../../utils.js";
|
||||
import { isEligible } from "../functions/isEligible.js";
|
||||
import { botControlCmd } from "../types.js";
|
||||
|
@ -18,19 +17,19 @@ export const AddServerFromInviteCmd = botControlCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const invite = await resolveInvite(pluginData.client, args.inviteCode, true);
|
||||
if (!invite || !isGuildInvite(invite)) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Could not resolve invite"); // :D
|
||||
void msg.channel.send("Could not resolve invite"); // :D
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await pluginData.state.allowedGuilds.find(invite.guild.id);
|
||||
if (existing) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Server is already allowed!");
|
||||
void msg.channel.send("Server is already allowed!");
|
||||
return;
|
||||
}
|
||||
|
||||
const { result, explanation } = await isEligible(pluginData, args.user, invite);
|
||||
if (!result) {
|
||||
sendErrorMessage(pluginData, msg.channel, `Could not add server because it's not eligible: ${explanation}`);
|
||||
msg.channel.send(`Could not add server because it's not eligible: ${explanation}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -51,6 +50,6 @@ export const AddServerFromInviteCmd = botControlCmd({
|
|||
);
|
||||
}
|
||||
|
||||
sendSuccessMessage(pluginData, msg.channel, "Server was eligible and is now allowed to use Zeppelin!");
|
||||
msg.channel.send("Server was eligible and is now allowed to use Zeppelin!");
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
||||
import moment from "moment-timezone";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { isStaffPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
|
||||
import { isStaffPreFilter } from "../../../pluginUtils.js";
|
||||
import { DBDateFormat, isSnowflake } from "../../../utils.js";
|
||||
import { botControlCmd } from "../types.js";
|
||||
|
||||
|
@ -20,17 +20,17 @@ export const AllowServerCmd = botControlCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const existing = await pluginData.state.allowedGuilds.find(args.guildId);
|
||||
if (existing) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Server is already allowed!");
|
||||
void msg.channel.send("Server is already allowed!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSnowflake(args.guildId)) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Invalid server ID!");
|
||||
void msg.channel.send("Invalid server ID!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.userId && !isSnowflake(args.userId)) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Invalid user ID!");
|
||||
void msg.channel.send("Invalid user ID!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,6 @@ export const AllowServerCmd = botControlCmd({
|
|||
);
|
||||
}
|
||||
|
||||
sendSuccessMessage(pluginData, msg.channel, "Server is now allowed to use Zeppelin!");
|
||||
void msg.channel.send("Server is now allowed to use Zeppelin!");
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { isStaffPreFilter, sendErrorMessage } from "../../../pluginUtils.js";
|
||||
import { isStaffPreFilter } from "../../../pluginUtils.js";
|
||||
import { botControlCmd } from "../types.js";
|
||||
|
||||
export const ChannelToServerCmd = botControlCmd({
|
||||
|
@ -16,7 +16,7 @@ export const ChannelToServerCmd = botControlCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const channel = pluginData.client.channels.cache.get(args.channelId);
|
||||
if (!channel) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Channel not found in cache!");
|
||||
void msg.channel.send("Channel not found in cache!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue