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": [
|
"contributors": [
|
||||||
"BanTheNons",
|
|
||||||
"CleverSource",
|
|
||||||
"DarkView",
|
|
||||||
"DenverCoder1",
|
|
||||||
"Jernik",
|
|
||||||
"Rstar284",
|
|
||||||
"almeidx",
|
"almeidx",
|
||||||
"axisiscool",
|
"axisiscool",
|
||||||
|
"BanTheNons",
|
||||||
|
"Benricheson101",
|
||||||
|
"brawaru",
|
||||||
|
"CleverSource",
|
||||||
|
"Dalkskkskk",
|
||||||
|
"DarkView",
|
||||||
|
"DenverCoder1",
|
||||||
"dexbiobot",
|
"dexbiobot",
|
||||||
"greenbigfrog",
|
"greenbigfrog",
|
||||||
|
"hawkeye7662",
|
||||||
|
"iamshoXy",
|
||||||
|
"Jernik",
|
||||||
"k200-1",
|
"k200-1",
|
||||||
|
"LilyBergonzat",
|
||||||
|
"martinbndr",
|
||||||
"metal0",
|
"metal0",
|
||||||
|
"Obliie",
|
||||||
"paolojpa",
|
"paolojpa",
|
||||||
"roflmaoqwerty",
|
"roflmaoqwerty",
|
||||||
|
"Rstar284",
|
||||||
|
"rubyowo",
|
||||||
|
"rukogit",
|
||||||
|
"Scraayp",
|
||||||
|
"TheKodeToad",
|
||||||
"thewilloftheshadow",
|
"thewilloftheshadow",
|
||||||
"usoka",
|
"usoka",
|
||||||
"vcokltfre",
|
"vcokltfre",
|
||||||
"Dragory",
|
"WeebHiroyuki",
|
||||||
"rubyowo",
|
|
||||||
"Dalkskkskk",
|
|
||||||
"iamshoXy",
|
|
||||||
"Scraayp",
|
|
||||||
"app/dependabot",
|
|
||||||
"dependabot[bot]",
|
|
||||||
"zayKenyon",
|
"zayKenyon",
|
||||||
"rukogit",
|
|
||||||
"Obliie",
|
"Dragory",
|
||||||
"brawaru",
|
"app/dependabot",
|
||||||
"Benricheson101",
|
"dependabot[bot]"
|
||||||
"hawkeye7662",
|
|
||||||
"LilyBergonzat",
|
|
||||||
"martinbndr"
|
|
||||||
],
|
],
|
||||||
"message": "Thank you for contributing to Zeppelin! We require contributors to sign our Contributor License Agreement (CLA). To let us review and merge your code, please visit https://github.com/ZeppelinBot/CLA to sign the CLA!"
|
"message": "Thank you for contributing to Zeppelin! We require contributors to sign our Contributor License Agreement (CLA). To let us review and merge your code, please visit https://github.com/ZeppelinBot/CLA to sign the CLA!"
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,10 @@ STAFF=
|
||||||
DEFAULT_ALLOWED_SERVERS=
|
DEFAULT_ALLOWED_SERVERS=
|
||||||
|
|
||||||
# Only required if relevant feature is used
|
# Only required if relevant feature is used
|
||||||
#PHISHERMAN_API_KEY=
|
#FISHFISH_API_KEY=
|
||||||
|
|
||||||
|
#DEFAULT_SUCCESS_EMOJI=
|
||||||
|
#DEFAULT_ERROR_EMOJI=
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# DEVELOPMENT
|
# DEVELOPMENT
|
||||||
|
|
2
.github/workflows/codequality.yml
vendored
2
.github/workflows/codequality.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.16]
|
node-version: [22]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- 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 mkdir /zeppelin
|
||||||
RUN chown node:node /zeppelin
|
RUN chown node:node /zeppelin
|
||||||
|
@ -32,3 +32,8 @@ RUN npm run build
|
||||||
# Prune dev dependencies
|
# Prune dev dependencies
|
||||||
WORKDIR /zeppelin
|
WORKDIR /zeppelin
|
||||||
RUN npm prune --omit=dev
|
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": "",
|
"description": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"./*": "./dist/*"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "tsc-watch --build --onSuccess \"node start-dev.js\"",
|
"watch": "tsc-watch --build --onSuccess \"node start-dev.js\"",
|
||||||
"watch-yaml-parse-test": "tsc-watch --build --onSuccess \"node dist/yamlParseTest.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-prod": "npm run migrate-rollback",
|
||||||
"migrate-rollback-dev": "npm run build && 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",
|
"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",
|
"test": "npm run build && npm run run-tests",
|
||||||
"run-tests": "ava",
|
"run-tests": "ava",
|
||||||
"test-watch": "tsc-watch --build --onSuccess \"npx ava\""
|
"test-watch": "tsc-watch --build --onSuccess \"npx ava\""
|
||||||
|
@ -38,18 +41,18 @@
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"deep-diff": "^1.0.2",
|
"deep-diff": "^1.0.2",
|
||||||
"discord.js": "^14.14.1",
|
"discord.js": "^14.19.3",
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
"escape-string-regexp": "^1.0.5",
|
"escape-string-regexp": "^1.0.5",
|
||||||
"express": "^4.20.0",
|
"express": "^4.20.0",
|
||||||
"fp-ts": "^2.0.1",
|
"fp-ts": "^2.0.1",
|
||||||
"humanize-duration": "^3.15.0",
|
"humanize-duration": "^3.15.0",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^4.1.0",
|
||||||
"knub": "^32.0.0-next.21",
|
"knub": "^32.0.0-next.25",
|
||||||
"knub-command-manager": "^9.1.0",
|
"knub-command-manager": "^9.1.0",
|
||||||
"last-commit-log": "^2.1.0",
|
"last-commit-log": "^2.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"moment-timezone": "^0.5.21",
|
"moment-timezone": "^0.5.21",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
|
@ -72,15 +75,14 @@
|
||||||
"utf-8-validate": "^5.0.5",
|
"utf-8-validate": "^5.0.5",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build",
|
"yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build",
|
||||||
"zlib-sync": "^0.1.7",
|
"zod": "^3.25.17"
|
||||||
"zod": "^3.7.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.5",
|
"@types/cors": "^2.8.5",
|
||||||
"@types/express": "^4.16.1",
|
"@types/express": "^4.16.1",
|
||||||
"@types/jest": "^24.0.15",
|
"@types/jest": "^24.0.15",
|
||||||
"@types/js-yaml": "^3.12.1",
|
"@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/moment-timezone": "^0.5.6",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/passport": "^1.0.0",
|
"@types/passport": "^1.0.0",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { CooldownManager } from "knub";
|
import { CooldownManager } from "knub";
|
||||||
|
import { EventEmitter } from "node:events";
|
||||||
import { RegExpWorker, TimeoutError } from "regexp-worker";
|
import { RegExpWorker, TimeoutError } from "regexp-worker";
|
||||||
import { MINUTES, SECONDS } from "./utils.js";
|
import { MINUTES, SECONDS } from "./utils.js";
|
||||||
import Timeout = NodeJS.Timeout;
|
import Timeout = NodeJS.Timeout;
|
||||||
|
|
|
@ -46,7 +46,7 @@ export class SimpleCache<T = any> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.maxItems && this.store.size > this.maxItems) {
|
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);
|
this.store.delete(keyToDelete);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,130 +1,135 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { guildPlugins } from "../plugins/availablePlugins.js";
|
import { availableGuildPlugins } from "../plugins/availablePlugins.js";
|
||||||
import { guildPluginInfo } from "../plugins/pluginInfo.js";
|
import { ZeppelinGuildPluginInfo } from "../types.js";
|
||||||
import { indentLines } from "../utils.js";
|
import { indentLines } from "../utils.js";
|
||||||
import { notFound } from "./responses.js";
|
import { notFound } from "./responses.js";
|
||||||
|
import { $ZodPipeDef } from "zod/v4/core";
|
||||||
|
|
||||||
function isZodObject(schema: z.ZodTypeAny): schema is z.ZodObject<any> {
|
function isZodObject(schema: z.ZodType): schema is z.ZodObject<any> {
|
||||||
return schema._def.typeName === "ZodObject";
|
return schema.def.type === "object";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZodRecord(schema: z.ZodTypeAny): schema is z.ZodRecord<any> {
|
function isZodRecord(schema: z.ZodType): schema is z.ZodRecord<any> {
|
||||||
return schema._def.typeName === "ZodRecord";
|
return schema.def.type === "record";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZodEffects(schema: z.ZodTypeAny): schema is z.ZodEffects<any, any> {
|
function isZodOptional(schema: z.ZodType): schema is z.ZodOptional<any> {
|
||||||
return schema._def.typeName === "ZodEffects";
|
return schema.def.type === "optional";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZodOptional(schema: z.ZodTypeAny): schema is z.ZodOptional<any> {
|
function isZodArray(schema: z.ZodType): schema is z.ZodArray<any> {
|
||||||
return schema._def.typeName === "ZodOptional";
|
return schema.def.type === "array";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZodArray(schema: z.ZodTypeAny): schema is z.ZodArray<any> {
|
function isZodUnion(schema: z.ZodType): schema is z.ZodUnion<any> {
|
||||||
return schema._def.typeName === "ZodArray";
|
return schema.def.type === "union";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZodUnion(schema: z.ZodTypeAny): schema is z.ZodUnion<any> {
|
function isZodNullable(schema: z.ZodType): schema is z.ZodNullable<any> {
|
||||||
return schema._def.typeName === "ZodUnion";
|
return schema.def.type === "nullable";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZodNullable(schema: z.ZodTypeAny): schema is z.ZodNullable<any> {
|
function isZodDefault(schema: z.ZodType): schema is z.ZodDefault<any> {
|
||||||
return schema._def.typeName === "ZodNullable";
|
return schema.def.type === "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZodDefault(schema: z.ZodTypeAny): schema is z.ZodDefault<any> {
|
function isZodLiteral(schema: z.ZodType): schema is z.ZodLiteral<any> {
|
||||||
return schema._def.typeName === "ZodDefault";
|
return schema.def.type === "literal";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZodLiteral(schema: z.ZodTypeAny): schema is z.ZodLiteral<any> {
|
function isZodIntersection(schema: z.ZodType): schema is z.ZodIntersection<any, any> {
|
||||||
return schema._def.typeName === "ZodLiteral";
|
return schema.def.type === "intersection";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZodIntersection(schema: z.ZodTypeAny): schema is z.ZodIntersection<any, any> {
|
function formatZodConfigSchema(schema: z.ZodType) {
|
||||||
return schema._def.typeName === "ZodIntersection";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatZodConfigSchema(schema: z.ZodTypeAny) {
|
|
||||||
if (isZodObject(schema)) {
|
if (isZodObject(schema)) {
|
||||||
return (
|
return (
|
||||||
`{\n` +
|
`{\n` +
|
||||||
Object.entries(schema._def.shape())
|
Object.entries(schema.def.shape)
|
||||||
.map(([k, value]) => indentLines(`${k}: ${formatZodConfigSchema(value as z.ZodTypeAny)}`, 2))
|
.map(([k, value]) => indentLines(`${k}: ${formatZodConfigSchema(value as z.ZodType)}`, 2))
|
||||||
.join("\n") +
|
.join("\n") +
|
||||||
"\n}"
|
"\n}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isZodRecord(schema)) {
|
if (isZodRecord(schema)) {
|
||||||
return "{\n" + indentLines(`[string]: ${formatZodConfigSchema(schema._def.valueType)}`, 2) + "\n}";
|
return "{\n" + indentLines(`[string]: ${formatZodConfigSchema(schema.valueType as z.ZodType)}`, 2) + "\n}";
|
||||||
}
|
|
||||||
if (isZodEffects(schema)) {
|
|
||||||
return formatZodConfigSchema(schema._def.schema);
|
|
||||||
}
|
}
|
||||||
if (isZodOptional(schema)) {
|
if (isZodOptional(schema)) {
|
||||||
return `Optional<${formatZodConfigSchema(schema._def.innerType)}>`;
|
return `Optional<${formatZodConfigSchema(schema.def.innerType)}>`;
|
||||||
}
|
}
|
||||||
if (isZodArray(schema)) {
|
if (isZodArray(schema)) {
|
||||||
return `Array<${formatZodConfigSchema(schema._def.type)}>`;
|
return `Array<${formatZodConfigSchema(schema.def.element)}>`;
|
||||||
}
|
}
|
||||||
if (isZodUnion(schema)) {
|
if (isZodUnion(schema)) {
|
||||||
return schema._def.options.map((t) => formatZodConfigSchema(t)).join(" | ");
|
return schema.def.options.map((t) => formatZodConfigSchema(t)).join(" | ");
|
||||||
}
|
}
|
||||||
if (isZodNullable(schema)) {
|
if (isZodNullable(schema)) {
|
||||||
return `Nullable<${formatZodConfigSchema(schema._def.innerType)}>`;
|
return `Nullable<${formatZodConfigSchema(schema.def.innerType)}>`;
|
||||||
}
|
}
|
||||||
if (isZodDefault(schema)) {
|
if (isZodDefault(schema)) {
|
||||||
return formatZodConfigSchema(schema._def.innerType);
|
return formatZodConfigSchema(schema.def.innerType);
|
||||||
}
|
}
|
||||||
if (isZodLiteral(schema)) {
|
if (isZodLiteral(schema)) {
|
||||||
return schema._def.value;
|
return schema.def.values;
|
||||||
}
|
}
|
||||||
if (isZodIntersection(schema)) {
|
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";
|
return "string";
|
||||||
}
|
}
|
||||||
if (schema._def.typeName === "ZodNumber") {
|
if (schema.def.type === "number") {
|
||||||
return "number";
|
return "number";
|
||||||
}
|
}
|
||||||
if (schema._def.typeName === "ZodBoolean") {
|
if (schema.def.type === "boolean") {
|
||||||
return "boolean";
|
return "boolean";
|
||||||
}
|
}
|
||||||
if (schema._def.typeName === "ZodNever") {
|
if (schema.def.type === "never") {
|
||||||
return "never";
|
return "never";
|
||||||
}
|
}
|
||||||
|
if (schema.def.type === "pipe") {
|
||||||
|
return formatZodConfigSchema((schema.def as $ZodPipeDef).in as z.ZodType);
|
||||||
|
}
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const availableGuildPluginsByName = availableGuildPlugins.reduce<Record<string, ZeppelinGuildPluginInfo>>(
|
||||||
|
(map, obj) => {
|
||||||
|
map[obj.plugin.name] = obj;
|
||||||
|
return map;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
export function initDocs(router: express.Router) {
|
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) => {
|
router.get("/docs/plugins", (req: express.Request, res: express.Response) => {
|
||||||
res.json(
|
res.json(
|
||||||
docsPluginNames.map((pluginName) => {
|
docsPlugins.map((obj) => ({
|
||||||
const info = guildPluginInfo[pluginName];
|
name: obj.plugin.name,
|
||||||
const thinInfo = info ? { prettyName: info.prettyName, legacy: info.legacy ?? false } : {};
|
info: {
|
||||||
return {
|
prettyName: obj.docs.prettyName,
|
||||||
name: pluginName,
|
type: obj.docs.type,
|
||||||
info: thinInfo,
|
},
|
||||||
};
|
})),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/docs/plugins/:pluginName", (req: express.Request, res: express.Response) => {
|
router.get("/docs/plugins/:pluginName", (req: express.Request, res: express.Response) => {
|
||||||
const name = req.params.pluginName;
|
const pluginInfo = availableGuildPluginsByName[req.params.pluginName];
|
||||||
const baseInfo = guildPluginInfo[name];
|
if (!pluginInfo) {
|
||||||
if (!baseInfo) {
|
|
||||||
return notFound(res);
|
return notFound(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = guildPlugins.find((p) => p.name === name)!;
|
const { configSchema, ...info } = pluginInfo.docs;
|
||||||
const { configSchema, ...info } = baseInfo;
|
|
||||||
const formattedConfigSchema = formatZodConfigSchema(configSchema);
|
const formattedConfigSchema = formatZodConfigSchema(configSchema);
|
||||||
|
|
||||||
const messageCommands = (plugin.messageCommands || []).map((cmd) => ({
|
const messageCommands = (pluginInfo.plugin.messageCommands || []).map((cmd) => ({
|
||||||
trigger: cmd.trigger,
|
trigger: cmd.trigger,
|
||||||
permission: cmd.permission,
|
permission: cmd.permission,
|
||||||
signature: cmd.signature,
|
signature: cmd.signature,
|
||||||
|
@ -133,10 +138,10 @@ export function initDocs(router: express.Router) {
|
||||||
config: cmd.config,
|
config: cmd.config,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const defaultOptions = plugin.defaultOptions || {};
|
const defaultOptions = pluginInfo.docs.configSchema.safeParse({}).data ?? {};
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
name,
|
name: pluginInfo.plugin.name,
|
||||||
info,
|
info,
|
||||||
configSchema: formattedConfigSchema,
|
configSchema: formattedConfigSchema,
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { GuildCases } from "../../data/GuildCases.js";
|
import { GuildCases } from "../../data/GuildCases.js";
|
||||||
import { Case } from "../../data/entities/Case.js";
|
import { Case } from "../../data/entities/Case.js";
|
||||||
import { MINUTES } from "../../utils.js";
|
import { MINUTES } from "../../utils.js";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import jsYaml from "js-yaml";
|
import { YAMLException } from "js-yaml";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { Queue } from "../../Queue.js";
|
import { Queue } from "../../Queue.js";
|
||||||
import { validateGuildConfig } from "../../configValidator.js";
|
import { validateGuildConfig } from "../../configValidator.js";
|
||||||
|
@ -15,8 +15,6 @@ import { ObjectAliasError } from "../../utils/validateNoObjectAliases.js";
|
||||||
import { hasGuildPermission, requireGuildPermission } from "../permissions.js";
|
import { hasGuildPermission, requireGuildPermission } from "../permissions.js";
|
||||||
import { clientError, ok, serverError, unauthorized } from "../responses.js";
|
import { clientError, ok, serverError, unauthorized } from "../responses.js";
|
||||||
|
|
||||||
const YAMLException = jsYaml.YAMLException;
|
|
||||||
|
|
||||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||||
const auditLog = new ApiAuditLog();
|
const auditLog = new ApiAuditLog();
|
||||||
|
|
||||||
|
|
|
@ -28,10 +28,10 @@ app.use(multer().none());
|
||||||
|
|
||||||
const rootRouter = express.Router();
|
const rootRouter = express.Router();
|
||||||
|
|
||||||
initAuth(app);
|
initAuth(rootRouter);
|
||||||
initGuildsAPI(app);
|
initGuildsAPI(rootRouter);
|
||||||
initArchives(app);
|
initArchives(rootRouter);
|
||||||
initDocs(app);
|
initDocs(rootRouter);
|
||||||
|
|
||||||
// Default route
|
// Default route
|
||||||
rootRouter.get("/", (req, res) => {
|
rootRouter.get("/", (req, res) => {
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { ConfigValidationError, GuildPluginBlueprint, PluginConfigManager } from "knub";
|
import { BaseConfig, ConfigValidationError, GuildPluginBlueprint, PluginConfigManager } from "knub";
|
||||||
import moment from "moment-timezone";
|
import { z, ZodError } from "zod/v4";
|
||||||
import { ZodError } from "zod";
|
import { availableGuildPlugins } from "./plugins/availablePlugins.js";
|
||||||
import { guildPlugins } from "./plugins/availablePlugins.js";
|
import { zZeppelinGuildConfig } from "./types.js";
|
||||||
import { ZeppelinGuildConfig, zZeppelinGuildConfig } from "./types.js";
|
|
||||||
import { formatZodIssue } from "./utils/formatZodIssue.js";
|
import { formatZodIssue } from "./utils/formatZodIssue.js";
|
||||||
|
|
||||||
const pluginNameToPlugin = new Map<string, GuildPluginBlueprint<any, any>>();
|
const pluginNameToPlugin = new Map<string, GuildPluginBlueprint<any, any>>();
|
||||||
for (const plugin of guildPlugins) {
|
for (const pluginInfo of availableGuildPlugins) {
|
||||||
pluginNameToPlugin.set(plugin.name, plugin);
|
pluginNameToPlugin.set(pluginInfo.plugin.name, pluginInfo.plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateGuildConfig(config: any): Promise<string | null> {
|
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");
|
return validationResult.error.issues.map(formatZodIssue).join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const guildConfig = config as ZeppelinGuildConfig;
|
const guildConfig = config as BaseConfig;
|
||||||
|
|
||||||
if (guildConfig.timezone) {
|
|
||||||
const validTimezones = moment.tz.names();
|
|
||||||
if (!validTimezones.includes(guildConfig.timezone)) {
|
|
||||||
return `Invalid timezone: ${guildConfig.timezone}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (guildConfig.plugins) {
|
if (guildConfig.plugins) {
|
||||||
for (const [pluginName, pluginOptions] of Object.entries(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 plugin = pluginNameToPlugin.get(pluginName)!;
|
||||||
const configManager = new PluginConfigManager(plugin.defaultOptions || { config: {} }, pluginOptions, {
|
const configManager = new PluginConfigManager(
|
||||||
levels: {},
|
pluginOptions,
|
||||||
parser: plugin.configParser,
|
{
|
||||||
});
|
configSchema: plugin.configSchema,
|
||||||
|
defaultOverrides: plugin.defaultOverrides ?? [],
|
||||||
|
levels: {},
|
||||||
|
customOverrideCriteriaFunctions: plugin.customOverrideCriteriaFunctions,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await configManager.init();
|
await configManager.init();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ZodError) {
|
if (err instanceof ZodError) {
|
||||||
return `${pluginName}: ${err.issues.map(formatZodIssue).join("\n")}`;
|
return `${pluginName}:\n${z.prettifyError(err)}`;
|
||||||
}
|
}
|
||||||
if (err instanceof ConfigValidationError) {
|
if (err instanceof ConfigValidationError) {
|
||||||
return `${pluginName}: ${err.message}`;
|
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 { Queue } from "../Queue.js";
|
||||||
import { chunkArray } from "../utils.js";
|
import { chunkArray } from "../utils.js";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository.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({
|
return this.cases.find({
|
||||||
relations: this.getRelations(),
|
relations: this.getRelations(),
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId,
|
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({
|
return this.cases.count({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
mod_id: modId,
|
mod_id: modId,
|
||||||
is_hidden: false,
|
is_hidden: false,
|
||||||
|
...filters,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecentByModId(modId: string, count: number, skip = 0): Promise<Case[]> {
|
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({
|
return this.cases.find({
|
||||||
relations: this.getRelations(),
|
relations: this.getRelations(),
|
||||||
where: {
|
where,
|
||||||
guild_id: this.guildId,
|
|
||||||
mod_id: modId,
|
|
||||||
is_hidden: false,
|
|
||||||
},
|
|
||||||
skip,
|
skip,
|
||||||
take: count,
|
take: count,
|
||||||
order: {
|
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}`;
|
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);
|
const mute = await getMutesRepository().findMute(guildId, userId);
|
||||||
if (!mute) {
|
if (!mute) {
|
||||||
// Mute was already cleared
|
// Mute was already cleared
|
||||||
|
@ -27,7 +27,7 @@ async function broadcastExpiredMute(guildId: string, userId: string, tries = 0)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[EXPIRING MUTES LOOP] Broadcasting expired mute: ${mute.guild_id}/${mute.user_id}`);
|
// console.log(`[EXPIRING MUTES LOOP] Broadcasting expired mute: ${mute.guild_id}/${mute.user_id}`);
|
||||||
if (!hasGuildEventListener(mute.guild_id, "expiredMute")) {
|
if (!hasGuildEventListener(mute.guild_id, "expiredMute")) {
|
||||||
// If there are no listeners registered for the server yet, try again in a bit
|
// If there are no listeners registered for the server yet, try again in a bit
|
||||||
if (tries < MAX_TRIES_PER_SERVER) {
|
if (tries < MAX_TRIES_PER_SERVER) {
|
||||||
|
@ -42,7 +42,7 @@ async function broadcastExpiredMute(guildId: string, userId: string, tries = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastTimeoutMuteToRenew(mute: Mute, 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 (!hasGuildEventListener(mute.guild_id, "timeoutMuteToRenew")) {
|
||||||
// If there are no listeners registered for the server yet, try again in a bit
|
// If there are no listeners registered for the server yet, try again in a bit
|
||||||
if (tries < MAX_TRIES_PER_SERVER) {
|
if (tries < MAX_TRIES_PER_SERVER) {
|
||||||
|
|
|
@ -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 dotenv from "dotenv";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { rootDir } from "./paths.js";
|
import { rootDir } from "./paths.js";
|
||||||
|
|
||||||
const envType = z.object({
|
const envType = z.object({
|
||||||
|
@ -37,6 +37,10 @@ const envType = z.object({
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
PHISHERMAN_API_KEY: z.string().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_HOST: z.string().optional(),
|
||||||
DB_PORT: z.preprocess((v) => Number(v), z.number()).optional(),
|
DB_PORT: z.preprocess((v) => Number(v), z.number()).optional(),
|
||||||
|
|
|
@ -1,23 +1,91 @@
|
||||||
import { z } from "zod";
|
import fs from "node:fs";
|
||||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
import { z } from "zod/v4";
|
||||||
import { guildPluginInfo } from "./plugins/pluginInfo.js";
|
import { availableGuildPlugins } from "./plugins/availablePlugins.js";
|
||||||
import { zZeppelinGuildConfig } from "./types.js";
|
import { zZeppelinGuildConfig } from "./types.js";
|
||||||
|
import { deepPartial } from "./utils/zodDeepPartial.js";
|
||||||
|
|
||||||
const pluginSchemaMap = Object.entries(guildPluginInfo).reduce((map, [pluginName, pluginInfo]) => {
|
const basePluginOverrideCriteriaSchema = z.strictObject({
|
||||||
if (pluginInfo.configSchema) {
|
channel: z
|
||||||
map[pluginName] = pluginInfo.configSchema;
|
.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;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const fullSchema = zZeppelinGuildConfig.omit({ plugins: true }).merge(
|
const fullSchema = zZeppelinGuildConfig.omit({ plugins: true }).extend({
|
||||||
z.strictObject({
|
plugins: z.strictObject(pluginSchemaMap).partial().optional(),
|
||||||
plugins: z.strictObject(pluginSchemaMap).partial(),
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
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,
|
TextChannel,
|
||||||
ThreadChannel,
|
ThreadChannel,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { Knub, PluginError, PluginLoadError, PluginNotLoadedError } from "knub";
|
import { Knub, PluginError, PluginLoadError, PluginNotLoadedError } from "knub";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { performance } from "perf_hooks";
|
import { performance } from "perf_hooks";
|
||||||
|
@ -22,9 +21,9 @@ import { RecoverablePluginError } from "./RecoverablePluginError.js";
|
||||||
import { SimpleError } from "./SimpleError.js";
|
import { SimpleError } from "./SimpleError.js";
|
||||||
import { AllowedGuilds } from "./data/AllowedGuilds.js";
|
import { AllowedGuilds } from "./data/AllowedGuilds.js";
|
||||||
import { Configs } from "./data/Configs.js";
|
import { Configs } from "./data/Configs.js";
|
||||||
|
import { FishFishError, initFishFish } from "./data/FishFish.js";
|
||||||
import { GuildLogs } from "./data/GuildLogs.js";
|
import { GuildLogs } from "./data/GuildLogs.js";
|
||||||
import { LogType } from "./data/LogType.js";
|
import { LogType } from "./data/LogType.js";
|
||||||
import { hasPhishermanMasterAPIKey } from "./data/Phisherman.js";
|
|
||||||
import { dataSource } from "./data/dataSource.js";
|
import { dataSource } from "./data/dataSource.js";
|
||||||
import { connect } from "./data/db.js";
|
import { connect } from "./data/db.js";
|
||||||
import { runExpiredArchiveDeletionLoop } from "./data/loops/expiredArchiveDeletionLoop.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 { runExpiringTempbansLoop } from "./data/loops/expiringTempbansLoop.js";
|
||||||
import { runExpiringVCAlertsLoop } from "./data/loops/expiringVCAlertsLoop.js";
|
import { runExpiringVCAlertsLoop } from "./data/loops/expiringVCAlertsLoop.js";
|
||||||
import { runMemberCacheDeletionLoop } from "./data/loops/memberCacheDeletionLoop.js";
|
import { runMemberCacheDeletionLoop } from "./data/loops/memberCacheDeletionLoop.js";
|
||||||
import { runPhishermanCacheCleanupLoop, runPhishermanReportingLoop } from "./data/loops/phishermanLoops.js";
|
|
||||||
import { runSavedMessageCleanupLoop } from "./data/loops/savedMessageCleanupLoop.js";
|
import { runSavedMessageCleanupLoop } from "./data/loops/savedMessageCleanupLoop.js";
|
||||||
import { runUpcomingRemindersLoop } from "./data/loops/upcomingRemindersLoop.js";
|
import { runUpcomingRemindersLoop } from "./data/loops/upcomingRemindersLoop.js";
|
||||||
import { runUpcomingScheduledPostsLoop } from "./data/loops/upcomingScheduledPostsLoop.js";
|
import { runUpcomingScheduledPostsLoop } from "./data/loops/upcomingScheduledPostsLoop.js";
|
||||||
import { consumeQueryStats } from "./data/queryLogger.js";
|
import { consumeQueryStats } from "./data/queryLogger.js";
|
||||||
import { env } from "./env.js";
|
import { env } from "./env.js";
|
||||||
import { logger } from "./logger.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 { setProfiler } from "./profiler.js";
|
||||||
import { logRateLimit } from "./rateLimitStats.js";
|
import { logRateLimit } from "./rateLimitStats.js";
|
||||||
import { startUptimeCounter } from "./uptime.js";
|
import { startUptimeCounter } from "./uptime.js";
|
||||||
|
@ -143,6 +141,12 @@ function errorHandler(err) {
|
||||||
return;
|
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
|
// tslint:disable:no-console
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
|
@ -166,10 +170,8 @@ function errorHandler(err) {
|
||||||
// tslint:enable:no-console
|
// tslint:enable:no-console
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
process.on("uncaughtException", errorHandler);
|
||||||
process.on("uncaughtException", errorHandler);
|
process.on("unhandledRejection", errorHandler);
|
||||||
process.on("unhandledRejection", errorHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify required Node.js version
|
// Verify required Node.js version
|
||||||
const REQUIRED_NODE_VERSION = "16.9.0";
|
const REQUIRED_NODE_VERSION = "16.9.0";
|
||||||
|
@ -252,8 +254,8 @@ connect().then(async () => {
|
||||||
GatewayIntentBits.GuildVoiceStates,
|
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 safe429DecayInterval = 5 * SECONDS;
|
||||||
const safe429MaxCount = 5;
|
const safe429MaxCount = 5;
|
||||||
|
@ -275,6 +277,10 @@ connect().then(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("error", (err) => {
|
client.on("error", (err) => {
|
||||||
|
if (err instanceof PluginLoadError) {
|
||||||
|
errorHandler(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
errorHandler(new DiscordJSError(err.message, (err as any).code, 0));
|
errorHandler(new DiscordJSError(err.message, (err as any).code, 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -282,8 +288,8 @@ connect().then(async () => {
|
||||||
const guildConfigs = new Configs();
|
const guildConfigs = new Configs();
|
||||||
|
|
||||||
const bot = new Knub(client, {
|
const bot = new Knub(client, {
|
||||||
guildPlugins,
|
guildPlugins: availableGuildPlugins.map((obj) => obj.plugin),
|
||||||
globalPlugins,
|
globalPlugins: availableGlobalPlugins.map((obj) => obj.plugin),
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
canLoadGuild(guildId): Promise<boolean> {
|
canLoadGuild(guildId): Promise<boolean> {
|
||||||
|
@ -292,7 +298,7 @@ connect().then(async () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugins are enabled if they...
|
* 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
|
* - are explicitly enabled in the guild config
|
||||||
* Dependencies are also automatically loaded by Knub.
|
* Dependencies are also automatically loaded by Knub.
|
||||||
*/
|
*/
|
||||||
|
@ -302,10 +308,10 @@ connect().then(async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const configuredPlugins = ctx.config.plugins;
|
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) => {
|
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;
|
return configuredPlugins[pluginName] && (configuredPlugins[pluginName] as any).enabled !== false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -323,12 +329,30 @@ connect().then(async () => {
|
||||||
if (row) {
|
if (row) {
|
||||||
try {
|
try {
|
||||||
const loaded = loadYamlSafely(row.config);
|
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
|
// Remove deprecated properties some may still have in their config
|
||||||
delete loaded.success_emoji;
|
delete loaded.success_emoji;
|
||||||
delete loaded.error_emoji;
|
delete loaded.error_emoji;
|
||||||
|
|
||||||
return loaded;
|
return loaded;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Error while loading config "${key}": ${err.message}`);
|
logger.error(`Error while loading config "${key}"`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -385,6 +409,8 @@ connect().then(async () => {
|
||||||
enableProfiling();
|
enableProfiling();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initFishFish();
|
||||||
|
|
||||||
runExpiringMutesLoop();
|
runExpiringMutesLoop();
|
||||||
await sleep(10 * SECONDS);
|
await sleep(10 * SECONDS);
|
||||||
runExpiringTempbansLoop();
|
runExpiringTempbansLoop();
|
||||||
|
@ -402,13 +428,6 @@ connect().then(async () => {
|
||||||
runExpiredMemberCacheDeletionLoop();
|
runExpiredMemberCacheDeletionLoop();
|
||||||
await sleep(10 * SECONDS);
|
await sleep(10 * SECONDS);
|
||||||
runMemberCacheDeletionLoop();
|
runMemberCacheDeletionLoop();
|
||||||
|
|
||||||
if (hasPhishermanMasterAPIKey()) {
|
|
||||||
await sleep(10 * SECONDS);
|
|
||||||
runPhishermanCacheCleanupLoop();
|
|
||||||
await sleep(10 * SECONDS);
|
|
||||||
runPhishermanReportingLoop();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let lowestGlobalRemaining = Infinity;
|
let lowestGlobalRemaining = Infinity;
|
||||||
|
|
|
@ -9,7 +9,7 @@ export class MigrateUsernamesToNewHistoryTable1556909512501 implements Migration
|
||||||
|
|
||||||
const migratedUsernames = new Set();
|
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");
|
const stream = await queryRunner.stream("SELECT CONCAT(user_id, '-', username) AS `key` FROM username_history");
|
||||||
stream.on("data", (row: any) => {
|
stream.on("data", (row: any) => {
|
||||||
migratedUsernames.add(row.key);
|
migratedUsernames.add(row.key);
|
||||||
|
|
|
@ -3,18 +3,35 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
BitField,
|
||||||
|
BitFieldResolvable,
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
CommandInteraction,
|
||||||
GuildMember,
|
GuildMember,
|
||||||
|
InteractionEditReplyOptions,
|
||||||
|
InteractionReplyOptions,
|
||||||
|
InteractionResponse,
|
||||||
Message,
|
Message,
|
||||||
MessageCreateOptions,
|
MessageCreateOptions,
|
||||||
MessageMentionOptions,
|
MessageEditOptions,
|
||||||
|
MessageFlags,
|
||||||
|
MessageFlagsString,
|
||||||
|
ModalSubmitInteraction,
|
||||||
PermissionsBitField,
|
PermissionsBitField,
|
||||||
TextBasedChannel,
|
TextBasedChannel,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { AnyPluginData, BasePluginData, CommandContext, ExtendedMatchParams, GuildPluginData, helpers } from "knub";
|
import {
|
||||||
import { logger } from "./logger.js";
|
AnyPluginData,
|
||||||
|
BasePluginData,
|
||||||
|
CommandContext,
|
||||||
|
ExtendedMatchParams,
|
||||||
|
GuildPluginData,
|
||||||
|
helpers,
|
||||||
|
PluginConfigManager,
|
||||||
|
} from "knub";
|
||||||
|
import z from "zod/v4";
|
||||||
import { isStaff } from "./staff.js";
|
import { isStaff } from "./staff.js";
|
||||||
import { TZeppelinKnub } from "./types.js";
|
import { TZeppelinKnub } from "./types.js";
|
||||||
import { errorMessage, successMessage } from "./utils.js";
|
|
||||||
import { Tail } from "./utils/typeUtils.js";
|
import { Tail } from "./utils/typeUtils.js";
|
||||||
|
|
||||||
const { getMemberLevel } = helpers;
|
const { getMemberLevel } = helpers;
|
||||||
|
@ -49,46 +66,118 @@ export async function hasPermission(
|
||||||
return helpers.hasPermission(config, permission);
|
return helpers.hasPermission(config, permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendSuccessMessage(
|
export type GenericCommandSource = Message | CommandInteraction | ModalSubmitInteraction;
|
||||||
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 };
|
|
||||||
|
|
||||||
return channel
|
export function isContextInteraction(
|
||||||
.send({ ...content }) // Force line break
|
context: GenericCommandSource,
|
||||||
.catch((err) => {
|
): context is CommandInteraction | ModalSubmitInteraction {
|
||||||
const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id;
|
return context instanceof CommandInteraction || context instanceof ModalSubmitInteraction;
|
||||||
logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`);
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendErrorMessage(
|
export function isContextMessage(context: GenericCommandSource): context is Message {
|
||||||
pluginData: AnyPluginData<any>,
|
return context instanceof Message;
|
||||||
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 };
|
|
||||||
|
|
||||||
return channel
|
export async function getContextChannel(context: GenericCommandSource): Promise<TextBasedChannel | null> {
|
||||||
.send({ ...content }) // Force line break
|
if (isContextInteraction(context)) {
|
||||||
.catch((err) => {
|
return context.channel;
|
||||||
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}`);
|
if (context instanceof Message) {
|
||||||
return undefined;
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBaseUrl(pluginData: AnyPluginData<any>) {
|
export function getBaseUrl(pluginData: AnyPluginData<any>) {
|
||||||
|
@ -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 { GuildLogs } from "../../data/GuildLogs.js";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
||||||
import { LogsPlugin } from "../Logs/LogsPlugin.js";
|
import { LogsPlugin } from "../Logs/LogsPlugin.js";
|
||||||
|
@ -8,19 +8,11 @@ import { onMessageCreate } from "./util/onMessageCreate.js";
|
||||||
import { onMessageDelete } from "./util/onMessageDelete.js";
|
import { onMessageDelete } from "./util/onMessageDelete.js";
|
||||||
import { onMessageDeleteBulk } from "./util/onMessageDeleteBulk.js";
|
import { onMessageDeleteBulk } from "./util/onMessageDeleteBulk.js";
|
||||||
|
|
||||||
const defaultOptions: PluginOptions<AutoDeletePluginType> = {
|
|
||||||
config: {
|
|
||||||
enabled: false,
|
|
||||||
delay: "5s",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AutoDeletePlugin = guildPlugin<AutoDeletePluginType>()({
|
export const AutoDeletePlugin = guildPlugin<AutoDeletePluginType>()({
|
||||||
name: "auto_delete",
|
name: "auto_delete",
|
||||||
|
|
||||||
dependencies: () => [TimeAndDatePlugin, LogsPlugin],
|
dependencies: () => [TimeAndDatePlugin, LogsPlugin],
|
||||||
configParser: (input) => zAutoDeleteConfig.parse(input),
|
configSchema: zAutoDeleteConfig,
|
||||||
defaultOptions,
|
|
||||||
|
|
||||||
beforeLoad(pluginData) {
|
beforeLoad(pluginData) {
|
||||||
const { state, guild } = pluginData;
|
const { state, guild } = pluginData;
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { ZeppelinPluginInfo } from "../../types.js";
|
import { ZeppelinPluginDocs } from "../../types.js";
|
||||||
import { zAutoDeleteConfig } from "./types.js";
|
import { zAutoDeleteConfig } from "./types.js";
|
||||||
|
|
||||||
export const autoDeletePluginInfo: ZeppelinPluginInfo = {
|
export const autoDeletePluginDocs: ZeppelinPluginDocs = {
|
||||||
showInDocs: true,
|
type: "stable",
|
||||||
|
configSchema: zAutoDeleteConfig,
|
||||||
|
|
||||||
prettyName: "Auto-delete",
|
prettyName: "Auto-delete",
|
||||||
description: "Allows Zeppelin to auto-delete messages from a channel after a delay",
|
description: "Allows Zeppelin to auto-delete messages from a channel after a delay",
|
||||||
configurationGuide: "Maximum deletion delay is currently 5 minutes",
|
configurationGuide: "Maximum deletion delay is currently 5 minutes",
|
||||||
configSchema: zAutoDeleteConfig,
|
|
||||||
};
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
import { BasePluginType } from "knub";
|
import { BasePluginType } from "knub";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { GuildLogs } from "../../data/GuildLogs.js";
|
import { GuildLogs } from "../../data/GuildLogs.js";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
||||||
import { SavedMessage } from "../../data/entities/SavedMessage.js";
|
import { SavedMessage } from "../../data/entities/SavedMessage.js";
|
||||||
|
@ -14,12 +14,12 @@ export interface IDeletionQueueItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const zAutoDeleteConfig = z.strictObject({
|
export const zAutoDeleteConfig = z.strictObject({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean().default(false),
|
||||||
delay: zDelayString,
|
delay: zDelayString.default("5s"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface AutoDeletePluginType extends BasePluginType {
|
export interface AutoDeletePluginType extends BasePluginType {
|
||||||
config: z.output<typeof zAutoDeleteConfig>;
|
configSchema: typeof zAutoDeleteConfig;
|
||||||
state: {
|
state: {
|
||||||
guildSavedMessages: GuildSavedMessages;
|
guildSavedMessages: GuildSavedMessages;
|
||||||
guildLogs: GuildLogs;
|
guildLogs: GuildLogs;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ChannelType, PermissionsBitField, Snowflake } from "discord.js";
|
import { PermissionsBitField, Snowflake } from "discord.js";
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { LogType } from "../../../data/LogType.js";
|
import { LogType } from "../../../data/LogType.js";
|
||||||
|
@ -17,8 +17,8 @@ export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePlugi
|
||||||
scheduleNextDeletion(pluginData);
|
scheduleNextDeletion(pluginData);
|
||||||
|
|
||||||
const channel = pluginData.guild.channels.cache.get(itemToDelete.message.channel_id as Snowflake);
|
const channel = pluginData.guild.channels.cache.get(itemToDelete.message.channel_id as Snowflake);
|
||||||
if (!channel || channel.type === ChannelType.GuildCategory) {
|
if (!channel || !("messages" in channel)) {
|
||||||
// Channel was deleted, ignore
|
// Channel does not exist or does not support messages, ignore
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
import { PluginOptions, guildPlugin } from "knub";
|
import { PluginOverride, guildPlugin } from "knub";
|
||||||
import { GuildAutoReactions } from "../../data/GuildAutoReactions.js";
|
import { GuildAutoReactions } from "../../data/GuildAutoReactions.js";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
||||||
|
import { CommonPlugin } from "../Common/CommonPlugin.js";
|
||||||
import { LogsPlugin } from "../Logs/LogsPlugin.js";
|
import { LogsPlugin } from "../Logs/LogsPlugin.js";
|
||||||
import { DisableAutoReactionsCmd } from "./commands/DisableAutoReactionsCmd.js";
|
import { DisableAutoReactionsCmd } from "./commands/DisableAutoReactionsCmd.js";
|
||||||
import { NewAutoReactionsCmd } from "./commands/NewAutoReactionsCmd.js";
|
import { NewAutoReactionsCmd } from "./commands/NewAutoReactionsCmd.js";
|
||||||
import { AddReactionsEvt } from "./events/AddReactionsEvt.js";
|
import { AddReactionsEvt } from "./events/AddReactionsEvt.js";
|
||||||
import { AutoReactionsPluginType, zAutoReactionsConfig } from "./types.js";
|
import { AutoReactionsPluginType, zAutoReactionsConfig } from "./types.js";
|
||||||
|
|
||||||
const defaultOptions: PluginOptions<AutoReactionsPluginType> = {
|
const defaultOverrides: Array<PluginOverride<AutoReactionsPluginType>> = [
|
||||||
config: {
|
{
|
||||||
can_manage: false,
|
level: ">=100",
|
||||||
},
|
config: {
|
||||||
overrides: [
|
can_manage: true,
|
||||||
{
|
|
||||||
level: ">=100",
|
|
||||||
config: {
|
|
||||||
can_manage: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
export const AutoReactionsPlugin = guildPlugin<AutoReactionsPluginType>()({
|
export const AutoReactionsPlugin = guildPlugin<AutoReactionsPluginType>()({
|
||||||
name: "auto_reactions",
|
name: "auto_reactions",
|
||||||
|
@ -29,8 +25,8 @@ export const AutoReactionsPlugin = guildPlugin<AutoReactionsPluginType>()({
|
||||||
LogsPlugin,
|
LogsPlugin,
|
||||||
],
|
],
|
||||||
|
|
||||||
configParser: (input) => zAutoReactionsConfig.parse(input),
|
configSchema: zAutoReactionsConfig,
|
||||||
defaultOptions,
|
defaultOverrides,
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
messageCommands: [
|
messageCommands: [
|
||||||
|
@ -50,4 +46,8 @@ export const AutoReactionsPlugin = guildPlugin<AutoReactionsPluginType>()({
|
||||||
state.autoReactions = GuildAutoReactions.getGuildInstance(guild.id);
|
state.autoReactions = GuildAutoReactions.getGuildInstance(guild.id);
|
||||||
state.cache = new Map();
|
state.cache = new Map();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
beforeStart(pluginData) {
|
||||||
|
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
|
|
||||||
import { autoReactionsCmd } from "../types.js";
|
import { autoReactionsCmd } from "../types.js";
|
||||||
|
|
||||||
export const DisableAutoReactionsCmd = autoReactionsCmd({
|
export const DisableAutoReactionsCmd = autoReactionsCmd({
|
||||||
|
@ -14,12 +13,12 @@ export const DisableAutoReactionsCmd = autoReactionsCmd({
|
||||||
async run({ message: msg, args, pluginData }) {
|
async run({ message: msg, args, pluginData }) {
|
||||||
const autoReaction = await pluginData.state.autoReactions.getForChannel(args.channelId);
|
const autoReaction = await pluginData.state.autoReactions.getForChannel(args.channelId);
|
||||||
if (!autoReaction) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await pluginData.state.autoReactions.removeFromChannel(args.channelId);
|
await pluginData.state.autoReactions.removeFromChannel(args.channelId);
|
||||||
pluginData.state.cache.delete(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 { PermissionsBitField } from "discord.js";
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
|
|
||||||
import { canUseEmoji, customEmojiRegex, isEmoji } from "../../../utils.js";
|
import { canUseEmoji, customEmojiRegex, isEmoji } from "../../../utils.js";
|
||||||
import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions.js";
|
import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions.js";
|
||||||
import { missingPermissionError } from "../../../utils/missingPermissionError.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 me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!;
|
||||||
const missingPermissions = getMissingChannelPermissions(me, args.channel, requiredPermissions);
|
const missingPermissions = getMissingChannelPermissions(me, args.channel, requiredPermissions);
|
||||||
if (missingPermissions) {
|
if (missingPermissions) {
|
||||||
sendErrorMessage(
|
pluginData.state.common.sendErrorMessage(
|
||||||
pluginData,
|
msg,
|
||||||
msg.channel,
|
|
||||||
`Cannot set auto-reactions for that channel. ${missingPermissionError(missingPermissions)}`,
|
`Cannot set auto-reactions for that channel. ${missingPermissionError(missingPermissions)}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -35,7 +33,7 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
|
||||||
|
|
||||||
for (const reaction of args.reactions) {
|
for (const reaction of args.reactions) {
|
||||||
if (!isEmoji(reaction)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +43,10 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
|
||||||
if (customEmojiMatch) {
|
if (customEmojiMatch) {
|
||||||
// Custom emoji
|
// Custom emoji
|
||||||
if (!canUseEmoji(pluginData.client, customEmojiMatch[2])) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +61,6 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
|
||||||
|
|
||||||
await pluginData.state.autoReactions.set(args.channel.id, finalReactions);
|
await pluginData.state.autoReactions.set(args.channel.id, finalReactions);
|
||||||
pluginData.state.cache.delete(args.channel.id);
|
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 { trimPluginDescription } from "../../utils.js";
|
||||||
import { zAutoReactionsConfig } from "./types.js";
|
import { zAutoReactionsConfig } from "./types.js";
|
||||||
|
|
||||||
export const autoReactionsInfo: ZeppelinPluginInfo = {
|
export const autoReactionsPluginDocs: ZeppelinPluginDocs = {
|
||||||
showInDocs: true,
|
type: "stable",
|
||||||
|
configSchema: zAutoReactionsConfig,
|
||||||
|
|
||||||
prettyName: "Auto-reactions",
|
prettyName: "Auto-reactions",
|
||||||
description: trimPluginDescription(`
|
description: trimPluginDescription(`
|
||||||
Allows setting up automatic reactions to all new messages on a channel
|
Allows setting up automatic reactions to all new messages on a channel
|
||||||
`),
|
`),
|
||||||
configSchema: zAutoReactionsConfig,
|
|
||||||
};
|
};
|
|
@ -1,21 +1,23 @@
|
||||||
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub";
|
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { GuildAutoReactions } from "../../data/GuildAutoReactions.js";
|
import { GuildAutoReactions } from "../../data/GuildAutoReactions.js";
|
||||||
import { GuildLogs } from "../../data/GuildLogs.js";
|
import { GuildLogs } from "../../data/GuildLogs.js";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
||||||
import { AutoReaction } from "../../data/entities/AutoReaction.js";
|
import { AutoReaction } from "../../data/entities/AutoReaction.js";
|
||||||
|
import { CommonPlugin } from "../Common/CommonPlugin.js";
|
||||||
|
|
||||||
export const zAutoReactionsConfig = z.strictObject({
|
export const zAutoReactionsConfig = z.strictObject({
|
||||||
can_manage: z.boolean(),
|
can_manage: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface AutoReactionsPluginType extends BasePluginType {
|
export interface AutoReactionsPluginType extends BasePluginType {
|
||||||
config: z.output<typeof zAutoReactionsConfig>;
|
configSchema: typeof zAutoReactionsConfig;
|
||||||
state: {
|
state: {
|
||||||
logs: GuildLogs;
|
logs: GuildLogs;
|
||||||
savedMessages: GuildSavedMessages;
|
savedMessages: GuildSavedMessages;
|
||||||
autoReactions: GuildAutoReactions;
|
autoReactions: GuildAutoReactions;
|
||||||
cache: Map<string, AutoReaction | null>;
|
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 { MINUTES, SECONDS } from "../../utils.js";
|
||||||
import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap.js";
|
import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap.js";
|
||||||
import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap.js";
|
import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap.js";
|
||||||
|
import { CommonPlugin } from "../Common/CommonPlugin.js";
|
||||||
import { CountersPlugin } from "../Counters/CountersPlugin.js";
|
import { CountersPlugin } from "../Counters/CountersPlugin.js";
|
||||||
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin.js";
|
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin.js";
|
||||||
import { LogsPlugin } from "../Logs/LogsPlugin.js";
|
import { LogsPlugin } from "../Logs/LogsPlugin.js";
|
||||||
|
@ -33,29 +34,6 @@ import { clearOldRecentActions } from "./functions/clearOldRecentActions.js";
|
||||||
import { clearOldRecentSpam } from "./functions/clearOldRecentSpam.js";
|
import { clearOldRecentSpam } from "./functions/clearOldRecentSpam.js";
|
||||||
import { AutomodPluginType, zAutomodConfig } from "./types.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>()({
|
export const AutomodPlugin = guildPlugin<AutomodPluginType>()({
|
||||||
name: "automod",
|
name: "automod",
|
||||||
|
|
||||||
|
@ -70,8 +48,7 @@ export const AutomodPlugin = guildPlugin<AutomodPluginType>()({
|
||||||
RoleManagerPlugin,
|
RoleManagerPlugin,
|
||||||
],
|
],
|
||||||
|
|
||||||
defaultOptions,
|
configSchema: zAutomodConfig,
|
||||||
configParser: (input) => zAutomodConfig.parse(input),
|
|
||||||
|
|
||||||
customOverrideCriteriaFunctions: {
|
customOverrideCriteriaFunctions: {
|
||||||
antiraid_level: (pluginData, matchParams, value) => {
|
antiraid_level: (pluginData, matchParams, value) => {
|
||||||
|
@ -117,6 +94,10 @@ export const AutomodPlugin = guildPlugin<AutomodPluginType>()({
|
||||||
state.cachedAntiraidLevel = await state.antiraidLevels.get();
|
state.cachedAntiraidLevel = await state.antiraidLevels.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
beforeStart(pluginData) {
|
||||||
|
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
|
||||||
|
},
|
||||||
|
|
||||||
async afterLoad(pluginData) {
|
async afterLoad(pluginData) {
|
||||||
const { state } = pluginData;
|
const { state } = pluginData;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PermissionFlagsBits, Snowflake } from "discord.js";
|
import { PermissionFlagsBits, Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { nonNullish, unique, zSnowflake } from "../../../utils.js";
|
import { nonNullish, unique, zSnowflake } from "../../../utils.js";
|
||||||
import { canAssignRole } from "../../../utils/canAssignRole.js";
|
import { canAssignRole } from "../../../utils/canAssignRole.js";
|
||||||
import { getMissingPermissions } from "../../../utils/getMissingPermissions.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 { zBoundedCharacters } from "../../../utils.js";
|
||||||
import { CountersPlugin } from "../../Counters/CountersPlugin.js";
|
import { CountersPlugin } from "../../Counters/CountersPlugin.js";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Snowflake } from "discord.js";
|
import { Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { LogType } from "../../../data/LogType.js";
|
import { LogType } from "../../../data/LogType.js";
|
||||||
import {
|
import {
|
||||||
createTypedTemplateSafeValueContainer,
|
createTypedTemplateSafeValueContainer,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { AnyThreadChannel } from "discord.js";
|
import { AnyThreadChannel } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { noop } from "../../../utils.js";
|
import { noop } from "../../../utils.js";
|
||||||
import { automodAction } from "../helpers.js";
|
import { automodAction } from "../helpers.js";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import {
|
import {
|
||||||
convertDelayStringToMS,
|
convertDelayStringToMS,
|
||||||
nonNullish,
|
nonNullish,
|
||||||
|
@ -47,6 +47,7 @@ export const BanAction = automodAction({
|
||||||
await modActions.banUserId(
|
await modActions.banUserId(
|
||||||
userId,
|
userId,
|
||||||
reason,
|
reason,
|
||||||
|
reason,
|
||||||
{
|
{
|
||||||
contactMethods,
|
contactMethods,
|
||||||
caseArgs,
|
caseArgs,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { nonNullish, unique, zBoundedCharacters } from "../../../utils.js";
|
import { nonNullish, unique, zBoundedCharacters } from "../../../utils.js";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||||
import { automodAction } from "../helpers.js";
|
import { automodAction } from "../helpers.js";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { PermissionsBitField, PermissionsString } from "discord.js";
|
import { PermissionsBitField, PermissionsString } from "discord.js";
|
||||||
import { U } from "ts-toolbelt";
|
import { U } from "ts-toolbelt";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter.js";
|
import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter.js";
|
||||||
import { isValidSnowflake, keys, noop, zBoundedCharacters } from "../../../utils.js";
|
import { isValidSnowflake, keys, noop, zBoundedCharacters } from "../../../utils.js";
|
||||||
import {
|
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 legacyPermissionNames = keys(legacyPermMap) as U.ListOf<keyof typeof legacyPermMap>;
|
||||||
const allPermissionNames = [...permissionNames, ...legacyPermissionNames] as const;
|
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({
|
export const ChangePermsAction = automodAction({
|
||||||
configSchema: z.strictObject({
|
configSchema: z.strictObject({
|
||||||
target: zBoundedCharacters(1, 2000),
|
target: zBoundedCharacters(1, 2000),
|
||||||
channel: zBoundedCharacters(1, 2000).nullable().default(null),
|
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 }) {
|
async apply({ pluginData, contexts, actionConfig, ruleName }) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { GuildTextBasedChannel, Snowflake } from "discord.js";
|
import { GuildTextBasedChannel, Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { LogType } from "../../../data/LogType.js";
|
import { LogType } from "../../../data/LogType.js";
|
||||||
import { noop } from "../../../utils.js";
|
import { noop } from "../../../utils.js";
|
||||||
import { automodAction } from "../helpers.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 { zBoundedCharacters } from "../../../utils.js";
|
||||||
import { automodAction } from "../helpers.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 { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils.js";
|
||||||
import { CaseArgs } from "../../Cases/types.js";
|
import { CaseArgs } from "../../Cases/types.js";
|
||||||
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
|
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
|
||||||
|
@ -33,7 +33,7 @@ export const KickAction = automodAction({
|
||||||
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
||||||
for (const member of membersToKick) {
|
for (const member of membersToKick) {
|
||||||
if (!member) continue;
|
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 { isTruthy, unique } from "../../../utils.js";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||||
import { automodAction } from "../helpers.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 { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError.js";
|
||||||
import {
|
import {
|
||||||
convertDelayStringToMS,
|
convertDelayStringToMS,
|
||||||
|
@ -57,6 +57,7 @@ export const MuteAction = automodAction({
|
||||||
userId,
|
userId,
|
||||||
duration,
|
duration,
|
||||||
reason,
|
reason,
|
||||||
|
reason,
|
||||||
{ contactMethods, caseArgs, isAutomodAction: true },
|
{ contactMethods, caseArgs, isAutomodAction: true },
|
||||||
rolesToRemove,
|
rolesToRemove,
|
||||||
rolesToRestore,
|
rolesToRestore,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { GuildFeature } from "discord.js";
|
import { GuildFeature } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { automodAction } from "../helpers.js";
|
import { automodAction } from "../helpers.js";
|
||||||
|
|
||||||
export const PauseInvitesAction = automodAction({
|
export const PauseInvitesAction = automodAction({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PermissionFlagsBits, Snowflake } from "discord.js";
|
import { PermissionFlagsBits, Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { nonNullish, unique, zSnowflake } from "../../../utils.js";
|
import { nonNullish, unique, zSnowflake } from "../../../utils.js";
|
||||||
import { canAssignRole } from "../../../utils/canAssignRole.js";
|
import { canAssignRole } from "../../../utils/canAssignRole.js";
|
||||||
import { getMissingPermissions } from "../../../utils/getMissingPermissions.js";
|
import { getMissingPermissions } from "../../../utils/getMissingPermissions.js";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { GuildTextBasedChannel, MessageCreateOptions, PermissionsBitField, Snowflake, User } from "discord.js";
|
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 { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter.js";
|
||||||
import {
|
import {
|
||||||
convertDelayStringToMS,
|
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 { MAX_COUNTER_VALUE, MIN_COUNTER_VALUE } from "../../../data/GuildCounters.js";
|
||||||
import { zBoundedCharacters } from "../../../utils.js";
|
import { zBoundedCharacters } from "../../../utils.js";
|
||||||
import { CountersPlugin } from "../../Counters/CountersPlugin.js";
|
import { CountersPlugin } from "../../Counters/CountersPlugin.js";
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { ChannelType, GuildTextBasedChannel, Snowflake } from "discord.js";
|
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 { convertDelayStringToMS, isDiscordAPIError, zDelayString, zSnowflake } from "../../../utils.js";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||||
import { automodAction } from "../helpers.js";
|
import { automodAction } from "../helpers.js";
|
||||||
|
|
||||||
export const SetSlowmodeAction = automodAction({
|
export const SetSlowmodeAction = automodAction({
|
||||||
configSchema: z.strictObject({
|
configSchema: z.strictObject({
|
||||||
channels: z.array(zSnowflake),
|
channels: z.array(zSnowflake).nullable().default([]),
|
||||||
duration: zDelayString.nullable().default("10s"),
|
duration: zDelayString.nullable().default("10s"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChannelType, GuildTextThreadCreateOptions, ThreadAutoArchiveDuration, ThreadChannel } from "discord.js";
|
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 { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter.js";
|
||||||
import { MINUTES, convertDelayStringToMS, noop, zBoundedCharacters, zDelayString } from "../../../utils.js";
|
import { MINUTES, convertDelayStringToMS, noop, zBoundedCharacters, zDelayString } from "../../../utils.js";
|
||||||
import { savedMessageToTemplateSafeSavedMessage, userToTemplateSafeUser } from "../../../utils/templateSafeObjects.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 { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils.js";
|
||||||
import { CaseArgs } from "../../Cases/types.js";
|
import { CaseArgs } from "../../Cases/types.js";
|
||||||
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
|
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
|
||||||
|
@ -33,7 +33,7 @@ export const WarnAction = automodAction({
|
||||||
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
||||||
for (const member of membersToWarn) {
|
for (const member of membersToWarn) {
|
||||||
if (!member) continue;
|
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 { guildPluginMessageCommand } from "knub";
|
||||||
import { sendSuccessMessage } from "../../../pluginUtils.js";
|
|
||||||
import { setAntiraidLevel } from "../functions/setAntiraidLevel.js";
|
import { setAntiraidLevel } from "../functions/setAntiraidLevel.js";
|
||||||
import { AutomodPluginType } from "../types.js";
|
import { AutomodPluginType } from "../types.js";
|
||||||
|
|
||||||
|
@ -9,6 +8,6 @@ export const AntiraidClearCmd = guildPluginMessageCommand<AutomodPluginType>()({
|
||||||
|
|
||||||
async run({ pluginData, message }) {
|
async run({ pluginData, message }) {
|
||||||
await setAntiraidLevel(pluginData, null, message.author);
|
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 { guildPluginMessageCommand } from "knub";
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
|
|
||||||
import { setAntiraidLevel } from "../functions/setAntiraidLevel.js";
|
import { setAntiraidLevel } from "../functions/setAntiraidLevel.js";
|
||||||
import { AutomodPluginType } from "../types.js";
|
import { AutomodPluginType } from "../types.js";
|
||||||
|
|
||||||
|
@ -15,11 +14,11 @@ export const SetAntiraidCmd = guildPluginMessageCommand<AutomodPluginType>()({
|
||||||
async run({ pluginData, message, args }) {
|
async run({ pluginData, message, args }) {
|
||||||
const config = pluginData.config.get();
|
const config = pluginData.config.get();
|
||||||
if (!config.antiraid_levels.includes(args.level)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await setAntiraidLevel(pluginData, args.level, message.author);
|
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";
|
import { MINUTES, SECONDS } from "../../utils.js";
|
||||||
|
|
||||||
export const RECENT_SPAM_EXPIRY_TIME = 10 * SECONDS;
|
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 { trimPluginDescription } from "../../utils.js";
|
||||||
import { zAutomodConfig } from "./types.js";
|
import { zAutomodConfig } from "./types.js";
|
||||||
|
|
||||||
export const automodPluginInfo: ZeppelinPluginInfo = {
|
export const automodPluginDocs: ZeppelinPluginDocs = {
|
||||||
showInDocs: true,
|
type: "stable",
|
||||||
prettyName: "Automod",
|
|
||||||
configSchema: zAutomodConfig,
|
configSchema: zAutomodConfig,
|
||||||
|
|
||||||
|
prettyName: "Automod",
|
||||||
description: trimPluginDescription(`
|
description: trimPluginDescription(`
|
||||||
Allows specifying automated actions in response to triggers. Example use cases include word filtering and spam prevention.
|
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 { guildPluginEventListener } from "knub";
|
||||||
import diff from "lodash/difference.js";
|
import { difference, isEqual } from "lodash-es";
|
||||||
import isEqual from "lodash/isEqual.js";
|
|
||||||
import { runAutomod } from "../functions/runAutomod.js";
|
import { runAutomod } from "../functions/runAutomod.js";
|
||||||
import { AutomodContext, AutomodPluginType } from "../types.js";
|
import { AutomodContext, AutomodPluginType } from "../types.js";
|
||||||
|
|
||||||
|
@ -15,8 +14,8 @@ export const RunAutomodOnMemberUpdate = guildPluginEventListener<AutomodPluginTy
|
||||||
|
|
||||||
if (isEqual(oldRoles, newRoles)) return;
|
if (isEqual(oldRoles, newRoles)) return;
|
||||||
|
|
||||||
const addedRoles = diff(newRoles, oldRoles);
|
const addedRoles = difference(newRoles, oldRoles);
|
||||||
const removedRoles = diff(oldRoles, newRoles);
|
const removedRoles = difference(oldRoles, newRoles);
|
||||||
|
|
||||||
if (addedRoles.length || removedRoles.length) {
|
if (addedRoles.length || removedRoles.length) {
|
||||||
const context: AutomodContext = {
|
const context: AutomodContext = {
|
||||||
|
|
|
@ -2,8 +2,13 @@ import { GuildPluginData } from "knub";
|
||||||
import { convertDelayStringToMS } from "../../../utils.js";
|
import { convertDelayStringToMS } from "../../../utils.js";
|
||||||
import { AutomodContext, AutomodPluginType, TRule } from "../types.js";
|
import { AutomodContext, AutomodPluginType, TRule } from "../types.js";
|
||||||
|
|
||||||
export function applyCooldown(pluginData: GuildPluginData<AutomodPluginType>, rule: TRule, context: AutomodContext) {
|
export function applyCooldown(
|
||||||
const cooldownKey = `${rule.name}-${context.user?.id}`;
|
pluginData: GuildPluginData<AutomodPluginType>,
|
||||||
|
rule: TRule,
|
||||||
|
ruleName: string,
|
||||||
|
context: AutomodContext,
|
||||||
|
) {
|
||||||
|
const cooldownKey = `${ruleName}-${context.user?.id}`;
|
||||||
|
|
||||||
const cooldownTime = convertDelayStringToMS(rule.cooldown, "s");
|
const cooldownTime = convertDelayStringToMS(rule.cooldown, "s");
|
||||||
if (cooldownTime) pluginData.state.cooldownManager.setCooldown(cooldownKey, cooldownTime);
|
if (cooldownTime) pluginData.state.cooldownManager.setCooldown(cooldownKey, cooldownTime);
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
import { AutomodContext, AutomodPluginType, TRule } from "../types.js";
|
import { AutomodContext, AutomodPluginType, TRule } from "../types.js";
|
||||||
|
|
||||||
export function checkCooldown(pluginData: GuildPluginData<AutomodPluginType>, rule: TRule, context: AutomodContext) {
|
export function checkCooldown(
|
||||||
const cooldownKey = `${rule.name}-${context.user?.id}`;
|
pluginData: GuildPluginData<AutomodPluginType>,
|
||||||
|
rule: TRule,
|
||||||
|
ruleName: string,
|
||||||
|
context: AutomodContext,
|
||||||
|
) {
|
||||||
|
const cooldownKey = `${ruleName}-${context.user?.id}`;
|
||||||
|
|
||||||
return pluginData.state.cooldownManager.isOnCooldown(cooldownKey);
|
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 { SavedMessage } from "../../../data/entities/SavedMessage.js";
|
||||||
import { humanizeDurationShort } from "../../../humanizeDurationShort.js";
|
import { humanizeDurationShort } from "../../../humanizeDuration.js";
|
||||||
import { getBaseUrl } from "../../../pluginUtils.js";
|
import { getBaseUrl } from "../../../pluginUtils.js";
|
||||||
import { convertDelayStringToMS, sorter, zDelayString } from "../../../utils.js";
|
import { convertDelayStringToMS, sorter, zDelayString } from "../../../utils.js";
|
||||||
import { RecentActionType } from "../constants.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.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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchResult) {
|
if (matchResult) {
|
||||||
if (rule.cooldown) applyCooldown(pluginData, rule, context);
|
if (rule.cooldown) applyCooldown(pluginData, rule, ruleName, context);
|
||||||
|
|
||||||
contexts = [context, ...(matchResult.extraContexts || [])];
|
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()) {
|
if (profilingEnabled()) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
import z, { ZodTypeAny } from "zod";
|
import z, { ZodTypeAny } from "zod/v4";
|
||||||
import { Awaitable } from "../../utils/typeUtils.js";
|
import { Awaitable } from "../../utils/typeUtils.js";
|
||||||
import { AutomodContext, AutomodPluginType } from "./types.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";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
interface AntiraidLevelTriggerResult {}
|
interface AntiraidLevelTriggerResult {}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Snowflake } from "discord.js";
|
import { Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { verboseChannelMention } from "../../../utils.js";
|
import { verboseChannelMention } from "../../../utils.js";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
// tslint:disable-next-line:no-empty-interface
|
// 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";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
interface ExampleMatchResultType {
|
interface ExampleMatchResultType {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
// tslint:disable-next-line:no-empty-interface
|
// tslint:disable-next-line:no-empty-interface
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { escapeInlineCode, Snowflake } from "discord.js";
|
import { escapeInlineCode, Snowflake } from "discord.js";
|
||||||
import { extname } from "path";
|
import { extname } from "path";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils.js";
|
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils.js";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
|
@ -9,30 +9,12 @@ interface MatchResultType {
|
||||||
mode: "blacklist" | "whitelist";
|
mode: "blacklist" | "whitelist";
|
||||||
}
|
}
|
||||||
|
|
||||||
const configSchema = z
|
const configSchema = z.strictObject({
|
||||||
.strictObject({
|
whitelist_enabled: z.boolean().default(false),
|
||||||
filetype_blacklist: z.array(z.string().max(32)).max(255).default([]),
|
filetype_whitelist: z.array(z.string().max(32)).max(255).default([]),
|
||||||
blacklist_enabled: z.boolean().default(false),
|
blacklist_enabled: z.boolean().default(false),
|
||||||
filetype_whitelist: z.array(z.string().max(32)).max(255).default([]),
|
filetype_blacklist: z.array(z.string().max(32)).max(255).default([]),
|
||||||
whitelist_enabled: z.boolean().default(false),
|
});
|
||||||
})
|
|
||||||
.transform((parsed, ctx) => {
|
|
||||||
if (parsed.blacklist_enabled && parsed.whitelist_enabled) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Cannot have both blacklist and whitelist enabled",
|
|
||||||
});
|
|
||||||
return z.NEVER;
|
|
||||||
}
|
|
||||||
if (!parsed.blacklist_enabled && !parsed.whitelist_enabled) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Must have either blacklist or whitelist enabled",
|
|
||||||
});
|
|
||||||
return z.NEVER;
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({
|
export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({
|
||||||
configSchema,
|
configSchema,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { getInviteCodesInString, GuildInvite, isGuildInvite, resolveInvite, zSnowflake } from "../../../utils.js";
|
import { getInviteCodesInString, GuildInvite, isGuildInvite, resolveInvite, zSnowflake } from "../../../utils.js";
|
||||||
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary.js";
|
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary.js";
|
||||||
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage.js";
|
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage.js";
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { escapeInlineCode } from "discord.js";
|
import { escapeInlineCode } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { allowTimeout } from "../../../RegExpRunner.js";
|
import { allowTimeout } from "../../../RegExpRunner.js";
|
||||||
import { phishermanDomainIsSafe } from "../../../data/Phisherman.js";
|
import { getFishFishDomain } from "../../../data/FishFish.js";
|
||||||
import { getUrlsInString, zRegex } from "../../../utils.js";
|
import { getUrlsInString, inputPatternToRegExp, zRegex } from "../../../utils.js";
|
||||||
import { mergeRegexes } from "../../../utils/mergeRegexes.js";
|
import { mergeRegexes } from "../../../utils/mergeRegexes.js";
|
||||||
import { mergeWordsIntoRegex } from "../../../utils/mergeWordsIntoRegex.js";
|
import { mergeWordsIntoRegex } from "../../../utils/mergeWordsIntoRegex.js";
|
||||||
import { PhishermanPlugin } from "../../Phisherman/PhishermanPlugin.js";
|
|
||||||
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary.js";
|
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary.js";
|
||||||
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage.js";
|
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage.js";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
@ -40,6 +39,7 @@ const configSchema = z.strictObject({
|
||||||
include_verified: z.boolean().optional(),
|
include_verified: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
include_malicious: z.boolean().default(false),
|
||||||
only_real_links: z.boolean().default(true),
|
only_real_links: z.boolean().default(true),
|
||||||
match_messages: z.boolean().default(true),
|
match_messages: z.boolean().default(true),
|
||||||
match_embeds: z.boolean().default(true),
|
match_embeds: z.boolean().default(true),
|
||||||
|
@ -73,7 +73,7 @@ export const MatchLinksTrigger = automodTrigger<MatchResultType>()({
|
||||||
|
|
||||||
if (trigger.exclude_regex) {
|
if (trigger.exclude_regex) {
|
||||||
if (!regexCache.has(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);
|
regexCache.set(trigger.exclude_regex, toCache);
|
||||||
}
|
}
|
||||||
const regexes = regexCache.get(trigger.exclude_regex)!;
|
const regexes = regexCache.get(trigger.exclude_regex)!;
|
||||||
|
@ -88,7 +88,7 @@ export const MatchLinksTrigger = automodTrigger<MatchResultType>()({
|
||||||
|
|
||||||
if (trigger.include_regex) {
|
if (trigger.include_regex) {
|
||||||
if (!regexCache.has(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);
|
regexCache.set(trigger.include_regex, toCache);
|
||||||
}
|
}
|
||||||
const regexes = regexCache.get(trigger.include_regex)!;
|
const regexes = regexCache.get(trigger.include_regex)!;
|
||||||
|
@ -155,22 +155,18 @@ export const MatchLinksTrigger = automodTrigger<MatchResultType>()({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trigger.phisherman) {
|
const includeMalicious =
|
||||||
const phishermanResult = await pluginData.getPlugin(PhishermanPlugin).getDomainInfo(normalizedHostname);
|
trigger.include_malicious || trigger.phisherman?.include_suspected || trigger.phisherman?.include_verified;
|
||||||
if (phishermanResult != null && !phishermanDomainIsSafe(phishermanResult)) {
|
if (includeMalicious) {
|
||||||
if (
|
const domainInfo = getFishFishDomain(normalizedHostname);
|
||||||
(trigger.phisherman.include_suspected && !phishermanResult.verifiedPhish) ||
|
if (domainInfo && domainInfo.category !== "safe") {
|
||||||
(trigger.phisherman.include_verified && phishermanResult.verifiedPhish)
|
return {
|
||||||
) {
|
extra: {
|
||||||
const suspectedVerified = phishermanResult.verifiedPhish ? "verified" : "suspected";
|
type,
|
||||||
return {
|
link: link.input,
|
||||||
extra: {
|
details: `(known ${domainInfo.category} domain)`,
|
||||||
type,
|
},
|
||||||
link: link.input,
|
};
|
||||||
details: `using Phisherman (${suspectedVerified})`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { escapeInlineCode } from "discord.js";
|
import { escapeInlineCode } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils.js";
|
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils.js";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
|
@ -8,30 +8,12 @@ interface MatchResultType {
|
||||||
mode: "blacklist" | "whitelist";
|
mode: "blacklist" | "whitelist";
|
||||||
}
|
}
|
||||||
|
|
||||||
const configSchema = z
|
const configSchema = z.strictObject({
|
||||||
.strictObject({
|
whitelist_enabled: z.boolean().default(false),
|
||||||
mime_type_blacklist: z.array(z.string().max(255)).max(255).default([]),
|
mime_type_whitelist: z.array(z.string().max(32)).max(255).default([]),
|
||||||
blacklist_enabled: z.boolean().default(false),
|
blacklist_enabled: z.boolean().default(false),
|
||||||
mime_type_whitelist: z.array(z.string().max(255)).max(255).default([]),
|
mime_type_blacklist: z.array(z.string().max(32)).max(255).default([]),
|
||||||
whitelist_enabled: z.boolean().default(false),
|
});
|
||||||
})
|
|
||||||
.transform((parsed, ctx) => {
|
|
||||||
if (parsed.blacklist_enabled && parsed.whitelist_enabled) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Cannot have both blacklist and whitelist enabled",
|
|
||||||
});
|
|
||||||
return z.NEVER;
|
|
||||||
}
|
|
||||||
if (!parsed.blacklist_enabled && !parsed.whitelist_enabled) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Must have either blacklist or whitelist enabled",
|
|
||||||
});
|
|
||||||
return z.NEVER;
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const MatchMimeTypeTrigger = automodTrigger<MatchResultType>()({
|
export const MatchMimeTypeTrigger = automodTrigger<MatchResultType>()({
|
||||||
configSchema,
|
configSchema,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { allowTimeout } from "../../../RegExpRunner.js";
|
import { allowTimeout } from "../../../RegExpRunner.js";
|
||||||
import { zRegex } from "../../../utils.js";
|
import { inputPatternToRegExp, zRegex } from "../../../utils.js";
|
||||||
import { mergeRegexes } from "../../../utils/mergeRegexes.js";
|
import { mergeRegexes } from "../../../utils/mergeRegexes.js";
|
||||||
import { normalizeText } from "../../../utils/normalizeText.js";
|
import { normalizeText } from "../../../utils/normalizeText.js";
|
||||||
import { stripMarkdown } from "../../../utils/stripMarkdown.js";
|
import { stripMarkdown } from "../../../utils/stripMarkdown.js";
|
||||||
|
@ -38,7 +38,7 @@ export const MatchRegexTrigger = automodTrigger<MatchResultType>()({
|
||||||
|
|
||||||
if (!regexCache.has(trigger)) {
|
if (!regexCache.has(trigger)) {
|
||||||
const flags = trigger.case_sensitive ? "" : "i";
|
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);
|
regexCache.set(trigger, toCache);
|
||||||
}
|
}
|
||||||
const regexes = regexCache.get(trigger)!;
|
const regexes = regexCache.get(trigger)!;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import escapeStringRegexp from "escape-string-regexp";
|
import escapeStringRegexp from "escape-string-regexp";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { normalizeText } from "../../../utils/normalizeText.js";
|
import { normalizeText } from "../../../utils/normalizeText.js";
|
||||||
import { stripMarkdown } from "../../../utils/stripMarkdown.js";
|
import { stripMarkdown } from "../../../utils/stripMarkdown.js";
|
||||||
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary.js";
|
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary.js";
|
||||||
|
@ -19,7 +19,7 @@ const configSchema = z.strictObject({
|
||||||
only_full_words: z.boolean().default(true),
|
only_full_words: z.boolean().default(true),
|
||||||
normalize: z.boolean().default(false),
|
normalize: z.boolean().default(false),
|
||||||
loose_matching: 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),
|
strip_markdown: z.boolean().default(false),
|
||||||
match_messages: z.boolean().default(true),
|
match_messages: z.boolean().default(true),
|
||||||
match_embeds: z.boolean().default(false),
|
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 { convertDelayStringToMS, zDelayString } from "../../../utils.js";
|
||||||
import { automodTrigger } from "../helpers.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 { convertDelayStringToMS, zDelayString } from "../../../utils.js";
|
||||||
import { RecentActionType } from "../constants.js";
|
import { RecentActionType } from "../constants.js";
|
||||||
import { findRecentSpam } from "../functions/findRecentSpam.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";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
const configSchema = z.strictObject({});
|
const configSchema = z.strictObject({});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
// tslint:disable-next-line:no-empty-interface
|
// 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";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
// tslint:disable-next-line:no-empty-interface
|
// tslint:disable-next-line:no-empty-interface
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Snowflake } from "discord.js";
|
import { Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { renderUsername, zSnowflake } from "../../../utils.js";
|
import { renderUsername, zSnowflake } from "../../../utils.js";
|
||||||
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges.js";
|
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges.js";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Snowflake } from "discord.js";
|
import { Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { renderUsername, zSnowflake } from "../../../utils.js";
|
import { renderUsername, zSnowflake } from "../../../utils.js";
|
||||||
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges.js";
|
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges.js";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { User, escapeBold, type Snowflake } from "discord.js";
|
import { User, escapeBold, type Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { renderUsername } from "../../../utils.js";
|
import { renderUsername } from "../../../utils.js";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { User, escapeBold, type Snowflake } from "discord.js";
|
import { User, escapeBold, type Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { renderUsername } from "../../../utils.js";
|
import { renderUsername } from "../../../utils.js";
|
||||||
import { automodTrigger } from "../helpers.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 { convertDelayStringToMS, zDelayString } from "../../../utils.js";
|
||||||
import { RecentActionType } from "../constants.js";
|
import { RecentActionType } from "../constants.js";
|
||||||
import { findRecentSpam } from "../functions/findRecentSpam.js";
|
import { findRecentSpam } from "../functions/findRecentSpam.js";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { User, escapeBold, type Snowflake } from "discord.js";
|
import { User, escapeBold, type Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { renderUsername } from "../../../utils.js";
|
import { renderUsername } from "../../../utils.js";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { User, escapeBold, type Snowflake } from "discord.js";
|
import { User, escapeBold, type Snowflake } from "discord.js";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { renderUsername } from "../../../utils.js";
|
import { renderUsername } from "../../../utils.js";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { automodTrigger } from "../helpers.js";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
// tslint:disable-next-line:no-empty-interface
|
// 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";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
// tslint:disable-next-line:no-empty-interface
|
// 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";
|
import { automodTrigger } from "../helpers.js";
|
||||||
|
|
||||||
// tslint:disable-next-line:no-empty-interface
|
// tslint:disable-next-line:no-empty-interface
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { GuildMember, GuildTextBasedChannel, PartialGuildMember, ThreadChannel, User } from "discord.js";
|
import { GuildMember, GuildTextBasedChannel, PartialGuildMember, ThreadChannel, User } from "discord.js";
|
||||||
import { BasePluginType, CooldownManager } from "knub";
|
import { BasePluginType, CooldownManager, pluginUtils } from "knub";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
import { Queue } from "../../Queue.js";
|
import { Queue } from "../../Queue.js";
|
||||||
import { RegExpRunner } from "../../RegExpRunner.js";
|
import { RegExpRunner } from "../../RegExpRunner.js";
|
||||||
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels.js";
|
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels.js";
|
||||||
|
@ -9,6 +9,7 @@ import { GuildLogs } from "../../data/GuildLogs.js";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
|
||||||
import { SavedMessage } from "../../data/entities/SavedMessage.js";
|
import { SavedMessage } from "../../data/entities/SavedMessage.js";
|
||||||
import { entries, zBoundedRecord, zDelayString } from "../../utils.js";
|
import { entries, zBoundedRecord, zDelayString } from "../../utils.js";
|
||||||
|
import { CommonPlugin } from "../Common/CommonPlugin.js";
|
||||||
import { CounterEvents } from "../Counters/types.js";
|
import { CounterEvents } from "../Counters/types.js";
|
||||||
import { ModActionType, ModActionsEvents } from "../ModActions/types.js";
|
import { ModActionType, ModActionsEvents } from "../ModActions/types.js";
|
||||||
import { MutesEvents } from "../Mutes/types.js";
|
import { MutesEvents } from "../Mutes/types.js";
|
||||||
|
@ -45,22 +46,6 @@ const zActionsMap = z
|
||||||
|
|
||||||
const zRule = z.strictObject({
|
const zRule = z.strictObject({
|
||||||
enabled: z.boolean().default(true),
|
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(),
|
pretty_name: z.string().optional(),
|
||||||
presets: z.array(z.string().max(100)).max(25).default([]),
|
presets: z.array(z.string().max(100)).max(25).default([]),
|
||||||
affects_bots: z.boolean().default(false),
|
affects_bots: z.boolean().default(false),
|
||||||
|
@ -68,21 +53,19 @@ const zRule = z.strictObject({
|
||||||
cooldown: zDelayString.nullable().default(null),
|
cooldown: zDelayString.nullable().default(null),
|
||||||
allow_further_rules: z.boolean().default(false),
|
allow_further_rules: z.boolean().default(false),
|
||||||
triggers: z.array(zTriggersMap),
|
triggers: z.array(zTriggersMap),
|
||||||
actions: zActionsMap.refine((v) => !(v.clean && v.start_thread), {
|
actions: zActionsMap,
|
||||||
message: "Cannot have both clean and start_thread active at the same time",
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
export type TRule = z.infer<typeof zRule>;
|
export type TRule = z.infer<typeof zRule>;
|
||||||
|
|
||||||
export const zAutomodConfig = z.strictObject({
|
export const zAutomodConfig = z.strictObject({
|
||||||
rules: zBoundedRecord(z.record(z.string().max(100), zRule), 0, 255),
|
rules: zBoundedRecord(z.record(z.string().max(100), zRule), 0, 255).default({}),
|
||||||
antiraid_levels: z.array(z.string().max(100)).max(10),
|
antiraid_levels: z.array(z.string().max(100)).max(10).default(["low", "medium", "high"]),
|
||||||
can_set_antiraid: z.boolean(),
|
can_set_antiraid: z.boolean().default(false),
|
||||||
can_view_antiraid: z.boolean(),
|
can_view_antiraid: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface AutomodPluginType extends BasePluginType {
|
export interface AutomodPluginType extends BasePluginType {
|
||||||
config: z.output<typeof zAutomodConfig>;
|
configSchema: typeof zAutomodConfig;
|
||||||
|
|
||||||
customOverrideCriteria: {
|
customOverrideCriteria: {
|
||||||
antiraid_level?: string;
|
antiraid_level?: string;
|
||||||
|
@ -140,6 +123,8 @@ export interface AutomodPluginType extends BasePluginType {
|
||||||
|
|
||||||
modActionsListeners: Map<keyof ModActionsEvents, any>;
|
modActionsListeners: Map<keyof ModActionsEvents, any>;
|
||||||
mutesListeners: Map<keyof MutesEvents, 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 { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments.js";
|
||||||
import { Configs } from "../../data/Configs.js";
|
import { Configs } from "../../data/Configs.js";
|
||||||
import { GuildArchives } from "../../data/GuildArchives.js";
|
import { GuildArchives } from "../../data/GuildArchives.js";
|
||||||
import { sendSuccessMessage } from "../../pluginUtils.js";
|
|
||||||
import { getActiveReload, resetActiveReload } from "./activeReload.js";
|
import { getActiveReload, resetActiveReload } from "./activeReload.js";
|
||||||
import { AddDashboardUserCmd } from "./commands/AddDashboardUserCmd.js";
|
import { AddDashboardUserCmd } from "./commands/AddDashboardUserCmd.js";
|
||||||
import { AddServerFromInviteCmd } from "./commands/AddServerFromInviteCmd.js";
|
import { AddServerFromInviteCmd } from "./commands/AddServerFromInviteCmd.js";
|
||||||
|
@ -24,21 +23,9 @@ import { RestPerformanceCmd } from "./commands/RestPerformanceCmd.js";
|
||||||
import { ServersCmd } from "./commands/ServersCmd.js";
|
import { ServersCmd } from "./commands/ServersCmd.js";
|
||||||
import { BotControlPluginType, zBotControlConfig } from "./types.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>()({
|
export const BotControlPlugin = globalPlugin<BotControlPluginType>()({
|
||||||
name: "bot_control",
|
name: "bot_control",
|
||||||
configParser: (input) => zBotControlConfig.parse(input),
|
configSchema: zBotControlConfig,
|
||||||
defaultOptions,
|
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
messageCommands: [
|
messageCommands: [
|
||||||
|
@ -77,7 +64,7 @@ export const BotControlPlugin = globalPlugin<BotControlPluginType>()({
|
||||||
if (guild) {
|
if (guild) {
|
||||||
const channel = guild.channels.cache.get(channelId as Snowflake);
|
const channel = guild.channels.cache.get(channelId as Snowflake);
|
||||||
if (channel instanceof TextChannel) {
|
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 { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes.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 { renderUsername } from "../../../utils.js";
|
||||||
import { botControlCmd } from "../types.js";
|
import { botControlCmd } from "../types.js";
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export const AddDashboardUserCmd = botControlCmd({
|
||||||
async run({ pluginData, message: msg, args }) {
|
async run({ pluginData, message: msg, args }) {
|
||||||
const guild = await pluginData.state.allowedGuilds.find(args.guildId);
|
const guild = await pluginData.state.allowedGuilds.find(args.guildId);
|
||||||
if (!guild) {
|
if (!guild) {
|
||||||
sendErrorMessage(pluginData, msg.channel, "Server is not using Zeppelin");
|
void msg.channel.send("Server is not using Zeppelin");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,10 +36,7 @@ export const AddDashboardUserCmd = botControlCmd({
|
||||||
}
|
}
|
||||||
|
|
||||||
const userNameList = args.users.map((user) => `<@!${user.id}> (**${renderUsername(user)}**, \`${user.id}\`)`);
|
const userNameList = args.users.map((user) => `<@!${user.id}> (**${renderUsername(user)}**, \`${user.id}\`)`);
|
||||||
sendSuccessMessage(
|
|
||||||
pluginData,
|
msg.channel.send(`The following users were given dashboard access for **${guild.name}**:\n\n${userNameList}`);
|
||||||
msg.channel,
|
|
||||||
`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 { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
|
|
||||||
import { DBDateFormat, isGuildInvite, resolveInvite } from "../../../utils.js";
|
import { DBDateFormat, isGuildInvite, resolveInvite } from "../../../utils.js";
|
||||||
import { isEligible } from "../functions/isEligible.js";
|
import { isEligible } from "../functions/isEligible.js";
|
||||||
import { botControlCmd } from "../types.js";
|
import { botControlCmd } from "../types.js";
|
||||||
|
@ -18,19 +17,19 @@ export const AddServerFromInviteCmd = botControlCmd({
|
||||||
async run({ pluginData, message: msg, args }) {
|
async run({ pluginData, message: msg, args }) {
|
||||||
const invite = await resolveInvite(pluginData.client, args.inviteCode, true);
|
const invite = await resolveInvite(pluginData.client, args.inviteCode, true);
|
||||||
if (!invite || !isGuildInvite(invite)) {
|
if (!invite || !isGuildInvite(invite)) {
|
||||||
sendErrorMessage(pluginData, msg.channel, "Could not resolve invite"); // :D
|
void msg.channel.send("Could not resolve invite"); // :D
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await pluginData.state.allowedGuilds.find(invite.guild.id);
|
const existing = await pluginData.state.allowedGuilds.find(invite.guild.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
sendErrorMessage(pluginData, msg.channel, "Server is already allowed!");
|
void msg.channel.send("Server is already allowed!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result, explanation } = await isEligible(pluginData, args.user, invite);
|
const { result, explanation } = await isEligible(pluginData, args.user, invite);
|
||||||
if (!result) {
|
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;
|
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 { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
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 { DBDateFormat, isSnowflake } from "../../../utils.js";
|
||||||
import { botControlCmd } from "../types.js";
|
import { botControlCmd } from "../types.js";
|
||||||
|
|
||||||
|
@ -20,17 +20,17 @@ export const AllowServerCmd = botControlCmd({
|
||||||
async run({ pluginData, message: msg, args }) {
|
async run({ pluginData, message: msg, args }) {
|
||||||
const existing = await pluginData.state.allowedGuilds.find(args.guildId);
|
const existing = await pluginData.state.allowedGuilds.find(args.guildId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
sendErrorMessage(pluginData, msg.channel, "Server is already allowed!");
|
void msg.channel.send("Server is already allowed!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSnowflake(args.guildId)) {
|
if (!isSnowflake(args.guildId)) {
|
||||||
sendErrorMessage(pluginData, msg.channel, "Invalid server ID!");
|
void msg.channel.send("Invalid server ID!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.userId && !isSnowflake(args.userId)) {
|
if (args.userId && !isSnowflake(args.userId)) {
|
||||||
sendErrorMessage(pluginData, msg.channel, "Invalid user ID!");
|
void msg.channel.send("Invalid user ID!");
|
||||||
return;
|
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 { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||||
import { isStaffPreFilter, sendErrorMessage } from "../../../pluginUtils.js";
|
import { isStaffPreFilter } from "../../../pluginUtils.js";
|
||||||
import { botControlCmd } from "../types.js";
|
import { botControlCmd } from "../types.js";
|
||||||
|
|
||||||
export const ChannelToServerCmd = botControlCmd({
|
export const ChannelToServerCmd = botControlCmd({
|
||||||
|
@ -16,7 +16,7 @@ export const ChannelToServerCmd = botControlCmd({
|
||||||
async run({ pluginData, message: msg, args }) {
|
async run({ pluginData, message: msg, args }) {
|
||||||
const channel = pluginData.client.channels.cache.get(args.channelId);
|
const channel = pluginData.client.channels.cache.get(args.channelId);
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
sendErrorMessage(pluginData, msg.channel, "Channel not found in cache!");
|
void msg.channel.send("Channel not found in cache!");
|
||||||
return;
|
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