mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-07-10 12:37:19 +00:00
Merge branch 'ZeppelinBot:master' into chore(basic-config)-rename-plugin
This commit is contained in:
commit
9578668636
591 changed files with 25054 additions and 8203 deletions
3
.clabot
3
.clabot
|
@ -16,7 +16,8 @@
|
||||||
"roflmaoqwerty",
|
"roflmaoqwerty",
|
||||||
"thewilloftheshadow",
|
"thewilloftheshadow",
|
||||||
"usoka",
|
"usoka",
|
||||||
"vcokltfre"
|
"vcokltfre",
|
||||||
|
"Dragory"
|
||||||
],
|
],
|
||||||
"message": "Thank you for contributing to Zeppelin! We require contributors to sign our Contributor License Agreement (CLA). To let us review and merge your code, please visit https://github.com/ZeppelinBot/CLA to sign the CLA!"
|
"message": "Thank you for contributing to Zeppelin! We require contributors to sign our Contributor License Agreement (CLA). To let us review and merge your code, please visit https://github.com/ZeppelinBot/CLA to sign the CLA!"
|
||||||
}
|
}
|
||||||
|
|
28
.eslintrc.js
Normal file
28
.eslintrc.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
|
"@typescript-eslint/ban-ts-comment": 0,
|
||||||
|
"@typescript-eslint/no-non-null-assertion": 0,
|
||||||
|
"no-async-promise-executor": 0,
|
||||||
|
"@typescript-eslint/no-empty-interface": 0,
|
||||||
|
"no-constant-condition": ["error", {
|
||||||
|
checkLoops: false,
|
||||||
|
}],
|
||||||
|
"prefer-const": ["error", {
|
||||||
|
destructuring: "all",
|
||||||
|
ignoreReadBeforeAssign: true,
|
||||||
|
}],
|
||||||
|
"@typescript-eslint/no-namespace": ["error", {
|
||||||
|
allowDeclarations: true,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
2
.github/workflows/codequality.yml
vendored
2
.github/workflows/codequality.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [16.6]
|
node-version: [18.16]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -6,6 +6,9 @@
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
.clinic
|
||||||
|
.clinic-bot
|
||||||
|
.clinic-api
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
pids
|
pids
|
||||||
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
||||||
16
|
18
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const pkgUp = require("pkg-up");
|
|
||||||
const { backendDir } = require("./dist/backend/src/paths");
|
const { backendDir } = require("./dist/backend/src/paths");
|
||||||
const { env } = require("./dist/backend/src/env");
|
const { env } = require("./dist/backend/src/env");
|
||||||
|
|
||||||
|
|
16549
backend/package-lock.json
generated
16549
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -8,10 +8,14 @@
|
||||||
"watch-yaml-parse-test": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node dist/backend/src/yamlParseTest.js\"",
|
"watch-yaml-parse-test": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node dist/backend/src/yamlParseTest.js\"",
|
||||||
"build": "rimraf dist && tsc",
|
"build": "rimraf dist && tsc",
|
||||||
"start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js",
|
"start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js",
|
||||||
|
"start-bot-dev-debug": "NODE_ENV=development clinic heapprofiler --collect-only --dest .clinic-bot -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js",
|
||||||
"start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js",
|
"start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js",
|
||||||
|
"start-bot-prod-debug": "NODE_ENV=production clinic heapprofiler --collect-only --dest .clinic-bot -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js",
|
||||||
"watch-bot": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-bot-dev\"",
|
"watch-bot": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-bot-dev\"",
|
||||||
"start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
|
"start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
|
||||||
|
"start-api-dev-debug": "NODE_ENV=development clinic heapprofiler --collect-only --dest .clinic-api -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
|
||||||
"start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js",
|
"start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js",
|
||||||
|
"start-api-prod-debug": "NODE_ENV=production clinic heapprofiler --collect-only --dest .clinic-api -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js",
|
||||||
"watch-api": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-api-dev\"",
|
"watch-api": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-api-dev\"",
|
||||||
"typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js",
|
"typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js",
|
||||||
"migrate-prod": "cross-env NODE_ENV=production npm run typeorm -- migration:run",
|
"migrate-prod": "cross-env NODE_ENV=production npm run typeorm -- migration:run",
|
||||||
|
@ -25,11 +29,11 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@silvia-odwyer/photon-node": "^0.3.1",
|
"@silvia-odwyer/photon-node": "^0.3.1",
|
||||||
"bufferutil": "^4.0.3",
|
"bufferutil": "^4.0.3",
|
||||||
|
"clinic": "^12.1.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"deep-diff": "^1.0.2",
|
"deep-diff": "^1.0.2",
|
||||||
"discord-api-types": "^0.33.1",
|
"discord.js": "^14.9.0",
|
||||||
"discord.js": "13.8",
|
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
"erlpack": "github:discord/erlpack",
|
"erlpack": "github:discord/erlpack",
|
||||||
|
@ -39,7 +43,7 @@
|
||||||
"humanize-duration": "^3.15.0",
|
"humanize-duration": "^3.15.0",
|
||||||
"io-ts": "^2.0.0",
|
"io-ts": "^2.0.0",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"knub": "^30.0.0-beta.46",
|
"knub": "^32.0.0-next.15",
|
||||||
"knub-command-manager": "^9.1.0",
|
"knub-command-manager": "^9.1.0",
|
||||||
"last-commit-log": "^2.1.0",
|
"last-commit-log": "^2.1.0",
|
||||||
"lodash.chunk": "^4.2.0",
|
"lodash.chunk": "^4.2.0",
|
||||||
|
@ -51,7 +55,6 @@
|
||||||
"moment-timezone": "^0.5.21",
|
"moment-timezone": "^0.5.21",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
"mysql": "^2.16.0",
|
"mysql": "^2.16.0",
|
||||||
"node-fetch": "^2.6.7",
|
|
||||||
"parse-color": "^1.0.0",
|
"parse-color": "^1.0.0",
|
||||||
"passport": "^0.4.0",
|
"passport": "^0.4.0",
|
||||||
"passport-custom": "^1.0.5",
|
"passport-custom": "^1.0.5",
|
||||||
|
@ -82,8 +85,7 @@
|
||||||
"@types/lodash.at": "^4.6.3",
|
"@types/lodash.at": "^4.6.3",
|
||||||
"@types/moment-timezone": "^0.5.6",
|
"@types/moment-timezone": "^0.5.6",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^14.0.14",
|
"@types/node": "^18.16.3",
|
||||||
"@types/node-fetch": "^2.5.12",
|
|
||||||
"@types/passport": "^1.0.0",
|
"@types/passport": "^1.0.0",
|
||||||
"@types/passport-oauth2": "^1.4.8",
|
"@types/passport-oauth2": "^1.4.8",
|
||||||
"@types/passport-strategy": "^0.2.35",
|
"@types/passport-strategy": "^0.2.35",
|
||||||
|
@ -92,8 +94,7 @@
|
||||||
"@types/twemoji": "^12.1.0",
|
"@types/twemoji": "^12.1.0",
|
||||||
"ava": "^3.15.0",
|
"ava": "^3.15.0",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"source-map-support": "^0.5.16",
|
"source-map-support": "^0.5.16"
|
||||||
"tsc-watch": "^5.0.2"
|
|
||||||
},
|
},
|
||||||
"ava": {
|
"ava": {
|
||||||
"files": [
|
"files": [
|
||||||
|
|
40
backend/src/Blocker.ts
Normal file
40
backend/src/Blocker.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
export type Block = {
|
||||||
|
count: number;
|
||||||
|
unblock: () => void;
|
||||||
|
getPromise: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Blocker {
|
||||||
|
#blocks: Map<string, Block> = new Map();
|
||||||
|
|
||||||
|
block(key: string): void {
|
||||||
|
if (!this.#blocks.has(key)) {
|
||||||
|
const promise = new Promise<void>((resolve) => {
|
||||||
|
this.#blocks.set(key, {
|
||||||
|
count: 0, // Incremented to 1 further below
|
||||||
|
unblock() {
|
||||||
|
this.count--;
|
||||||
|
if (this.count === 0) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPromise: () => promise, // :d
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.#blocks.get(key)!.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
unblock(key: string): void {
|
||||||
|
if (this.#blocks.has(key)) {
|
||||||
|
this.#blocks.get(key)!.unblock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitToBeUnblocked(key: string): Promise<void> {
|
||||||
|
if (!this.#blocks.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.#blocks.get(key)!.getPromise();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ export class SimpleError extends Error {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[util.inspect.custom](depth, options) {
|
[util.inspect.custom]() {
|
||||||
return `Error: ${this.message}`;
|
return `Error: ${this.message}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ import { ApiLogins } from "../data/ApiLogins";
|
||||||
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
||||||
import { ApiUserInfo } from "../data/ApiUserInfo";
|
import { ApiUserInfo } from "../data/ApiUserInfo";
|
||||||
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
||||||
import { ok } from "./responses";
|
|
||||||
import { env } from "../env";
|
import { env } from "../env";
|
||||||
|
import { ok } from "./responses";
|
||||||
|
|
||||||
interface IPassportApiUser {
|
interface IPassportApiUser {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
@ -18,7 +18,6 @@ interface IPassportApiUser {
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
// tslint:disable-next-line:no-empty-interface
|
|
||||||
interface User extends IPassportApiUser {}
|
interface User extends IPassportApiUser {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +55,7 @@ export function initAuth(app: express.Express) {
|
||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
|
|
||||||
passport.serializeUser((user, done) => done(null, user));
|
passport.serializeUser((user, done) => done(null, user));
|
||||||
passport.deserializeUser((user, done) => done(null, user));
|
passport.deserializeUser((user, done) => done(null, user as IPassportApiUser));
|
||||||
|
|
||||||
const apiLogins = new ApiLogins();
|
const apiLogins = new ApiLogins();
|
||||||
const apiUserInfo = new ApiUserInfo();
|
const apiUserInfo = new ApiUserInfo();
|
||||||
|
@ -151,6 +150,7 @@ export function initAuth(app: express.Express) {
|
||||||
export function apiTokenAuthHandlers() {
|
export function apiTokenAuthHandlers() {
|
||||||
return [
|
return [
|
||||||
passport.authenticate("api-token", { failWithError: true }),
|
passport.authenticate("api-token", { failWithError: true }),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
(err, req: Request, res: Response, next) => {
|
(err, req: Request, res: Response, next) => {
|
||||||
return res.status(401).json({ error: err.message });
|
return res.status(401).json({ error: err.message });
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,9 +54,10 @@ export function initDocs(app: express.Express) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = plugin.name;
|
const name = plugin.name;
|
||||||
const info = plugin.info || {};
|
const info = { ...(plugin.info || {}) };
|
||||||
|
delete info.configSchema;
|
||||||
|
|
||||||
const commands = (plugin.commands || []).map((cmd) => ({
|
const messageCommands = (plugin.messageCommands || []).map((cmd) => ({
|
||||||
trigger: cmd.trigger,
|
trigger: cmd.trigger,
|
||||||
permission: cmd.permission,
|
permission: cmd.permission,
|
||||||
signature: cmd.signature,
|
signature: cmd.signature,
|
||||||
|
@ -66,14 +67,14 @@ export function initDocs(app: express.Express) {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const defaultOptions = plugin.defaultOptions || {};
|
const defaultOptions = plugin.defaultOptions || {};
|
||||||
const configSchema = plugin.configSchema && formatConfigSchema(plugin.configSchema);
|
const configSchema = plugin.info?.configSchema && formatConfigSchema(plugin.info.configSchema);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
name,
|
name,
|
||||||
info,
|
info,
|
||||||
configSchema,
|
configSchema,
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
commands,
|
messageCommands,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import { ApiPermissions } from "@shared/apiPermissions";
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import { YAMLException } from "js-yaml";
|
import { YAMLException } from "js-yaml";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { Queue } from "../Queue";
|
||||||
import { validateGuildConfig } from "../configValidator";
|
import { validateGuildConfig } from "../configValidator";
|
||||||
import { AllowedGuilds } from "../data/AllowedGuilds";
|
import { AllowedGuilds } from "../data/AllowedGuilds";
|
||||||
|
import { ApiAuditLog } from "../data/ApiAuditLog";
|
||||||
import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments";
|
import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments";
|
||||||
import { Configs } from "../data/Configs";
|
import { Configs } from "../data/Configs";
|
||||||
|
import { AuditLogEventTypes } from "../data/apiAuditLogTypes";
|
||||||
|
import { isSnowflake } from "../utils";
|
||||||
|
import { loadYamlSafely } from "../utils/loadYamlSafely";
|
||||||
|
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
|
||||||
import { apiTokenAuthHandlers } from "./auth";
|
import { apiTokenAuthHandlers } from "./auth";
|
||||||
import { hasGuildPermission, requireGuildPermission } from "./permissions";
|
import { hasGuildPermission, requireGuildPermission } from "./permissions";
|
||||||
import { clientError, ok, serverError, unauthorized } from "./responses";
|
import { clientError, ok, serverError, unauthorized } from "./responses";
|
||||||
import { loadYamlSafely } from "../utils/loadYamlSafely";
|
|
||||||
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
|
|
||||||
import { isSnowflake } from "../utils";
|
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { ApiAuditLog } from "../data/ApiAuditLog";
|
|
||||||
import { AuditLogEventTypes } from "../data/apiAuditLogTypes";
|
|
||||||
import { Queue } from "../Queue";
|
|
||||||
|
|
||||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||||
const auditLog = new ApiAuditLog();
|
const auditLog = new ApiAuditLog();
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { ApiPermissions } from "@shared/apiPermissions";
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import { requireGuildPermission } from "../permissions";
|
|
||||||
import { clientError, ok } from "../responses";
|
|
||||||
import { GuildCases } from "../../data/GuildCases";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { Case } from "../../data/entities/Case";
|
|
||||||
import { rateLimit } from "../rateLimits";
|
|
||||||
import { MINUTES } from "../../utils";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { GuildCases } from "../../data/GuildCases";
|
||||||
|
import { Case } from "../../data/entities/Case";
|
||||||
|
import { MINUTES } from "../../utils";
|
||||||
|
import { requireGuildPermission } from "../permissions";
|
||||||
|
import { rateLimit } from "../rateLimits";
|
||||||
|
import { clientError, ok } from "../responses";
|
||||||
|
|
||||||
const caseHandlingModeSchema = z.union([
|
const caseHandlingModeSchema = z.union([
|
||||||
z.literal("replace"),
|
z.literal("replace"),
|
||||||
|
@ -50,7 +50,7 @@ export function initGuildsImportExportAPI(guildRouter: express.Router) {
|
||||||
importExportRouter.get(
|
importExportRouter.get(
|
||||||
"/:guildId/pre-import",
|
"/:guildId/pre-import",
|
||||||
requireGuildPermission(ApiPermissions.ManageAccess),
|
requireGuildPermission(ApiPermissions.ManageAccess),
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request) => {
|
||||||
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
||||||
const minNum = await guildCases.getMinCaseNumber();
|
const minNum = await guildCases.getMinCaseNumber();
|
||||||
const maxNum = await guildCases.getMaxCaseNumber();
|
const maxNum = await guildCases.getMaxCaseNumber();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { apiTokenAuthHandlers } from "../auth";
|
import { apiTokenAuthHandlers } from "../auth";
|
||||||
import { initGuildsMiscAPI } from "./misc";
|
|
||||||
import { initGuildsImportExportAPI } from "./importExport";
|
import { initGuildsImportExportAPI } from "./importExport";
|
||||||
|
import { initGuildsMiscAPI } from "./misc";
|
||||||
|
|
||||||
export function initGuildsAPI(app: express.Express) {
|
export function initGuildsAPI(app: express.Express) {
|
||||||
const guildRouter = express.Router();
|
const guildRouter = express.Router();
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
import { ApiPermissions } from "@shared/apiPermissions";
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import { YAMLException } from "js-yaml";
|
import { YAMLException } from "js-yaml";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { Queue } from "../../Queue";
|
||||||
import { validateGuildConfig } from "../../configValidator";
|
import { validateGuildConfig } from "../../configValidator";
|
||||||
import { AllowedGuilds } from "../../data/AllowedGuilds";
|
import { AllowedGuilds } from "../../data/AllowedGuilds";
|
||||||
|
import { ApiAuditLog } from "../../data/ApiAuditLog";
|
||||||
import { ApiPermissionAssignments, ApiPermissionTypes } from "../../data/ApiPermissionAssignments";
|
import { ApiPermissionAssignments, ApiPermissionTypes } from "../../data/ApiPermissionAssignments";
|
||||||
import { Configs } from "../../data/Configs";
|
import { Configs } from "../../data/Configs";
|
||||||
import { apiTokenAuthHandlers } from "../auth";
|
import { AuditLogEventTypes } from "../../data/apiAuditLogTypes";
|
||||||
import { hasGuildPermission, requireGuildPermission } from "../permissions";
|
import { isSnowflake } from "../../utils";
|
||||||
import { clientError, ok, serverError, unauthorized } from "../responses";
|
|
||||||
import { loadYamlSafely } from "../../utils/loadYamlSafely";
|
import { loadYamlSafely } from "../../utils/loadYamlSafely";
|
||||||
import { ObjectAliasError } from "../../utils/validateNoObjectAliases";
|
import { ObjectAliasError } from "../../utils/validateNoObjectAliases";
|
||||||
import { isSnowflake } from "../../utils";
|
import { hasGuildPermission, requireGuildPermission } from "../permissions";
|
||||||
import moment from "moment-timezone";
|
import { clientError, ok, serverError, unauthorized } from "../responses";
|
||||||
import { ApiAuditLog } from "../../data/ApiAuditLog";
|
|
||||||
import { AuditLogEventTypes } from "../../data/apiAuditLogTypes";
|
|
||||||
import { Queue } from "../../Queue";
|
|
||||||
import { GuildCases } from "../../data/GuildCases";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||||
const auditLog = new ApiAuditLog();
|
const auditLog = new ApiAuditLog();
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
// KEEP THIS AS FIRST IMPORT
|
||||||
|
// See comment in module for details
|
||||||
|
import "../threadsSignalFix";
|
||||||
|
|
||||||
import { connect } from "../data/db";
|
import { connect } from "../data/db";
|
||||||
import { setIsAPI } from "../globals";
|
|
||||||
import { env } from "../env";
|
import { env } from "../env";
|
||||||
|
import { setIsAPI } from "../globals";
|
||||||
|
|
||||||
if (!env.KEY) {
|
if (!env.KEY) {
|
||||||
// tslint:disable-next-line:no-console
|
// tslint:disable-next-line:no-console
|
||||||
|
@ -20,5 +24,5 @@ setIsAPI(true);
|
||||||
// Connect to the database before loading the rest of the code (that depend on the database connection)
|
// Connect to the database before loading the rest of the code (that depend on the database connection)
|
||||||
console.log("Connecting to database..."); // tslint:disable-line
|
console.log("Connecting to database..."); // tslint:disable-line
|
||||||
connect().then(() => {
|
connect().then(() => {
|
||||||
import("./start");
|
import("./start.js");
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ export function unauthorized(res: Response) {
|
||||||
res.status(403).json({ error: "Unauthorized" });
|
res.status(403).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function error(res: Response, message: string, statusCode: number = 500) {
|
export function error(res: Response, message: string, statusCode = 500) {
|
||||||
res.status(statusCode).json({ error: message });
|
res.status(statusCode).json({ error: message });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import multer from "multer";
|
||||||
import { TokenError } from "passport-oauth2";
|
import { TokenError } from "passport-oauth2";
|
||||||
|
import { env } from "../env";
|
||||||
import { initArchives } from "./archives";
|
import { initArchives } from "./archives";
|
||||||
import { initAuth } from "./auth";
|
import { initAuth } from "./auth";
|
||||||
import { initDocs } from "./docs";
|
import { initDocs } from "./docs";
|
||||||
import { initGuildsAPI } from "./guilds/index";
|
import { initGuildsAPI } from "./guilds/index";
|
||||||
import { clientError, error, notFound } from "./responses";
|
import { clientError, error, notFound } from "./responses";
|
||||||
import { startBackgroundTasks } from "./tasks";
|
import { startBackgroundTasks } from "./tasks";
|
||||||
import multer from "multer";
|
|
||||||
import { env } from "../env";
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ app.get("/", (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error response
|
// Error response
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
if (err instanceof TokenError) {
|
if (err instanceof TokenError) {
|
||||||
clientError(res, "Invalid code");
|
clientError(res, "Invalid code");
|
||||||
|
@ -45,6 +46,7 @@ app.use((err, req, res, next) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 404 response
|
// 404 response
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
return notFound(res);
|
return notFound(res);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
import { GuildChannel, GuildMember, Snowflake, Util, User, GuildTextBasedChannel } from "discord.js";
|
import {
|
||||||
import { baseCommandParameterTypeHelpers, baseTypeConverters, CommandContext, TypeConversionError } from "knub";
|
escapeCodeBlock,
|
||||||
|
escapeInlineCode,
|
||||||
|
GuildChannel,
|
||||||
|
GuildMember,
|
||||||
|
GuildTextBasedChannel,
|
||||||
|
Snowflake,
|
||||||
|
User,
|
||||||
|
} from "discord.js";
|
||||||
|
import {
|
||||||
|
baseCommandParameterTypeHelpers,
|
||||||
|
CommandContext,
|
||||||
|
messageCommandBaseTypeConverters,
|
||||||
|
TypeConversionError,
|
||||||
|
} from "knub";
|
||||||
import { createTypeHelper } from "knub-command-manager";
|
import { createTypeHelper } from "knub-command-manager";
|
||||||
import {
|
import {
|
||||||
channelMentionRegex,
|
channelMentionRegex,
|
||||||
|
@ -14,11 +27,9 @@ import {
|
||||||
import { isValidTimezone } from "./utils/isValidTimezone";
|
import { isValidTimezone } from "./utils/isValidTimezone";
|
||||||
import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarget";
|
import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarget";
|
||||||
import { inputPatternToRegExp } from "./validatorUtils";
|
import { inputPatternToRegExp } from "./validatorUtils";
|
||||||
import { getChannelId } from "knub/dist/utils";
|
|
||||||
import { disableCodeBlocks } from "knub/dist/helpers";
|
|
||||||
|
|
||||||
export const commandTypes = {
|
export const commandTypes = {
|
||||||
...baseTypeConverters,
|
...messageCommandBaseTypeConverters,
|
||||||
|
|
||||||
delay(value) {
|
delay(value) {
|
||||||
const result = convertDelayStringToMS(value);
|
const result = convertDelayStringToMS(value);
|
||||||
|
@ -32,7 +43,7 @@ export const commandTypes = {
|
||||||
async resolvedUser(value, context: CommandContext<any>) {
|
async resolvedUser(value, context: CommandContext<any>) {
|
||||||
const result = await resolveUser(context.pluginData.client, value);
|
const result = await resolveUser(context.pluginData.client, value);
|
||||||
if (result == null || result instanceof UnknownUser) {
|
if (result == null || result instanceof UnknownUser) {
|
||||||
throw new TypeConversionError(`User \`${Util.escapeCodeBlock(value)}\` was not found`);
|
throw new TypeConversionError(`User \`${escapeCodeBlock(value)}\` was not found`);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -40,7 +51,7 @@ export const commandTypes = {
|
||||||
async resolvedUserLoose(value, context: CommandContext<any>) {
|
async resolvedUserLoose(value, context: CommandContext<any>) {
|
||||||
const result = await resolveUser(context.pluginData.client, value);
|
const result = await resolveUser(context.pluginData.client, value);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
throw new TypeConversionError(`Invalid user: \`${Util.escapeCodeBlock(value)}\``);
|
throw new TypeConversionError(`Invalid user: \`${escapeCodeBlock(value)}\``);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -52,9 +63,7 @@ export const commandTypes = {
|
||||||
|
|
||||||
const result = await resolveMember(context.pluginData.client, context.message.channel.guild, value);
|
const result = await resolveMember(context.pluginData.client, context.message.channel.guild, value);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
throw new TypeConversionError(
|
throw new TypeConversionError(`Member \`${escapeCodeBlock(value)}\` was not found or they have left the server`);
|
||||||
`Member \`${Util.escapeCodeBlock(value)}\` was not found or they have left the server`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -64,7 +73,7 @@ export const commandTypes = {
|
||||||
|
|
||||||
const result = await resolveMessageTarget(context.pluginData, value);
|
const result = await resolveMessageTarget(context.pluginData, value);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new TypeConversionError(`Unknown message \`${Util.escapeInlineCode(value)}\``);
|
throw new TypeConversionError(`Unknown message \`${escapeInlineCode(value)}\``);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -84,28 +93,27 @@ export const commandTypes = {
|
||||||
return value as Snowflake;
|
return value as Snowflake;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new TypeConversionError(`Could not parse ID: \`${Util.escapeInlineCode(value)}\``);
|
throw new TypeConversionError(`Could not parse ID: \`${escapeInlineCode(value)}\``);
|
||||||
},
|
},
|
||||||
|
|
||||||
regex(value: string, context: CommandContext<any>): RegExp {
|
regex(value: string): RegExp {
|
||||||
try {
|
try {
|
||||||
return inputPatternToRegExp(value);
|
return inputPatternToRegExp(value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new TypeConversionError(`Could not parse RegExp: \`${Util.escapeInlineCode(e.message)}\``);
|
throw new TypeConversionError(`Could not parse RegExp: \`${escapeInlineCode(e.message)}\``);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
timezone(value: string) {
|
timezone(value: string) {
|
||||||
if (!isValidTimezone(value)) {
|
if (!isValidTimezone(value)) {
|
||||||
throw new TypeConversionError(`Invalid timezone: ${Util.escapeInlineCode(value)}`);
|
throw new TypeConversionError(`Invalid timezone: ${escapeInlineCode(value)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
guildTextBasedChannel(value: string, context: CommandContext<any>) {
|
guildTextBasedChannel(value: string, context: CommandContext<any>) {
|
||||||
// FIXME: Remove once Knub's types have been fixed
|
return messageCommandBaseTypeConverters.textChannel(value, context);
|
||||||
return baseTypeConverters.textChannel(value, context) as GuildTextBasedChannel;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { configUtils, ConfigValidationError, PluginOptions } from "knub";
|
import { ConfigValidationError, PluginConfigManager } from "knub";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { guildPlugins } from "./plugins/availablePlugins";
|
|
||||||
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
||||||
|
import { guildPlugins } from "./plugins/availablePlugins";
|
||||||
import { PartialZeppelinGuildConfigSchema, ZeppelinGuildConfig } from "./types";
|
import { PartialZeppelinGuildConfigSchema, ZeppelinGuildConfig } from "./types";
|
||||||
import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils";
|
import { StrictValidationError, decodeAndValidateStrict } from "./validatorUtils";
|
||||||
|
|
||||||
const pluginNameToPlugin = new Map<string, ZeppelinPlugin>();
|
const pluginNameToPlugin = new Map<string, ZeppelinPlugin>();
|
||||||
for (const plugin of guildPlugins) {
|
for (const plugin of guildPlugins) {
|
||||||
|
@ -34,9 +34,12 @@ export async function validateGuildConfig(config: any): Promise<string | null> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = pluginNameToPlugin.get(pluginName)!;
|
const plugin = pluginNameToPlugin.get(pluginName)!;
|
||||||
|
const configManager = new PluginConfigManager(plugin.defaultOptions || { config: {} }, pluginOptions, {
|
||||||
|
levels: {},
|
||||||
|
parser: plugin.configParser,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const mergedOptions = configUtils.mergeConfig(plugin.defaultOptions || {}, pluginOptions);
|
await configManager.init();
|
||||||
await plugin.configPreprocessor?.(mergedOptions as unknown as PluginOptions<any>, true);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ConfigValidationError || err instanceof StrictValidationError) {
|
if (err instanceof ConfigValidationError || err instanceof StrictValidationError) {
|
||||||
return `${pluginName}: ${err.message}`;
|
return `${pluginName}: ${err.message}`;
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
|
import moment from "moment-timezone";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { getRepository, Repository } from "typeorm";
|
||||||
|
import { DBDateFormat } from "../utils";
|
||||||
import { ApiPermissionTypes } from "./ApiPermissionAssignments";
|
import { ApiPermissionTypes } from "./ApiPermissionAssignments";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
import { AllowedGuild } from "./entities/AllowedGuild";
|
import { AllowedGuild } from "./entities/AllowedGuild";
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { DBDateFormat } from "../utils";
|
|
||||||
import { env } from "../env";
|
|
||||||
|
|
||||||
export class AllowedGuilds extends BaseRepository {
|
export class AllowedGuilds extends BaseRepository {
|
||||||
private allowedGuilds: Repository<AllowedGuild>;
|
private allowedGuilds: Repository<AllowedGuild>;
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import { getRepository, Repository } from "typeorm/index";
|
import { getRepository, Repository } from "typeorm/index";
|
||||||
import { ApiAuditLogEntry } from "./entities/ApiAuditLogEntry";
|
|
||||||
import { ApiLogin } from "./entities/ApiLogin";
|
|
||||||
import { AuditLogEventData, AuditLogEventType } from "./apiAuditLogTypes";
|
import { AuditLogEventData, AuditLogEventType } from "./apiAuditLogTypes";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { ApiAuditLogEntry } from "./entities/ApiAuditLogEntry";
|
||||||
|
|
||||||
export class ApiAuditLog extends BaseRepository {
|
export class ApiAuditLog extends BaseRepository {
|
||||||
private auditLog: Repository<ApiAuditLogEntry<any>>;
|
private auditLog: Repository<ApiAuditLogEntry<any>>;
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { ApiPermissions } from "@shared/apiPermissions";
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
|
|
||||||
import { Permissions } from "discord.js";
|
|
||||||
import { ApiAuditLog } from "./ApiAuditLog";
|
import { ApiAuditLog } from "./ApiAuditLog";
|
||||||
import { AuditLogEventTypes } from "./apiAuditLogTypes";
|
import { AuditLogEventTypes } from "./apiAuditLogTypes";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
|
||||||
|
|
||||||
export enum ApiPermissionTypes {
|
export enum ApiPermissionTypes {
|
||||||
User = "USER",
|
User = "USER",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { getRepository, Repository } from "typeorm";
|
||||||
import { DBDateFormat } from "../utils";
|
import { DBDateFormat } from "../utils";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
import { connection } from "./db";
|
import { connection } from "./db";
|
||||||
import { ApiUserInfo as ApiUserInfoEntity, ApiUserInfoData } from "./entities/ApiUserInfo";
|
import { ApiUserInfoData, ApiUserInfo as ApiUserInfoEntity } from "./entities/ApiUserInfo";
|
||||||
|
|
||||||
export class ApiUserInfo extends BaseRepository {
|
export class ApiUserInfo extends BaseRepository {
|
||||||
private apiUserInfo: Repository<ApiUserInfoEntity>;
|
private apiUserInfo: Repository<ApiUserInfoEntity>;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
||||||
|
|
||||||
export class Archives extends BaseRepository {
|
export class Archives extends BaseRepository {
|
||||||
protected archives: Repository<ArchiveEntry>;
|
protected archives: Repository<ArchiveEntry>;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
|
||||||
export class BaseGuildRepository<TEntity extends unknown = unknown> extends BaseRepository<TEntity> {
|
export class BaseGuildRepository<TEntity = unknown> extends BaseRepository<TEntity> {
|
||||||
private static guildInstances: Map<string, any>;
|
private static guildInstances: Map<string, any>;
|
||||||
|
|
||||||
protected guildId: string;
|
protected guildId: string;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { asyncMap } from "../utils/async";
|
import { asyncMap } from "../utils/async";
|
||||||
|
|
||||||
export class BaseRepository<TEntity extends unknown = unknown> {
|
export class BaseRepository<TEntity = unknown> {
|
||||||
private nextRelations: string[];
|
private nextRelations: string[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -6,15 +6,16 @@ import { cleanupConfigs } from "./cleanup/configs";
|
||||||
import { connection } from "./db";
|
import { connection } from "./db";
|
||||||
import { Config } from "./entities/Config";
|
import { Config } from "./entities/Config";
|
||||||
|
|
||||||
|
const CLEANUP_INTERVAL = 1 * HOURS;
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
await cleanupConfigs();
|
||||||
|
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
if (isAPI()) {
|
if (isAPI()) {
|
||||||
const CLEANUP_INTERVAL = 1 * HOURS;
|
|
||||||
|
|
||||||
async function cleanup() {
|
|
||||||
await cleanupConfigs();
|
|
||||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start first cleanup 30 seconds after startup
|
// Start first cleanup 30 seconds after startup
|
||||||
|
// TODO: Move to bot startup code
|
||||||
setTimeout(cleanup, 30 * SECONDS);
|
setTimeout(cleanup, 30 * SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
"MEMBER_SOFTBAN": "{timestamp} 🔨 {userMention(member)} was softbanned by {userMention(mod)}",
|
"MEMBER_SOFTBAN": "{timestamp} 🔨 {userMention(member)} was softbanned by {userMention(mod)}",
|
||||||
"MEMBER_JOIN": "{timestamp} 📥 {new} {userMention(member)} joined (created {account_age} ago)",
|
"MEMBER_JOIN": "{timestamp} 📥 {new} {userMention(member)} joined (created {account_age} ago)",
|
||||||
"MEMBER_LEAVE": "{timestamp} 📤 {userMention(member)} left the server",
|
"MEMBER_LEAVE": "{timestamp} 📤 {userMention(member)} left the server",
|
||||||
"MEMBER_ROLE_ADD": "{timestamp} 🔑 {userMention(member)} received roles: **{roles}**",
|
"MEMBER_ROLE_ADD": "{timestamp} 🔑 {userMention(mod)} added roles for {userMention(member)}: **{roles}**",
|
||||||
"MEMBER_ROLE_REMOVE": "{timestamp} 🔑 {userMention(member)} lost roles: **{roles}**",
|
"MEMBER_ROLE_REMOVE": "{timestamp} 🔑 {userMention(mod)} removed roles from {userMention(member)}: **{roles}**",
|
||||||
"MEMBER_ROLE_CHANGES": "{timestamp} 🔑 {userMention(member)} had role changes: received **{addedRoles}**, lost **{removedRoles}**",
|
"MEMBER_ROLE_CHANGES": "{timestamp} 🔑 {userMention(member)} had role changes: received **{addedRoles}**, lost **{removedRoles}**",
|
||||||
"MEMBER_NICK_CHANGE": "{timestamp} ✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**",
|
"MEMBER_NICK_CHANGE": "{timestamp} ✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**",
|
||||||
"MEMBER_USERNAME_CHANGE": "{timestamp} ✏ {userMention(user)}: username changed from **{oldName}** to **{newName}**",
|
"MEMBER_USERNAME_CHANGE": "{timestamp} ✏ {userMention(user)}: username changed from **{oldName}** to **{newName}**",
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import { Guild, Snowflake, User } from "discord.js";
|
import { Guild, Snowflake } from "discord.js";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { isDefaultSticker } from "src/utils/isDefaultSticker";
|
import { isDefaultSticker } from "src/utils/isDefaultSticker";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository, getRepository } from "typeorm";
|
||||||
import { renderTemplate, TemplateSafeValueContainer } from "../templateFormatter";
|
import { TemplateSafeValueContainer, renderTemplate } from "../templateFormatter";
|
||||||
import { trimLines } from "../utils";
|
import { trimLines } from "../utils";
|
||||||
|
import { decrypt, encrypt } from "../utils/crypt";
|
||||||
|
import { channelToTemplateSafeChannel, guildToTemplateSafeGuild } from "../utils/templateSafeObjects";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
||||||
import {
|
|
||||||
channelToTemplateSafeChannel,
|
|
||||||
guildToTemplateSafeGuild,
|
|
||||||
userToTemplateSafeUser,
|
|
||||||
} from "../utils/templateSafeObjects";
|
|
||||||
import { SavedMessage } from "./entities/SavedMessage";
|
import { SavedMessage } from "./entities/SavedMessage";
|
||||||
import { decrypt, encrypt } from "../utils/crypt";
|
|
||||||
|
|
||||||
const DEFAULT_EXPIRY_DAYS = 30;
|
const DEFAULT_EXPIRY_DAYS = 30;
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import { getRepository, In, InsertResult, Repository } from "typeorm";
|
import { getRepository, In, InsertResult, Repository } from "typeorm";
|
||||||
|
import { Queue } from "../Queue";
|
||||||
|
import { chunkArray } from "../utils";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { CaseTypes } from "./CaseTypes";
|
import { CaseTypes } from "./CaseTypes";
|
||||||
import { connection } from "./db";
|
import { connection } from "./db";
|
||||||
import { Case } from "./entities/Case";
|
import { Case } from "./entities/Case";
|
||||||
import { CaseNote } from "./entities/CaseNote";
|
import { CaseNote } from "./entities/CaseNote";
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { chunkArray } from "../utils";
|
|
||||||
import { Queue } from "../Queue";
|
|
||||||
|
|
||||||
const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
|
|
||||||
|
|
||||||
export class GuildCases extends BaseGuildRepository {
|
export class GuildCases extends BaseGuildRepository {
|
||||||
private cases: Repository<Case>;
|
private cases: Repository<Case>;
|
||||||
|
|
|
@ -161,7 +161,7 @@ export class GuildCounters extends BaseGuildRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
|
const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
|
||||||
if (decayAmountToApply === 0) {
|
if (decayAmountToApply === 0 || Number.isNaN(decayAmountToApply)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { Mute } from "./entities/Mute";
|
import { Mute } from "./entities/Mute";
|
||||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
|
||||||
import { Reminder } from "./entities/Reminder";
|
import { Reminder } from "./entities/Reminder";
|
||||||
|
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||||
import { Tempban } from "./entities/Tempban";
|
import { Tempban } from "./entities/Tempban";
|
||||||
import { VCAlert } from "./entities/VCAlert";
|
import { VCAlert } from "./entities/VCAlert";
|
||||||
|
|
||||||
interface GuildEventArgs extends Record<string, unknown[]> {
|
interface GuildEventArgs extends Record<string, unknown[]> {
|
||||||
expiredMute: [Mute];
|
expiredMute: [Mute];
|
||||||
|
timeoutMuteToRenew: [Mute];
|
||||||
scheduledPost: [ScheduledPost];
|
scheduledPost: [ScheduledPost];
|
||||||
reminder: [Reminder];
|
reminder: [Reminder];
|
||||||
expiredTempban: [Tempban];
|
expiredTempban: [Tempban];
|
||||||
|
|
101
backend/src/data/GuildMemberCache.ts
Normal file
101
backend/src/data/GuildMemberCache.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { getRepository, Repository } from "typeorm";
|
||||||
|
import { Blocker } from "../Blocker";
|
||||||
|
import { DBDateFormat, MINUTES } from "../utils";
|
||||||
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { MemberCacheItem } from "./entities/MemberCacheItem";
|
||||||
|
|
||||||
|
const SAVE_PENDING_BLOCKER_KEY = "save-pending" as const;
|
||||||
|
|
||||||
|
const DELETION_DELAY = 5 * MINUTES;
|
||||||
|
|
||||||
|
type UpdateData = Pick<MemberCacheItem, "username" | "nickname" | "roles">;
|
||||||
|
|
||||||
|
export class GuildMemberCache extends BaseGuildRepository {
|
||||||
|
#memberCache: Repository<MemberCacheItem>;
|
||||||
|
|
||||||
|
#pendingUpdates: Map<string, Partial<MemberCacheItem>>;
|
||||||
|
|
||||||
|
#blocker: Blocker;
|
||||||
|
|
||||||
|
constructor(guildId: string) {
|
||||||
|
super(guildId);
|
||||||
|
this.#memberCache = getRepository(MemberCacheItem);
|
||||||
|
this.#pendingUpdates = new Map();
|
||||||
|
this.#blocker = new Blocker();
|
||||||
|
}
|
||||||
|
|
||||||
|
async savePendingUpdates(): Promise<void> {
|
||||||
|
await this.#blocker.waitToBeUnblocked(SAVE_PENDING_BLOCKER_KEY);
|
||||||
|
|
||||||
|
if (this.#pendingUpdates.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#blocker.block(SAVE_PENDING_BLOCKER_KEY);
|
||||||
|
|
||||||
|
const entitiesToSave = Array.from(this.#pendingUpdates.values());
|
||||||
|
this.#pendingUpdates.clear();
|
||||||
|
|
||||||
|
await this.#memberCache.upsert(entitiesToSave, ["guild_id", "user_id"]).finally(() => {
|
||||||
|
this.#blocker.unblock(SAVE_PENDING_BLOCKER_KEY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCachedMemberData(userId: string): Promise<MemberCacheItem | null> {
|
||||||
|
await this.#blocker.waitToBeUnblocked(SAVE_PENDING_BLOCKER_KEY);
|
||||||
|
|
||||||
|
const dbItem = await this.#memberCache.findOne({
|
||||||
|
where: {
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const pendingItem = this.#pendingUpdates.get(userId);
|
||||||
|
if (!dbItem && !pendingItem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = new MemberCacheItem();
|
||||||
|
Object.assign(item, dbItem ?? {});
|
||||||
|
Object.assign(item, pendingItem ?? {});
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCachedMemberData(userId: string, data: UpdateData): Promise<void> {
|
||||||
|
await this.#blocker.waitToBeUnblocked(SAVE_PENDING_BLOCKER_KEY);
|
||||||
|
|
||||||
|
if (!this.#pendingUpdates.has(userId)) {
|
||||||
|
const newItem = new MemberCacheItem();
|
||||||
|
newItem.guild_id = this.guildId;
|
||||||
|
newItem.user_id = userId;
|
||||||
|
this.#pendingUpdates.set(userId, newItem);
|
||||||
|
}
|
||||||
|
Object.assign(this.#pendingUpdates.get(userId)!, data);
|
||||||
|
this.#pendingUpdates.get(userId)!.last_seen = moment().format("YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
async markMemberForDeletion(userId: string): Promise<void> {
|
||||||
|
await this.#memberCache.update(
|
||||||
|
{
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delete_at: moment().add(DELETION_DELAY, "ms").format(DBDateFormat),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unmarkMemberForDeletion(userId: string): Promise<void> {
|
||||||
|
await this.#memberCache.update(
|
||||||
|
{
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delete_at: null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,18 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { Brackets, getRepository, Repository } from "typeorm";
|
import { Brackets, getRepository, Repository } from "typeorm";
|
||||||
|
import { DBDateFormat } from "../utils";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { Mute } from "./entities/Mute";
|
import { Mute } from "./entities/Mute";
|
||||||
|
import { MuteTypes } from "./MuteTypes";
|
||||||
|
|
||||||
|
export type AddMuteParams = {
|
||||||
|
userId: Mute["user_id"];
|
||||||
|
type: MuteTypes;
|
||||||
|
expiresAt: number | null;
|
||||||
|
rolesToRestore?: Mute["roles_to_restore"];
|
||||||
|
muteRole?: string | null;
|
||||||
|
timeoutExpiresAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class GuildMutes extends BaseGuildRepository {
|
export class GuildMutes extends BaseGuildRepository {
|
||||||
private mutes: Repository<Mute>;
|
private mutes: Repository<Mute>;
|
||||||
|
@ -34,14 +45,18 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
return mute != null;
|
return mute != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addMute(userId, expiryTime, rolesToRestore?: string[]): Promise<Mute> {
|
async addMute(params: AddMuteParams): Promise<Mute> {
|
||||||
const expiresAt = expiryTime ? moment.utc().add(expiryTime, "ms").format("YYYY-MM-DD HH:mm:ss") : null;
|
const expiresAt = params.expiresAt ? moment.utc(params.expiresAt).format(DBDateFormat) : null;
|
||||||
|
const timeoutExpiresAt = params.timeoutExpiresAt ? moment.utc(params.timeoutExpiresAt).format(DBDateFormat) : null;
|
||||||
|
|
||||||
const result = await this.mutes.insert({
|
const result = await this.mutes.insert({
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId,
|
user_id: params.userId,
|
||||||
|
type: params.type,
|
||||||
expires_at: expiresAt,
|
expires_at: expiresAt,
|
||||||
roles_to_restore: rolesToRestore ?? [],
|
roles_to_restore: params.rolesToRestore ?? [],
|
||||||
|
mute_role: params.muteRole,
|
||||||
|
timeout_expires_at: timeoutExpiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (await this.mutes.findOne({ where: result.identifiers[0] }))!;
|
return (await this.mutes.findOne({ where: result.identifiers[0] }))!;
|
||||||
|
@ -74,6 +89,32 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateExpiresAt(userId: string, timestamp: number | null): Promise<void> {
|
||||||
|
const expiresAt = timestamp ? moment.utc(timestamp).format("YYYY-MM-DD HH:mm:ss") : null;
|
||||||
|
await this.mutes.update(
|
||||||
|
{
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expires_at: expiresAt,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTimeoutExpiresAt(userId: string, timestamp: number): Promise<void> {
|
||||||
|
const timeoutExpiresAt = moment.utc(timestamp).format(DBDateFormat);
|
||||||
|
await this.mutes.update(
|
||||||
|
{
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout_expires_at: timeoutExpiresAt,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getActiveMutes(): Promise<Mute[]> {
|
async getActiveMutes(): Promise<Mute[]> {
|
||||||
return this.mutes
|
return this.mutes
|
||||||
.createQueryBuilder("mutes")
|
.createQueryBuilder("mutes")
|
||||||
|
@ -104,4 +145,16 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fillMissingMuteRole(muteRole: string): Promise<void> {
|
||||||
|
await this.mutes
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||||
|
.andWhere("type = :type", { type: MuteTypes.Role })
|
||||||
|
.andWhere("mute_role IS NULL")
|
||||||
|
.update({
|
||||||
|
mute_role: muteRole,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,15 +5,16 @@ import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { cleanupNicknames } from "./cleanup/nicknames";
|
import { cleanupNicknames } from "./cleanup/nicknames";
|
||||||
import { NicknameHistoryEntry } from "./entities/NicknameHistoryEntry";
|
import { NicknameHistoryEntry } from "./entities/NicknameHistoryEntry";
|
||||||
|
|
||||||
|
const CLEANUP_INTERVAL = 5 * MINUTES;
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
await cleanupNicknames();
|
||||||
|
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAPI()) {
|
if (!isAPI()) {
|
||||||
const CLEANUP_INTERVAL = 5 * MINUTES;
|
|
||||||
|
|
||||||
async function cleanup() {
|
|
||||||
await cleanupNicknames();
|
|
||||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start first cleanup 30 seconds after startup
|
// Start first cleanup 30 seconds after startup
|
||||||
|
// TODO: Move to bot startup code
|
||||||
setTimeout(cleanup, 30 * SECONDS);
|
setTimeout(cleanup, 30 * SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,6 @@ import { getRepository, Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { PersistedData } from "./entities/PersistedData";
|
import { PersistedData } from "./entities/PersistedData";
|
||||||
|
|
||||||
export interface IPartialPersistData {
|
|
||||||
roles?: string[];
|
|
||||||
nickname?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GuildPersistedData extends BaseGuildRepository {
|
export class GuildPersistedData extends BaseGuildRepository {
|
||||||
private persistedData: Repository<PersistedData>;
|
private persistedData: Repository<PersistedData>;
|
||||||
|
|
||||||
|
@ -24,11 +19,7 @@ export class GuildPersistedData extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(userId: string, data: IPartialPersistData = {}) {
|
async set(userId: string, data: Partial<PersistedData> = {}) {
|
||||||
const finalData: any = {};
|
|
||||||
if (data.roles) finalData.roles = data.roles.join(",");
|
|
||||||
if (data.nickname) finalData.nickname = data.nickname;
|
|
||||||
|
|
||||||
const existing = await this.find(userId);
|
const existing = await this.find(userId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await this.persistedData.update(
|
await this.persistedData.update(
|
||||||
|
@ -36,11 +27,11 @@ export class GuildPersistedData extends BaseGuildRepository {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
},
|
},
|
||||||
finalData,
|
data,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await this.persistedData.insert({
|
await this.persistedData.insert({
|
||||||
...finalData,
|
...data,
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { Reminder } from "./entities/Reminder";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { DBDateFormat } from "../utils";
|
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { RoleQueueItem } from "./entities/RoleQueueItem";
|
|
||||||
import { connection } from "./db";
|
|
||||||
import { RoleButtonsItem } from "./entities/RoleButtonsItem";
|
import { RoleButtonsItem } from "./entities/RoleButtonsItem";
|
||||||
|
|
||||||
export class GuildRoleButtons extends BaseGuildRepository {
|
export class GuildRoleButtons extends BaseGuildRepository {
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { Reminder } from "./entities/Reminder";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { DBDateFormat } from "../utils";
|
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { RoleQueueItem } from "./entities/RoleQueueItem";
|
|
||||||
import { connection } from "./db";
|
import { connection } from "./db";
|
||||||
|
import { RoleQueueItem } from "./entities/RoleQueueItem";
|
||||||
|
|
||||||
export class GuildRoleQueue extends BaseGuildRepository {
|
export class GuildRoleQueue extends BaseGuildRepository {
|
||||||
private roleQueue: Repository<RoleQueueItem>;
|
private roleQueue: Repository<RoleQueueItem>;
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import { GuildChannel, Message } from "discord.js";
|
import { GuildChannel, Message } from "discord.js";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository, getRepository } from "typeorm";
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
|
||||||
import { QueuedEventEmitter } from "../QueuedEventEmitter";
|
import { QueuedEventEmitter } from "../QueuedEventEmitter";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
|
||||||
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
|
|
||||||
import { buildEntity } from "./buildEntity";
|
|
||||||
import { noop } from "../utils";
|
import { noop } from "../utils";
|
||||||
import { decrypt } from "../utils/crypt";
|
|
||||||
import { decryptJson, encryptJson } from "../utils/cryptHelpers";
|
|
||||||
import { asyncMap } from "../utils/async";
|
import { asyncMap } from "../utils/async";
|
||||||
|
import { decryptJson, encryptJson } from "../utils/cryptHelpers";
|
||||||
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { buildEntity } from "./buildEntity";
|
||||||
|
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
|
||||||
|
|
||||||
export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
||||||
private messages: Repository<SavedMessage>;
|
private messages: Repository<SavedMessage>;
|
||||||
|
@ -53,13 +51,13 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
||||||
title: embed.title,
|
title: embed.title,
|
||||||
description: embed.description,
|
description: embed.description,
|
||||||
url: embed.url,
|
url: embed.url,
|
||||||
timestamp: embed.timestamp,
|
timestamp: embed.timestamp ? Date.parse(embed.timestamp) : null,
|
||||||
color: embed.color,
|
color: embed.color,
|
||||||
|
|
||||||
fields: embed.fields.map((field) => ({
|
fields: embed.fields.map((field) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
value: field.value,
|
value: field.value,
|
||||||
inline: field.inline,
|
inline: field.inline ?? false,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
author: embed.author
|
author: embed.author
|
||||||
|
@ -128,7 +126,7 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.data = await decryptJson(entity.data as unknown as string);
|
entity.data = (await decryptJson(entity.data as unknown as string)) as ISavedMessageData;
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
29
backend/src/data/MemberCache.ts
Normal file
29
backend/src/data/MemberCache.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { getRepository, Repository } from "typeorm";
|
||||||
|
import { DAYS } from "../utils";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { MemberCacheItem } from "./entities/MemberCacheItem";
|
||||||
|
|
||||||
|
const STALE_PERIOD = 90 * DAYS;
|
||||||
|
|
||||||
|
export class MemberCache extends BaseRepository {
|
||||||
|
#memberCache: Repository<MemberCacheItem>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.#memberCache = getRepository(MemberCacheItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteStaleData(): Promise<void> {
|
||||||
|
const cutoff = moment().subtract(STALE_PERIOD, "ms").format("YYYY-MM-DD");
|
||||||
|
await this.#memberCache.createQueryBuilder().where("last_seen < :cutoff", { cutoff }).delete().execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMarkedToBeDeletedEntries(): Promise<void> {
|
||||||
|
await this.#memberCache
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where("delete_at IS NOT NULL AND delete_at <= NOW()")
|
||||||
|
.delete()
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
4
backend/src/data/MuteTypes.ts
Normal file
4
backend/src/data/MuteTypes.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export enum MuteTypes {
|
||||||
|
Role = 1,
|
||||||
|
Timeout = 2,
|
||||||
|
}
|
|
@ -1,11 +1,16 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { Brackets, getRepository, Repository } from "typeorm";
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { Mute } from "./entities/Mute";
|
|
||||||
import { DAYS, DBDateFormat } from "../utils";
|
import { DAYS, DBDateFormat } from "../utils";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { Mute } from "./entities/Mute";
|
||||||
|
import { MuteTypes } from "./MuteTypes";
|
||||||
|
|
||||||
const OLD_EXPIRED_MUTE_THRESHOLD = 7 * DAYS;
|
const OLD_EXPIRED_MUTE_THRESHOLD = 7 * DAYS;
|
||||||
|
|
||||||
|
export const MAX_TIMEOUT_DURATION = 28 * DAYS;
|
||||||
|
// When a timeout is under this duration but the mute expires later, the timeout will be reset to max duration
|
||||||
|
export const TIMEOUT_RENEWAL_THRESHOLD = 21 * DAYS;
|
||||||
|
|
||||||
export class Mutes extends BaseRepository {
|
export class Mutes extends BaseRepository {
|
||||||
private mutes: Repository<Mute>;
|
private mutes: Repository<Mute>;
|
||||||
|
|
||||||
|
@ -14,7 +19,16 @@ export class Mutes extends BaseRepository {
|
||||||
this.mutes = getRepository(Mute);
|
this.mutes = getRepository(Mute);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSoonExpiringMutes(threshold: number): Promise<Mute[]> {
|
findMute(guildId: string, userId: string): Promise<Mute | undefined> {
|
||||||
|
return this.mutes.findOne({
|
||||||
|
where: {
|
||||||
|
guild_id: guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSoonExpiringMutes(threshold: number): Promise<Mute[]> {
|
||||||
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
|
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
|
||||||
return this.mutes
|
return this.mutes
|
||||||
.createQueryBuilder("mutes")
|
.createQueryBuilder("mutes")
|
||||||
|
@ -23,6 +37,16 @@ export class Mutes extends BaseRepository {
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTimeoutMutesToRenew(threshold: number): Promise<Mute[]> {
|
||||||
|
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
|
||||||
|
return this.mutes
|
||||||
|
.createQueryBuilder("mutes")
|
||||||
|
.andWhere("type = :type", { type: MuteTypes.Timeout })
|
||||||
|
.andWhere("(expires_at IS NULL OR timeout_expires_at < expires_at)")
|
||||||
|
.andWhere("timeout_expires_at <= :date", { date: thresholdDateStr })
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
async clearOldExpiredMutes(): Promise<void> {
|
async clearOldExpiredMutes(): Promise<void> {
|
||||||
const thresholdDateStr = moment.utc().subtract(OLD_EXPIRED_MUTE_THRESHOLD, "ms").format(DBDateFormat);
|
const thresholdDateStr = moment.utc().subtract(OLD_EXPIRED_MUTE_THRESHOLD, "ms").format(DBDateFormat);
|
||||||
await this.mutes
|
await this.mutes
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
|
||||||
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry";
|
|
||||||
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman";
|
|
||||||
import fetch, { Headers } from "node-fetch";
|
|
||||||
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
|
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
|
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { env } from "../env";
|
import { env } from "../env";
|
||||||
|
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
|
||||||
|
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry";
|
||||||
|
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
|
||||||
|
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman";
|
||||||
|
|
||||||
const API_URL = "https://api.phisherman.gg";
|
const API_URL = "https://api.phisherman.gg";
|
||||||
const MASTER_API_KEY = env.PHISHERMAN_API_KEY;
|
const MASTER_API_KEY = env.PHISHERMAN_API_KEY;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
|
||||||
import { Reminder } from "./entities/Reminder";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { DBDateFormat } from "../utils";
|
import { DBDateFormat } from "../utils";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { Reminder } from "./entities/Reminder";
|
||||||
|
|
||||||
export class Reminders extends BaseRepository {
|
export class Reminders extends BaseRepository {
|
||||||
private reminders: Repository<Reminder>;
|
private reminders: Repository<Reminder>;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
|
||||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { DBDateFormat } from "../utils";
|
import { DBDateFormat } from "../utils";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||||
|
|
||||||
export class ScheduledPosts extends BaseRepository {
|
export class ScheduledPosts extends BaseRepository {
|
||||||
private scheduledPosts: Repository<ScheduledPost>;
|
private scheduledPosts: Repository<ScheduledPost>;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { Tempban } from "./entities/Tempban";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import { DBDateFormat } from "../utils";
|
import { DBDateFormat } from "../utils";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { Tempban } from "./entities/Tempban";
|
||||||
|
|
||||||
export class Tempbans extends BaseRepository {
|
export class Tempbans extends BaseRepository {
|
||||||
private tempbans: Repository<Tempban>;
|
private tempbans: Repository<Tempban>;
|
||||||
|
|
|
@ -5,15 +5,16 @@ import { BaseRepository } from "./BaseRepository";
|
||||||
import { cleanupUsernames } from "./cleanup/usernames";
|
import { cleanupUsernames } from "./cleanup/usernames";
|
||||||
import { UsernameHistoryEntry } from "./entities/UsernameHistoryEntry";
|
import { UsernameHistoryEntry } from "./entities/UsernameHistoryEntry";
|
||||||
|
|
||||||
|
const CLEANUP_INTERVAL = 5 * MINUTES;
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
await cleanupUsernames();
|
||||||
|
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAPI()) {
|
if (!isAPI()) {
|
||||||
const CLEANUP_INTERVAL = 5 * MINUTES;
|
|
||||||
|
|
||||||
async function cleanup() {
|
|
||||||
await cleanupUsernames();
|
|
||||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start first cleanup 30 seconds after startup
|
// Start first cleanup 30 seconds after startup
|
||||||
|
// TODO: Move to bot startup code
|
||||||
setTimeout(cleanup, 30 * SECONDS);
|
setTimeout(cleanup, 30 * SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
|
||||||
import { VCAlert } from "./entities/VCAlert";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { DBDateFormat } from "../utils";
|
import { DBDateFormat } from "../utils";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { VCAlert } from "./entities/VCAlert";
|
||||||
|
|
||||||
export class VCAlerts extends BaseRepository {
|
export class VCAlerts extends BaseRepository {
|
||||||
private allAlerts: Repository<VCAlert>;
|
private allAlerts: Repository<VCAlert>;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { getRepository, Repository } from "typeorm";
|
||||||
import { Webhook } from "./entities/Webhook";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import { decrypt, encrypt } from "../utils/crypt";
|
import { decrypt, encrypt } from "../utils/crypt";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { Webhook } from "./entities/Webhook";
|
||||||
|
|
||||||
export class Webhooks extends BaseRepository {
|
export class Webhooks extends BaseRepository {
|
||||||
repository: Repository<Webhook> = getRepository(Webhook);
|
repository: Repository<Webhook> = getRepository(Webhook);
|
||||||
|
|
|
@ -19,8 +19,6 @@ export type RemoveApiPermissionEventData = {
|
||||||
target_id: string;
|
target_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditConfigEventData = {};
|
|
||||||
|
|
||||||
export interface AuditLogEventData extends Record<AuditLogEventType, unknown> {
|
export interface AuditLogEventData extends Record<AuditLogEventType, unknown> {
|
||||||
ADD_API_PERMISSION: {
|
ADD_API_PERMISSION: {
|
||||||
type: ApiPermissionTypes;
|
type: ApiPermissionTypes;
|
||||||
|
@ -41,7 +39,7 @@ export interface AuditLogEventData extends Record<AuditLogEventType, unknown> {
|
||||||
target_id: string;
|
target_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
EDIT_CONFIG: {};
|
EDIT_CONFIG: Record<string, never>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnyAuditLogEventData = AuditLogEventData[AuditLogEventType];
|
export type AnyAuditLogEventData = AuditLogEventData[AuditLogEventType];
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export function buildEntity<T extends any>(Entity: new () => T, data: Partial<T>): T {
|
export function buildEntity<T extends object>(Entity: new () => T, data: Partial<T>): T {
|
||||||
const instance = new Entity();
|
const instance = new Entity();
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
instance[key] = value;
|
instance[key] = value;
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
import path from "path";
|
||||||
import { Connection, createConnection } from "typeorm";
|
import { Connection, createConnection } from "typeorm";
|
||||||
import { SimpleError } from "../SimpleError";
|
import { SimpleError } from "../SimpleError";
|
||||||
import { QueryLogger } from "./queryLogger";
|
|
||||||
import path from "path";
|
|
||||||
import { backendDir } from "../paths";
|
import { backendDir } from "../paths";
|
||||||
|
import { QueryLogger } from "./queryLogger";
|
||||||
|
|
||||||
const ormconfigPath = path.join(backendDir, "ormconfig.js");
|
const ormconfigPath = path.join(backendDir, "ormconfig.js");
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const connectionOptions = require(ormconfigPath);
|
const connectionOptions = require(ormconfigPath);
|
||||||
|
|
||||||
let connectionPromise: Promise<Connection>;
|
let connectionPromise: Promise<Connection>;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
|
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||||
import { ApiUserInfo } from "./ApiUserInfo";
|
|
||||||
import { AuditLogEventData, AuditLogEventType } from "../apiAuditLogTypes";
|
import { AuditLogEventData, AuditLogEventType } from "../apiAuditLogTypes";
|
||||||
|
|
||||||
@Entity("api_audit_log")
|
@Entity("api_audit_log")
|
||||||
|
|
|
@ -19,7 +19,7 @@ export class ApiLogin {
|
||||||
@Column()
|
@Column()
|
||||||
expires_at: string;
|
expires_at: string;
|
||||||
|
|
||||||
@ManyToOne((type) => ApiUserInfo, (userInfo) => userInfo.logins)
|
@ManyToOne(() => ApiUserInfo, (userInfo) => userInfo.logins)
|
||||||
@JoinColumn({ name: "user_id" })
|
@JoinColumn({ name: "user_id" })
|
||||||
userInfo: ApiUserInfo;
|
userInfo: ApiUserInfo;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
|
||||||
import { ApiUserInfo } from "./ApiUserInfo";
|
|
||||||
import { ApiPermissionTypes } from "../ApiPermissionAssignments";
|
import { ApiPermissionTypes } from "../ApiPermissionAssignments";
|
||||||
|
import { ApiUserInfo } from "./ApiUserInfo";
|
||||||
|
|
||||||
@Entity("api_permissions")
|
@Entity("api_permissions")
|
||||||
export class ApiPermissionAssignment {
|
export class ApiPermissionAssignment {
|
||||||
|
@ -22,7 +22,7 @@ export class ApiPermissionAssignment {
|
||||||
@Column({ type: String, nullable: true })
|
@Column({ type: String, nullable: true })
|
||||||
expires_at: string | null;
|
expires_at: string | null;
|
||||||
|
|
||||||
@ManyToOne((type) => ApiUserInfo, (userInfo) => userInfo.permissionAssignments)
|
@ManyToOne(() => ApiUserInfo, (userInfo) => userInfo.permissionAssignments)
|
||||||
@JoinColumn({ name: "target_id" })
|
@JoinColumn({ name: "target_id" })
|
||||||
userInfo: ApiUserInfo;
|
userInfo: ApiUserInfo;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ export class ApiUserInfo {
|
||||||
@Column()
|
@Column()
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|
||||||
@OneToMany((type) => ApiLogin, (login) => login.userInfo)
|
@OneToMany(() => ApiLogin, (login) => login.userInfo)
|
||||||
logins: ApiLogin[];
|
logins: ApiLogin[];
|
||||||
|
|
||||||
@OneToMany((type) => ApiPermissionAssignment, (p) => p.userInfo)
|
@OneToMany(() => ApiPermissionAssignment, (p) => p.userInfo)
|
||||||
permissionAssignments: ApiPermissionAssignment[];
|
permissionAssignments: ApiPermissionAssignment[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,6 @@ export class Case {
|
||||||
*/
|
*/
|
||||||
@Column({ type: String, nullable: true }) log_message_id: string | null;
|
@Column({ type: String, nullable: true }) log_message_id: string | null;
|
||||||
|
|
||||||
@OneToMany((type) => CaseNote, (note) => note.case)
|
@OneToMany(() => CaseNote, (note) => note.case)
|
||||||
notes: CaseNote[];
|
notes: CaseNote[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class CaseNote {
|
||||||
|
|
||||||
@Column() created_at: string;
|
@Column() created_at: string;
|
||||||
|
|
||||||
@ManyToOne((type) => Case, (theCase) => theCase.notes)
|
@ManyToOne(() => Case, (theCase) => theCase.notes)
|
||||||
@JoinColumn({ name: "case_id" })
|
@JoinColumn({ name: "case_id" })
|
||||||
case: Case;
|
case: Case;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export class Config {
|
||||||
@Column()
|
@Column()
|
||||||
edited_at: string;
|
edited_at: string;
|
||||||
|
|
||||||
@ManyToOne((type) => ApiUserInfo)
|
@ManyToOne(() => ApiUserInfo)
|
||||||
@JoinColumn({ name: "edited_by" })
|
@JoinColumn({ name: "edited_by" })
|
||||||
userInfo: ApiUserInfo;
|
userInfo: ApiUserInfo;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
export const TRIGGER_COMPARISON_OPS = ["=", "!=", ">", "<", ">=", "<="] as const;
|
export const TRIGGER_COMPARISON_OPS = ["=", "!=", ">", "<", ">=", "<="] as const;
|
||||||
|
|
||||||
export type TriggerComparisonOp = typeof TRIGGER_COMPARISON_OPS[number];
|
export type TriggerComparisonOp = (typeof TRIGGER_COMPARISON_OPS)[number];
|
||||||
|
|
||||||
const REVERSE_OPS: Record<TriggerComparisonOp, TriggerComparisonOp> = {
|
const REVERSE_OPS: Record<TriggerComparisonOp, TriggerComparisonOp> = {
|
||||||
"=": "!=",
|
"=": "!=",
|
||||||
|
|
20
backend/src/data/entities/MemberCacheItem.ts
Normal file
20
backend/src/data/entities/MemberCacheItem.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
|
@Entity("member_cache")
|
||||||
|
export class MemberCacheItem {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
|
||||||
|
@Column() guild_id: string;
|
||||||
|
|
||||||
|
@Column() user_id: string;
|
||||||
|
|
||||||
|
@Column() username: string;
|
||||||
|
|
||||||
|
@Column({ type: String, nullable: true }) nickname: string | null;
|
||||||
|
|
||||||
|
@Column("simple-json") roles: string[];
|
||||||
|
|
||||||
|
@Column() last_seen: string;
|
||||||
|
|
||||||
|
@Column({ type: String, nullable: true }) delete_at: string | null;
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ export class Mute {
|
||||||
@PrimaryColumn()
|
@PrimaryColumn()
|
||||||
user_id: string;
|
user_id: string;
|
||||||
|
|
||||||
|
@Column() type: number;
|
||||||
|
|
||||||
@Column() created_at: string;
|
@Column() created_at: string;
|
||||||
|
|
||||||
@Column({ type: String, nullable: true }) expires_at: string | null;
|
@Column({ type: String, nullable: true }) expires_at: string | null;
|
||||||
|
@ -17,4 +19,8 @@ export class Mute {
|
||||||
@Column() case_id: number;
|
@Column() case_id: number;
|
||||||
|
|
||||||
@Column("simple-array") roles_to_restore: string[];
|
@Column("simple-array") roles_to_restore: string[];
|
||||||
|
|
||||||
|
@Column({ type: String, nullable: true }) mute_role: string | null;
|
||||||
|
|
||||||
|
@Column({ type: String, nullable: true }) timeout_expires_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,5 +14,5 @@ export class PersistedData {
|
||||||
|
|
||||||
@Column() nickname: string;
|
@Column() nickname: string;
|
||||||
|
|
||||||
@Column() is_voice_muted: number;
|
@Column({ type: "boolean" }) is_voice_muted: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
@Entity("reminders")
|
@Entity("reminders")
|
||||||
export class Reminder {
|
export class Reminder {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Snowflake } from "discord.js";
|
import { Snowflake, StickerFormatType, StickerType } from "discord.js";
|
||||||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||||
|
|
||||||
export interface ISavedMessageAttachmentData {
|
export interface ISavedMessageAttachmentData {
|
||||||
|
@ -55,13 +55,13 @@ export interface ISavedMessageEmbedData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISavedMessageStickerData {
|
export interface ISavedMessageStickerData {
|
||||||
format: string;
|
format: StickerFormatType;
|
||||||
guildId: Snowflake | null;
|
guildId: Snowflake | null;
|
||||||
id: Snowflake;
|
id: Snowflake;
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
available: boolean | null;
|
available: boolean | null;
|
||||||
type: string | null;
|
type: StickerType | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISavedMessageData {
|
export interface ISavedMessageData {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { MessageAttachment } from "discord.js";
|
import { Attachment } from "discord.js";
|
||||||
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
import { StrictMessageContent } from "../../utils";
|
import { StrictMessageContent } from "../../utils";
|
||||||
|
|
||||||
@Entity("scheduled_posts")
|
@Entity("scheduled_posts")
|
||||||
|
@ -17,7 +17,7 @@ export class ScheduledPost {
|
||||||
|
|
||||||
@Column("simple-json") content: StrictMessageContent;
|
@Column("simple-json") content: StrictMessageContent;
|
||||||
|
|
||||||
@Column("simple-json") attachments: MessageAttachment[];
|
@Column("simple-json") attachments: Attachment[];
|
||||||
|
|
||||||
@Column({ type: String, nullable: true }) post_at: string | null;
|
@Column({ type: String, nullable: true }) post_at: string | null;
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ export class StarboardMessage {
|
||||||
@Column()
|
@Column()
|
||||||
guild_id: string;
|
guild_id: string;
|
||||||
|
|
||||||
@OneToOne((type) => SavedMessage)
|
@OneToOne(() => SavedMessage)
|
||||||
@JoinColumn({ name: "message_id" })
|
@JoinColumn({ name: "message_id" })
|
||||||
message: SavedMessage;
|
message: SavedMessage;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export class StarboardReaction {
|
||||||
@Column()
|
@Column()
|
||||||
reactor_id: string;
|
reactor_id: string;
|
||||||
|
|
||||||
@OneToOne((type) => SavedMessage)
|
@OneToOne(() => SavedMessage)
|
||||||
@JoinColumn({ name: "message_id" })
|
@JoinColumn({ name: "message_id" })
|
||||||
message: SavedMessage;
|
message: SavedMessage;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
@Entity("vc_alerts")
|
@Entity("vc_alerts")
|
||||||
export class VCAlert {
|
export class VCAlert {
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { lazyMemoize, MINUTES } from "../../utils";
|
import { lazyMemoize, MINUTES } from "../../utils";
|
||||||
import { Archives } from "../Archives";
|
import { Archives } from "../Archives";
|
||||||
import moment from "moment-timezone";
|
|
||||||
|
|
||||||
const LOOP_INTERVAL = 15 * MINUTES;
|
const LOOP_INTERVAL = 15 * MINUTES;
|
||||||
const getArchivesRepository = lazyMemoize(() => new Archives());
|
const getArchivesRepository = lazyMemoize(() => new Archives());
|
||||||
|
|
13
backend/src/data/loops/expiredMemberCacheDeletionLoop.ts
Normal file
13
backend/src/data/loops/expiredMemberCacheDeletionLoop.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// tslint:disable:no-console
|
||||||
|
|
||||||
|
import { HOURS, lazyMemoize } from "../../utils";
|
||||||
|
import { MemberCache } from "../MemberCache";
|
||||||
|
|
||||||
|
const LOOP_INTERVAL = 6 * HOURS;
|
||||||
|
const getMemberCacheRepository = lazyMemoize(() => new MemberCache());
|
||||||
|
|
||||||
|
export async function runExpiredMemberCacheDeletionLoop() {
|
||||||
|
console.log("[EXPIRED MEMBER CACHE DELETION LOOP] Deleting stale member cache entries");
|
||||||
|
await getMemberCacheRepository().deleteStaleData();
|
||||||
|
setTimeout(() => runExpiredMemberCacheDeletionLoop(), LOOP_INTERVAL);
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
// tslint:disable:no-console
|
// tslint:disable:no-console
|
||||||
|
|
||||||
import { lazyMemoize, memoize, MINUTES } from "../../utils";
|
|
||||||
import { Mutes } from "../Mutes";
|
|
||||||
import Timeout = NodeJS.Timeout;
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { lazyMemoize, MINUTES, SECONDS } from "../../utils";
|
||||||
import { Mute } from "../entities/Mute";
|
import { Mute } from "../entities/Mute";
|
||||||
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
||||||
|
import { Mutes, TIMEOUT_RENEWAL_THRESHOLD } from "../Mutes";
|
||||||
|
import Timeout = NodeJS.Timeout;
|
||||||
|
|
||||||
const LOOP_INTERVAL = 15 * MINUTES;
|
const LOOP_INTERVAL = 15 * MINUTES;
|
||||||
const MAX_TRIES_PER_SERVER = 3;
|
const MAX_TRIES_PER_SERVER = 3;
|
||||||
|
@ -16,14 +16,24 @@ function muteToKey(mute: Mute) {
|
||||||
return `${mute.guild_id}/${mute.user_id}`;
|
return `${mute.guild_id}/${mute.user_id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastExpiredMute(mute: Mute, tries = 0) {
|
async function broadcastExpiredMute(guildId: string, userId: string, tries = 0) {
|
||||||
|
const mute = await getMutesRepository().findMute(guildId, userId);
|
||||||
|
if (!mute) {
|
||||||
|
// Mute was already cleared
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mute.expires_at || moment(mute.expires_at).diff(moment()) > 10 * SECONDS) {
|
||||||
|
// Mute duration was changed and it's no longer expiring now
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[EXPIRING MUTES LOOP] Broadcasting expired mute: ${mute.guild_id}/${mute.user_id}`);
|
console.log(`[EXPIRING MUTES LOOP] Broadcasting expired mute: ${mute.guild_id}/${mute.user_id}`);
|
||||||
if (!hasGuildEventListener(mute.guild_id, "expiredMute")) {
|
if (!hasGuildEventListener(mute.guild_id, "expiredMute")) {
|
||||||
// If there are no listeners registered for the server yet, try again in a bit
|
// If there are no listeners registered for the server yet, try again in a bit
|
||||||
if (tries < MAX_TRIES_PER_SERVER) {
|
if (tries < MAX_TRIES_PER_SERVER) {
|
||||||
timeouts.set(
|
timeouts.set(
|
||||||
muteToKey(mute),
|
muteToKey(mute),
|
||||||
setTimeout(() => broadcastExpiredMute(mute, tries + 1), 1 * MINUTES),
|
setTimeout(() => broadcastExpiredMute(guildId, userId, tries + 1), 1 * MINUTES),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -31,6 +41,21 @@ function broadcastExpiredMute(mute: Mute, tries = 0) {
|
||||||
emitGuildEvent(mute.guild_id, "expiredMute", [mute]);
|
emitGuildEvent(mute.guild_id, "expiredMute", [mute]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function broadcastTimeoutMuteToRenew(mute: Mute, tries = 0) {
|
||||||
|
console.log(`[EXPIRING MUTES LOOP] Broadcasting timeout mute to renew: ${mute.guild_id}/${mute.user_id}`);
|
||||||
|
if (!hasGuildEventListener(mute.guild_id, "timeoutMuteToRenew")) {
|
||||||
|
// If there are no listeners registered for the server yet, try again in a bit
|
||||||
|
if (tries < MAX_TRIES_PER_SERVER) {
|
||||||
|
timeouts.set(
|
||||||
|
muteToKey(mute),
|
||||||
|
setTimeout(() => broadcastTimeoutMuteToRenew(mute, tries + 1), 1 * MINUTES),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emitGuildEvent(mute.guild_id, "timeoutMuteToRenew", [mute]);
|
||||||
|
}
|
||||||
|
|
||||||
export async function runExpiringMutesLoop() {
|
export async function runExpiringMutesLoop() {
|
||||||
console.log("[EXPIRING MUTES LOOP] Clearing old timeouts");
|
console.log("[EXPIRING MUTES LOOP] Clearing old timeouts");
|
||||||
for (const timeout of timeouts.values()) {
|
for (const timeout of timeouts.values()) {
|
||||||
|
@ -46,10 +71,16 @@ export async function runExpiringMutesLoop() {
|
||||||
const remaining = Math.max(0, moment.utc(mute.expires_at!).diff(moment.utc()));
|
const remaining = Math.max(0, moment.utc(mute.expires_at!).diff(moment.utc()));
|
||||||
timeouts.set(
|
timeouts.set(
|
||||||
muteToKey(mute),
|
muteToKey(mute),
|
||||||
setTimeout(() => broadcastExpiredMute(mute), remaining),
|
setTimeout(() => broadcastExpiredMute(mute.guild_id, mute.user_id), remaining),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[EXPIRING MUTES LOOP] Broadcasting timeout mutes to renew");
|
||||||
|
const timeoutMutesToRenew = await getMutesRepository().getTimeoutMutesToRenew(TIMEOUT_RENEWAL_THRESHOLD);
|
||||||
|
for (const mute of timeoutMutesToRenew) {
|
||||||
|
broadcastTimeoutMuteToRenew(mute);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("[EXPIRING MUTES LOOP] Scheduling next loop");
|
console.log("[EXPIRING MUTES LOOP] Scheduling next loop");
|
||||||
setTimeout(() => runExpiringMutesLoop(), LOOP_INTERVAL);
|
setTimeout(() => runExpiringMutesLoop(), LOOP_INTERVAL);
|
||||||
}
|
}
|
||||||
|
@ -69,7 +100,7 @@ export function registerExpiringMute(mute: Mute) {
|
||||||
|
|
||||||
timeouts.set(
|
timeouts.set(
|
||||||
muteToKey(mute),
|
muteToKey(mute),
|
||||||
setTimeout(() => broadcastExpiredMute(mute), remaining),
|
setTimeout(() => broadcastExpiredMute(mute.guild_id, mute.user_id), remaining),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
// tslint:disable:no-console
|
// tslint:disable:no-console
|
||||||
|
|
||||||
import { lazyMemoize, MINUTES } from "../../utils";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { lazyMemoize, MINUTES } from "../../utils";
|
||||||
|
import { Tempban } from "../entities/Tempban";
|
||||||
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
||||||
import { Tempbans } from "../Tempbans";
|
import { Tempbans } from "../Tempbans";
|
||||||
import { Tempban } from "../entities/Tempban";
|
|
||||||
import Timeout = NodeJS.Timeout;
|
import Timeout = NodeJS.Timeout;
|
||||||
|
|
||||||
const LOOP_INTERVAL = 15 * MINUTES;
|
const LOOP_INTERVAL = 15 * MINUTES;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
// tslint:disable:no-console
|
// tslint:disable:no-console
|
||||||
|
|
||||||
import { lazyMemoize, MINUTES } from "../../utils";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
import { lazyMemoize, MINUTES } from "../../utils";
|
||||||
import Timeout = NodeJS.Timeout;
|
|
||||||
import { VCAlerts } from "../VCAlerts";
|
|
||||||
import { VCAlert } from "../entities/VCAlert";
|
import { VCAlert } from "../entities/VCAlert";
|
||||||
|
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
||||||
|
import { VCAlerts } from "../VCAlerts";
|
||||||
|
import Timeout = NodeJS.Timeout;
|
||||||
|
|
||||||
const LOOP_INTERVAL = 15 * MINUTES;
|
const LOOP_INTERVAL = 15 * MINUTES;
|
||||||
const MAX_TRIES_PER_SERVER = 3;
|
const MAX_TRIES_PER_SERVER = 3;
|
||||||
|
|
13
backend/src/data/loops/memberCacheDeletionLoop.ts
Normal file
13
backend/src/data/loops/memberCacheDeletionLoop.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// tslint:disable:no-console
|
||||||
|
|
||||||
|
import { lazyMemoize, MINUTES } from "../../utils";
|
||||||
|
import { MemberCache } from "../MemberCache";
|
||||||
|
|
||||||
|
const LOOP_INTERVAL = 5 * MINUTES;
|
||||||
|
const getMemberCacheRepository = lazyMemoize(() => new MemberCache());
|
||||||
|
|
||||||
|
export async function runMemberCacheDeletionLoop() {
|
||||||
|
console.log("[MEMBER CACHE DELETION LOOP] Deleting entries marked to be deleted");
|
||||||
|
await getMemberCacheRepository().deleteMarkedToBeDeletedEntries();
|
||||||
|
setTimeout(() => runMemberCacheDeletionLoop(), LOOP_INTERVAL);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
// tslint:disable:no-console
|
// tslint:disable:no-console
|
||||||
|
|
||||||
import { HOURS, MINUTES } from "../../utils";
|
import { MINUTES } from "../../utils";
|
||||||
import {
|
import {
|
||||||
deleteStalePhishermanCacheEntries,
|
deleteStalePhishermanCacheEntries,
|
||||||
deleteStalePhishermanKeyCacheEntries,
|
deleteStalePhishermanKeyCacheEntries,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// tslint:disable:no-console
|
// tslint:disable:no-console
|
||||||
|
|
||||||
import { lazyMemoize, MINUTES } from "../../utils";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
import { lazyMemoize, MINUTES } from "../../utils";
|
||||||
import { Reminder } from "../entities/Reminder";
|
import { Reminder } from "../entities/Reminder";
|
||||||
|
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
||||||
import { Reminders } from "../Reminders";
|
import { Reminders } from "../Reminders";
|
||||||
import Timeout = NodeJS.Timeout;
|
import Timeout = NodeJS.Timeout;
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
// tslint:disable:no-console
|
// tslint:disable:no-console
|
||||||
|
|
||||||
import { lazyMemoize, MINUTES } from "../../utils";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { lazyMemoize, MINUTES } from "../../utils";
|
||||||
|
import { ScheduledPost } from "../entities/ScheduledPost";
|
||||||
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
||||||
import { ScheduledPosts } from "../ScheduledPosts";
|
import { ScheduledPosts } from "../ScheduledPosts";
|
||||||
import { ScheduledPost } from "../entities/ScheduledPost";
|
|
||||||
import Timeout = NodeJS.Timeout;
|
import Timeout = NodeJS.Timeout;
|
||||||
|
|
||||||
const LOOP_INTERVAL = 15 * MINUTES;
|
const LOOP_INTERVAL = 15 * MINUTES;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { AdvancedConsoleLogger } from "typeorm/logger/AdvancedConsoleLogger";
|
import { AdvancedConsoleLogger } from "typeorm/logger/AdvancedConsoleLogger";
|
||||||
import type { QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
let groupedQueryStats: Map<string, number> = new Map();
|
let groupedQueryStats: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
@ -9,7 +8,7 @@ const deleteTableRegex = /FROM `?([^\s`]+)/;
|
||||||
const insertTableRegex = /INTO `?([^\s`]+)/;
|
const insertTableRegex = /INTO `?([^\s`]+)/;
|
||||||
|
|
||||||
export class QueryLogger extends AdvancedConsoleLogger {
|
export class QueryLogger extends AdvancedConsoleLogger {
|
||||||
logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner): any {
|
logQuery(query: string): any {
|
||||||
let type: string | undefined;
|
let type: string | undefined;
|
||||||
let table: string | undefined;
|
let table: string | undefined;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import path from "path";
|
|
||||||
import fs from "fs";
|
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { rootDir } from "./paths";
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { rootDir } from "./paths";
|
||||||
|
|
||||||
const envType = z.object({
|
const envType = z.object({
|
||||||
KEY: z.string().length(32),
|
KEY: z.string().length(32),
|
||||||
|
@ -52,11 +52,11 @@ const envType = z.object({
|
||||||
DB_DATABASE: z.string().optional().default("zeppelin"),
|
DB_DATABASE: z.string().optional().default("zeppelin"),
|
||||||
});
|
});
|
||||||
|
|
||||||
let toValidate = {};
|
let toValidate = { ...process.env };
|
||||||
const envPath = path.join(rootDir, ".env");
|
const envPath = path.join(rootDir, ".env");
|
||||||
if (fs.existsSync(envPath)) {
|
if (fs.existsSync(envPath)) {
|
||||||
const buf = fs.readFileSync(envPath);
|
const buf = fs.readFileSync(envPath);
|
||||||
toValidate = dotenv.parse(buf);
|
toValidate = { ...toValidate, ...dotenv.parse(buf) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const env = envType.parse(toValidate);
|
export const env = envType.parse(toValidate);
|
||||||
|
|
|
@ -1,42 +1,52 @@
|
||||||
import { Client, Constants, Intents, Options, TextChannel, ThreadChannel } from "discord.js";
|
// KEEP THIS AS FIRST IMPORT
|
||||||
import { Knub, PluginError } from "knub";
|
// See comment in module for details
|
||||||
import { PluginLoadError } from "knub/dist/plugins/PluginLoadError";
|
import "./threadsSignalFix";
|
||||||
// Always use UTC internally
|
|
||||||
// This is also enforced for the database in data/db.ts
|
import {
|
||||||
|
Client,
|
||||||
|
Events,
|
||||||
|
GatewayIntentBits,
|
||||||
|
Options,
|
||||||
|
Partials,
|
||||||
|
RESTEvents,
|
||||||
|
TextChannel,
|
||||||
|
ThreadChannel,
|
||||||
|
} from "discord.js";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { Knub, PluginError, PluginLoadError, PluginNotLoadedError } from "knub";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { AllowedGuilds } from "./data/AllowedGuilds";
|
import { performance } from "perf_hooks";
|
||||||
import { Configs } from "./data/Configs";
|
import process from "process";
|
||||||
import { connect } from "./data/db";
|
|
||||||
import { GuildLogs } from "./data/GuildLogs";
|
|
||||||
import { LogType } from "./data/LogType";
|
|
||||||
import { DiscordJSError } from "./DiscordJSError";
|
import { DiscordJSError } from "./DiscordJSError";
|
||||||
import { logger } from "./logger";
|
|
||||||
import { baseGuildPlugins, globalPlugins, guildPlugins } from "./plugins/availablePlugins";
|
|
||||||
import { RecoverablePluginError } from "./RecoverablePluginError";
|
import { RecoverablePluginError } from "./RecoverablePluginError";
|
||||||
import { SimpleError } from "./SimpleError";
|
import { SimpleError } from "./SimpleError";
|
||||||
import { ZeppelinGlobalConfig, ZeppelinGuildConfig } from "./types";
|
import { AllowedGuilds } from "./data/AllowedGuilds";
|
||||||
import { startUptimeCounter } from "./uptime";
|
import { Configs } from "./data/Configs";
|
||||||
import { errorMessage, isDiscordAPIError, isDiscordHTTPError, MINUTES, SECONDS, sleep, successMessage } from "./utils";
|
import { GuildLogs } from "./data/GuildLogs";
|
||||||
import { loadYamlSafely } from "./utils/loadYamlSafely";
|
import { LogType } from "./data/LogType";
|
||||||
import { DecayingCounter } from "./utils/DecayingCounter";
|
import { hasPhishermanMasterAPIKey } from "./data/Phisherman";
|
||||||
import { PluginNotLoadedError } from "knub/dist/plugins/PluginNotLoadedError";
|
import { connect } from "./data/db";
|
||||||
import { logRestCall } from "./restCallStats";
|
import { runExpiredArchiveDeletionLoop } from "./data/loops/expiredArchiveDeletionLoop";
|
||||||
import { logRateLimit } from "./rateLimitStats";
|
import { runExpiredMemberCacheDeletionLoop } from "./data/loops/expiredMemberCacheDeletionLoop";
|
||||||
import { runExpiringMutesLoop } from "./data/loops/expiringMutesLoop";
|
import { runExpiringMutesLoop } from "./data/loops/expiringMutesLoop";
|
||||||
import { runUpcomingRemindersLoop } from "./data/loops/upcomingRemindersLoop";
|
|
||||||
import { runUpcomingScheduledPostsLoop } from "./data/loops/upcomingScheduledPostsLoop";
|
|
||||||
import { runExpiringTempbansLoop } from "./data/loops/expiringTempbansLoop";
|
import { runExpiringTempbansLoop } from "./data/loops/expiringTempbansLoop";
|
||||||
import { runExpiringVCAlertsLoop } from "./data/loops/expiringVCAlertsLoop";
|
import { runExpiringVCAlertsLoop } from "./data/loops/expiringVCAlertsLoop";
|
||||||
import { runExpiredArchiveDeletionLoop } from "./data/loops/expiredArchiveDeletionLoop";
|
import { runMemberCacheDeletionLoop } from "./data/loops/memberCacheDeletionLoop";
|
||||||
import { runSavedMessageCleanupLoop } from "./data/loops/savedMessageCleanupLoop";
|
|
||||||
import { performance } from "perf_hooks";
|
|
||||||
import { setProfiler } from "./profiler";
|
|
||||||
import { enableProfiling } from "./utils/easyProfiler";
|
|
||||||
import { runPhishermanCacheCleanupLoop, runPhishermanReportingLoop } from "./data/loops/phishermanLoops";
|
import { runPhishermanCacheCleanupLoop, runPhishermanReportingLoop } from "./data/loops/phishermanLoops";
|
||||||
import { hasPhishermanMasterAPIKey } from "./data/Phisherman";
|
import { runSavedMessageCleanupLoop } from "./data/loops/savedMessageCleanupLoop";
|
||||||
|
import { runUpcomingRemindersLoop } from "./data/loops/upcomingRemindersLoop";
|
||||||
|
import { runUpcomingScheduledPostsLoop } from "./data/loops/upcomingScheduledPostsLoop";
|
||||||
import { consumeQueryStats } from "./data/queryLogger";
|
import { consumeQueryStats } from "./data/queryLogger";
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { env } from "./env";
|
import { env } from "./env";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
import { baseGuildPlugins, globalPlugins, guildPlugins } from "./plugins/availablePlugins";
|
||||||
|
import { setProfiler } from "./profiler";
|
||||||
|
import { logRateLimit } from "./rateLimitStats";
|
||||||
|
import { startUptimeCounter } from "./uptime";
|
||||||
|
import { MINUTES, SECONDS, errorMessage, isDiscordAPIError, isDiscordHTTPError, sleep, successMessage } from "./utils";
|
||||||
|
import { DecayingCounter } from "./utils/DecayingCounter";
|
||||||
|
import { enableProfiling } from "./utils/easyProfiler";
|
||||||
|
import { loadYamlSafely } from "./utils/loadYamlSafely";
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
let recentPluginErrors = 0;
|
let recentPluginErrors = 0;
|
||||||
|
@ -58,8 +68,8 @@ const SAFE_TO_IGNORE_ERIS_ERROR_CODES = [
|
||||||
const SAFE_TO_IGNORE_ERIS_ERROR_MESSAGES = ["Server didn't acknowledge previous heartbeat, possible lost connection"];
|
const SAFE_TO_IGNORE_ERIS_ERROR_MESSAGES = ["Server didn't acknowledge previous heartbeat, possible lost connection"];
|
||||||
|
|
||||||
function errorHandler(err) {
|
function errorHandler(err) {
|
||||||
const guildName = err.guild?.name || "Global";
|
const guildId = err.guild?.id || err.guildId || "0";
|
||||||
const guildId = err.guild?.id || "0";
|
const guildName = err.guild?.name || (guildId && guildId !== "0" ? "Unknown" : "Global");
|
||||||
|
|
||||||
if (err instanceof RecoverablePluginError) {
|
if (err instanceof RecoverablePluginError) {
|
||||||
// Recoverable plugin errors can be, well, recovered from.
|
// Recoverable plugin errors can be, well, recovered from.
|
||||||
|
@ -162,6 +172,8 @@ for (const [i, part] of actualVersionParts.entries()) {
|
||||||
throw new SimpleError(`Unsupported Node.js version! Must be at least ${REQUIRED_NODE_VERSION}`);
|
throw new SimpleError(`Unsupported Node.js version! Must be at least ${REQUIRED_NODE_VERSION}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always use UTC internally
|
||||||
|
// This is also enforced for the database in data/db.ts
|
||||||
moment.tz.setDefault("UTC");
|
moment.tz.setDefault("UTC");
|
||||||
|
|
||||||
// Blocking check
|
// Blocking check
|
||||||
|
@ -186,19 +198,21 @@ setInterval(() => {
|
||||||
}, 5 * 60 * 1000);
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
logger.info("Connecting to database");
|
logger.info("Connecting to database");
|
||||||
connect().then(async () => {
|
connect().then(async (connection) => {
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
partials: ["USER", "CHANNEL", "GUILD_MEMBER", "MESSAGE", "REACTION"],
|
partials: [Partials.User, Partials.Channel, Partials.GuildMember, Partials.Message, Partials.Reaction],
|
||||||
|
|
||||||
makeCache: Options.cacheWithLimits({
|
makeCache: Options.cacheWithLimits({
|
||||||
...Options.defaultMakeCacheSettings,
|
...Options.DefaultMakeCacheSettings,
|
||||||
MessageManager: 1,
|
MessageManager: 1,
|
||||||
// GuildMemberManager: 15000,
|
// GuildMemberManager: 15000,
|
||||||
GuildInviteManager: 0,
|
GuildInviteManager: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
restGlobalRateLimit: 50,
|
rest: {
|
||||||
// restTimeOffset: 1000,
|
// globalRequestsPerSecond: 50,
|
||||||
|
// offset: 1000,
|
||||||
|
},
|
||||||
|
|
||||||
// Disable mentions by default
|
// Disable mentions by default
|
||||||
allowedMentions: {
|
allowedMentions: {
|
||||||
|
@ -209,33 +223,29 @@ connect().then(async () => {
|
||||||
},
|
},
|
||||||
intents: [
|
intents: [
|
||||||
// Privileged
|
// Privileged
|
||||||
Intents.FLAGS.GUILD_MEMBERS,
|
GatewayIntentBits.GuildMembers,
|
||||||
// Intents.FLAGS.GUILD_PRESENCES,
|
GatewayIntentBits.MessageContent,
|
||||||
Intents.FLAGS.GUILD_MESSAGE_TYPING,
|
// GatewayIntentBits.GuildPresences,
|
||||||
|
|
||||||
// Regular
|
// Regular
|
||||||
Intents.FLAGS.DIRECT_MESSAGES,
|
GatewayIntentBits.GuildMessageTyping,
|
||||||
Intents.FLAGS.GUILD_BANS,
|
GatewayIntentBits.DirectMessages,
|
||||||
Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS,
|
GatewayIntentBits.GuildModeration,
|
||||||
Intents.FLAGS.GUILD_INVITES,
|
GatewayIntentBits.GuildEmojisAndStickers,
|
||||||
Intents.FLAGS.GUILD_MESSAGE_REACTIONS,
|
GatewayIntentBits.GuildInvites,
|
||||||
Intents.FLAGS.GUILD_MESSAGES,
|
GatewayIntentBits.GuildMessageReactions,
|
||||||
Intents.FLAGS.GUILDS,
|
GatewayIntentBits.GuildMessages,
|
||||||
Intents.FLAGS.GUILD_VOICE_STATES,
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
// FIXME: TS doesn't see Client as a child of EventEmitter for some reason
|
// FIXME: TS doesn't see Client as a child of EventEmitter for some reason
|
||||||
(client as unknown as EventEmitter).setMaxListeners(200);
|
(client as unknown as EventEmitter).setMaxListeners(200);
|
||||||
|
|
||||||
client.on(Constants.Events.RATE_LIMIT, (data) => {
|
|
||||||
// tslint:disable-next-line:no-console
|
|
||||||
// console.log(`[DEBUG] [RATE_LIMIT] ${JSON.stringify(data)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const safe429DecayInterval = 5 * SECONDS;
|
const safe429DecayInterval = 5 * SECONDS;
|
||||||
const safe429MaxCount = 5;
|
const safe429MaxCount = 5;
|
||||||
const safe429Counter = new DecayingCounter(safe429DecayInterval);
|
const safe429Counter = new DecayingCounter(safe429DecayInterval);
|
||||||
client.on(Constants.Events.DEBUG, (errorText) => {
|
client.on(Events.Debug, (errorText) => {
|
||||||
if (!errorText.includes("429")) {
|
if (!errorText.includes("429")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -258,7 +268,7 @@ connect().then(async () => {
|
||||||
const allowedGuilds = new AllowedGuilds();
|
const allowedGuilds = new AllowedGuilds();
|
||||||
const guildConfigs = new Configs();
|
const guildConfigs = new Configs();
|
||||||
|
|
||||||
const bot = new Knub<ZeppelinGuildConfig, ZeppelinGlobalConfig>(client, {
|
const bot = new Knub(client, {
|
||||||
guildPlugins,
|
guildPlugins,
|
||||||
globalPlugins,
|
globalPlugins,
|
||||||
|
|
||||||
|
@ -283,7 +293,7 @@ connect().then(async () => {
|
||||||
|
|
||||||
return Array.from(plugins.keys()).filter((pluginName) => {
|
return Array.from(plugins.keys()).filter((pluginName) => {
|
||||||
if (basePluginNames.includes(pluginName)) return true;
|
if (basePluginNames.includes(pluginName)) return true;
|
||||||
return configuredPlugins[pluginName] && configuredPlugins[pluginName].enabled !== false;
|
return configuredPlugins[pluginName] && (configuredPlugins[pluginName] as any).enabled !== false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -299,7 +309,11 @@ connect().then(async () => {
|
||||||
const row = await guildConfigs.getActiveByKey(key);
|
const row = await guildConfigs.getActiveByKey(key);
|
||||||
if (row) {
|
if (row) {
|
||||||
try {
|
try {
|
||||||
return loadYamlSafely(row.config);
|
const loaded = loadYamlSafely(row.config);
|
||||||
|
// Remove deprecated properties some may still have in their config
|
||||||
|
delete loaded.success_emoji;
|
||||||
|
delete loaded.error_emoji;
|
||||||
|
return loaded;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Error while loading config "${key}": ${err.message}`);
|
logger.error(`Error while loading config "${key}": ${err.message}`);
|
||||||
return {};
|
return {};
|
||||||
|
@ -329,6 +343,7 @@ connect().then(async () => {
|
||||||
sendSuccessMessageFn(channel, body) {
|
sendSuccessMessageFn(channel, body) {
|
||||||
const guildId =
|
const guildId =
|
||||||
channel instanceof TextChannel || channel instanceof ThreadChannel ? channel.guild.id : undefined;
|
channel instanceof TextChannel || channel instanceof ThreadChannel ? channel.guild.id : undefined;
|
||||||
|
// @ts-expect-error
|
||||||
const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.success_emoji : undefined;
|
const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.success_emoji : undefined;
|
||||||
channel.send(successMessage(body, emoji));
|
channel.send(successMessage(body, emoji));
|
||||||
},
|
},
|
||||||
|
@ -336,6 +351,7 @@ connect().then(async () => {
|
||||||
sendErrorMessageFn(channel, body) {
|
sendErrorMessageFn(channel, body) {
|
||||||
const guildId =
|
const guildId =
|
||||||
channel instanceof TextChannel || channel instanceof ThreadChannel ? channel.guild.id : undefined;
|
channel instanceof TextChannel || channel instanceof ThreadChannel ? channel.guild.id : undefined;
|
||||||
|
// @ts-expect-error
|
||||||
const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.error_emoji : undefined;
|
const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.error_emoji : undefined;
|
||||||
channel.send(errorMessage(body, emoji));
|
channel.send(errorMessage(body, emoji));
|
||||||
},
|
},
|
||||||
|
@ -346,11 +362,16 @@ connect().then(async () => {
|
||||||
startUptimeCounter();
|
startUptimeCounter();
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on(Constants.Events.RATE_LIMIT, (data) => {
|
client.rest.on(RESTEvents.RateLimited, (data) => {
|
||||||
logRateLimit(data);
|
logRateLimit(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.on("loadingFinished", async () => {
|
bot.on("loadingFinished", async () => {
|
||||||
|
setProfiler(bot.profiler);
|
||||||
|
if (process.env.PROFILING === "true") {
|
||||||
|
enableProfiling();
|
||||||
|
}
|
||||||
|
|
||||||
runExpiringMutesLoop();
|
runExpiringMutesLoop();
|
||||||
await sleep(10 * SECONDS);
|
await sleep(10 * SECONDS);
|
||||||
runExpiringTempbansLoop();
|
runExpiringTempbansLoop();
|
||||||
|
@ -364,6 +385,10 @@ connect().then(async () => {
|
||||||
runExpiredArchiveDeletionLoop();
|
runExpiredArchiveDeletionLoop();
|
||||||
await sleep(10 * SECONDS);
|
await sleep(10 * SECONDS);
|
||||||
runSavedMessageCleanupLoop();
|
runSavedMessageCleanupLoop();
|
||||||
|
await sleep(10 * SECONDS);
|
||||||
|
runExpiredMemberCacheDeletionLoop();
|
||||||
|
await sleep(10 * SECONDS);
|
||||||
|
runMemberCacheDeletionLoop();
|
||||||
|
|
||||||
if (hasPhishermanMasterAPIKey()) {
|
if (hasPhishermanMasterAPIKey()) {
|
||||||
await sleep(10 * SECONDS);
|
await sleep(10 * SECONDS);
|
||||||
|
@ -373,11 +398,6 @@ connect().then(async () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setProfiler(bot.profiler);
|
|
||||||
if (process.env.PROFILING === "true") {
|
|
||||||
enableProfiling();
|
|
||||||
}
|
|
||||||
|
|
||||||
let lowestGlobalRemaining = Infinity;
|
let lowestGlobalRemaining = Infinity;
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
lowestGlobalRemaining = Math.min(lowestGlobalRemaining, (client as any).rest.globalRemaining);
|
lowestGlobalRemaining = Math.min(lowestGlobalRemaining, (client as any).rest.globalRemaining);
|
||||||
|
@ -408,4 +428,28 @@ connect().then(async () => {
|
||||||
logger.info("Bot Initialized");
|
logger.info("Bot Initialized");
|
||||||
logger.info("Logging in...");
|
logger.info("Logging in...");
|
||||||
await client.login(env.BOT_TOKEN);
|
await client.login(env.BOT_TOKEN);
|
||||||
|
|
||||||
|
let stopping = false;
|
||||||
|
const cleanupAndStop = async (code) => {
|
||||||
|
if (stopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopping = true;
|
||||||
|
logger.info("Cleaning up before exit...");
|
||||||
|
// Force exit after 10sec
|
||||||
|
setTimeout(() => process.exit(code), 10 * SECONDS);
|
||||||
|
await bot.stop();
|
||||||
|
await connection.close();
|
||||||
|
logger.info("Done! Exiting now.");
|
||||||
|
process.exit(code);
|
||||||
|
};
|
||||||
|
process.on("beforeExit", () => cleanupAndStop(0));
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
logger.info("Received SIGINT, exiting...");
|
||||||
|
cleanupAndStop(0);
|
||||||
|
});
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
logger.info("Received SIGTERM, exiting...");
|
||||||
|
cleanupAndStop(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -106,7 +106,7 @@ export class CreatePreTypeORMTables1540519249973 implements MigrationInterface {
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
public async down(): Promise<any> {
|
||||||
// No down function since we're migrating (hehe) from another migration system (knex)
|
// No down function since we're migrating (hehe) from another migration system (knex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,6 @@ export class MigrateUsernamesToNewHistoryTable1556909512501 implements Migration
|
||||||
await queryRunner.query("START TRANSACTION");
|
await queryRunner.query("START TRANSACTION");
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line:no-empty
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
public async down(queryRunner: QueryRunner): Promise<any> {}
|
public async down(): Promise<any> {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ export class AddTypeAndPermissionsToApiPermissions1573158035867 implements Migra
|
||||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
try {
|
try {
|
||||||
await queryRunner.dropPrimaryKey("api_permissions");
|
await queryRunner.dropPrimaryKey("api_permissions");
|
||||||
} catch {} // tslint:disable-line
|
} catch {} // eslint-disable-line no-empty
|
||||||
|
|
||||||
const table = (await queryRunner.getTable("api_permissions"))!;
|
const table = (await queryRunner.getTable("api_permissions"))!;
|
||||||
if (table.indices.length) {
|
if (table.indices.length) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm";
|
||||||
|
|
||||||
export class CreateTempBansTable1608753440716 implements MigrationInterface {
|
export class CreateTempBansTable1608753440716 implements MigrationInterface {
|
||||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
const table = await queryRunner.createTable(
|
await queryRunner.createTable(
|
||||||
new Table({
|
new Table({
|
||||||
name: "tempbans",
|
name: "tempbans",
|
||||||
columns: [
|
columns: [
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from "typeorm";
|
||||||
|
|
||||||
|
export class AddTimeoutColumnsToMutes1680354053183 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.addColumns("mutes", [
|
||||||
|
new TableColumn({
|
||||||
|
name: "type",
|
||||||
|
type: "tinyint",
|
||||||
|
unsigned: true,
|
||||||
|
default: 1, // The value for "Role" mute at the time of this migration
|
||||||
|
}),
|
||||||
|
new TableColumn({
|
||||||
|
name: "mute_role",
|
||||||
|
type: "bigint",
|
||||||
|
unsigned: true,
|
||||||
|
isNullable: true,
|
||||||
|
default: null,
|
||||||
|
}),
|
||||||
|
new TableColumn({
|
||||||
|
name: "timeout_expires_at",
|
||||||
|
type: "datetime",
|
||||||
|
isNullable: true,
|
||||||
|
default: null,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
"mutes",
|
||||||
|
new TableIndex({
|
||||||
|
columnNames: ["type"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
"mutes",
|
||||||
|
new TableIndex({
|
||||||
|
columnNames: ["timeout_expires_at"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn("mutes", "type");
|
||||||
|
await queryRunner.dropColumn("mutes", "mute_role");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { MigrationInterface, QueryRunner, Table } from "typeorm";
|
||||||
|
|
||||||
|
export class CreateMemberCacheTable1682788165866 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: "member_cache",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "int",
|
||||||
|
isPrimary: true,
|
||||||
|
isGenerated: true,
|
||||||
|
generationStrategy: "increment",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "guild_id",
|
||||||
|
type: "bigint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user_id",
|
||||||
|
type: "bigint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username",
|
||||||
|
type: "varchar",
|
||||||
|
length: "255",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nickname",
|
||||||
|
type: "varchar",
|
||||||
|
length: "255",
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "roles",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last_seen",
|
||||||
|
type: "date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete_at",
|
||||||
|
type: "datetime",
|
||||||
|
isNullable: true,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indices: [
|
||||||
|
{
|
||||||
|
columnNames: ["guild_id", "user_id"],
|
||||||
|
isUnique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnNames: ["last_seen"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnNames: ["delete_at"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable("member_cache");
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,19 +2,30 @@
|
||||||
* @file Utility functions that are plugin-instance-specific (i.e. use PluginData)
|
* @file Utility functions that are plugin-instance-specific (i.e. use PluginData)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { GuildMember, Message, MessageMentionOptions, MessageOptions, TextChannel } from "discord.js";
|
import {
|
||||||
|
GuildMember,
|
||||||
|
Message,
|
||||||
|
MessageCreateOptions,
|
||||||
|
MessageMentionOptions,
|
||||||
|
PermissionsBitField,
|
||||||
|
TextBasedChannel,
|
||||||
|
} from "discord.js";
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import { CommandContext, configUtils, ConfigValidationError, GuildPluginData, helpers, PluginOptions } from "knub";
|
import {
|
||||||
import { PluginOverrideCriteria } from "knub/dist/config/configTypes";
|
AnyPluginData,
|
||||||
import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager"; // TODO: Export from Knub index
|
CommandContext,
|
||||||
import { AnyPluginData } from "knub/dist/plugins/PluginData";
|
ConfigValidationError,
|
||||||
|
ExtendedMatchParams,
|
||||||
|
GuildPluginData,
|
||||||
|
PluginOverrideCriteria,
|
||||||
|
helpers,
|
||||||
|
} from "knub";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
|
||||||
import { TZeppelinKnub } from "./types";
|
|
||||||
import { deepKeyIntersect, errorMessage, successMessage, tDeepPartial, tNullable } from "./utils";
|
|
||||||
import { Tail } from "./utils/typeUtils";
|
|
||||||
import { decodeAndValidateStrict, StrictValidationError, validate } from "./validatorUtils";
|
|
||||||
import { isStaff } from "./staff";
|
import { isStaff } from "./staff";
|
||||||
|
import { TZeppelinKnub } from "./types";
|
||||||
|
import { errorMessage, successMessage, tNullable } from "./utils";
|
||||||
|
import { Tail } from "./utils/typeUtils";
|
||||||
|
import { StrictValidationError, parseIoTsSchema } from "./validatorUtils";
|
||||||
|
|
||||||
const { getMemberLevel } = helpers;
|
const { getMemberLevel } = helpers;
|
||||||
|
|
||||||
|
@ -23,10 +34,16 @@ export function canActOn(
|
||||||
member1: GuildMember,
|
member1: GuildMember,
|
||||||
member2: GuildMember,
|
member2: GuildMember,
|
||||||
allowSameLevel = false,
|
allowSameLevel = false,
|
||||||
|
allowAdmins = false,
|
||||||
) {
|
) {
|
||||||
if (member2.id === pluginData.client.user!.id) {
|
if (member2.id === pluginData.client.user!.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const isOwnerOrAdmin =
|
||||||
|
member2.id === member2.guild.ownerId || member2.permissions.has(PermissionsBitField.Flags.Administrator);
|
||||||
|
if (isOwnerOrAdmin && !allowAdmins) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const ourLevel = getMemberLevel(pluginData, member1);
|
const ourLevel = getMemberLevel(pluginData, member1);
|
||||||
const memberLevel = getMemberLevel(pluginData, member2);
|
const memberLevel = getMemberLevel(pluginData, member2);
|
||||||
|
@ -60,28 +77,6 @@ const PluginOverrideCriteriaType: t.Type<PluginOverrideCriteria<unknown>> = t.re
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const validTopLevelOverrideKeys = [
|
|
||||||
"channel",
|
|
||||||
"category",
|
|
||||||
"thread",
|
|
||||||
"is_thread",
|
|
||||||
"level",
|
|
||||||
"user",
|
|
||||||
"role",
|
|
||||||
"all",
|
|
||||||
"any",
|
|
||||||
"not",
|
|
||||||
"extra",
|
|
||||||
"config",
|
|
||||||
];
|
|
||||||
|
|
||||||
const BasicPluginStructureType = t.type({
|
|
||||||
enabled: tNullable(t.boolean),
|
|
||||||
config: tNullable(t.unknown),
|
|
||||||
overrides: tNullable(t.array(t.union([PluginOverrideCriteriaType, t.type({ config: t.unknown })]))),
|
|
||||||
replaceDefaultOverrides: tNullable(t.boolean),
|
|
||||||
});
|
|
||||||
|
|
||||||
export function strictValidationErrorToConfigValidationError(err: StrictValidationError) {
|
export function strictValidationErrorToConfigValidationError(err: StrictValidationError) {
|
||||||
return new ConfigValidationError(
|
return new ConfigValidationError(
|
||||||
err
|
err
|
||||||
|
@ -91,117 +86,35 @@ export function strictValidationErrorToConfigValidationError(err: StrictValidati
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPluginConfigPreprocessor(
|
export function makeIoTsConfigParser<Schema extends t.Type<any>>(schema: Schema): (input: unknown) => t.TypeOf<Schema> {
|
||||||
blueprint: ZeppelinPlugin,
|
return (input: unknown) => {
|
||||||
customPreprocessor?: ZeppelinPlugin["configPreprocessor"],
|
try {
|
||||||
) {
|
return parseIoTsSchema(schema, input);
|
||||||
return async (options: PluginOptions<any>, strict?: boolean) => {
|
} catch (err) {
|
||||||
// 1. Validate the basic structure of plugin config
|
if (err instanceof StrictValidationError) {
|
||||||
const basicOptionsValidation = validate(BasicPluginStructureType, options);
|
throw strictValidationErrorToConfigValidationError(err);
|
||||||
if (basicOptionsValidation instanceof StrictValidationError) {
|
|
||||||
throw strictValidationErrorToConfigValidationError(basicOptionsValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Validate config/overrides against *partial* config schema. This ensures valid properties have valid types.
|
|
||||||
const partialConfigSchema = tDeepPartial(blueprint.configSchema);
|
|
||||||
|
|
||||||
if (options.config) {
|
|
||||||
const partialConfigValidation = validate(partialConfigSchema, options.config);
|
|
||||||
if (partialConfigValidation instanceof StrictValidationError) {
|
|
||||||
throw strictValidationErrorToConfigValidationError(partialConfigValidation);
|
|
||||||
}
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.overrides) {
|
|
||||||
for (const override of options.overrides) {
|
|
||||||
// Validate criteria and extra criteria
|
|
||||||
// FIXME: This is ugly
|
|
||||||
for (const key of Object.keys(override)) {
|
|
||||||
if (!validTopLevelOverrideKeys.includes(key)) {
|
|
||||||
if (strict) {
|
|
||||||
throw new ConfigValidationError(`Unknown override criterion '${key}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete override[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (override.extra != null) {
|
|
||||||
for (const extraCriterion of Object.keys(override.extra)) {
|
|
||||||
if (!blueprint.customOverrideCriteriaFunctions?.[extraCriterion]) {
|
|
||||||
if (strict) {
|
|
||||||
throw new ConfigValidationError(`Unknown override extra criterion '${extraCriterion}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete override.extra[extraCriterion];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate override config
|
|
||||||
const partialOverrideConfigValidation = decodeAndValidateStrict(partialConfigSchema, override.config || {});
|
|
||||||
if (partialOverrideConfigValidation instanceof StrictValidationError) {
|
|
||||||
throw strictValidationErrorToConfigValidationError(partialOverrideConfigValidation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Run custom preprocessor, if any
|
|
||||||
if (customPreprocessor) {
|
|
||||||
options = await customPreprocessor(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Merge with default options and validate/decode the entire config
|
|
||||||
let decodedConfig = {};
|
|
||||||
const decodedOverrides: Array<PluginOverrideCriteria<unknown> & { config: any }> = [];
|
|
||||||
|
|
||||||
if (options.config) {
|
|
||||||
decodedConfig = blueprint.configSchema
|
|
||||||
? decodeAndValidateStrict(blueprint.configSchema, options.config)
|
|
||||||
: options.config;
|
|
||||||
if (decodedConfig instanceof StrictValidationError) {
|
|
||||||
throw strictValidationErrorToConfigValidationError(decodedConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.overrides) {
|
|
||||||
for (const override of options.overrides) {
|
|
||||||
const overrideConfigMergedWithBaseConfig = configUtils.mergeConfig(options.config || {}, override.config || {});
|
|
||||||
const decodedOverrideConfig = blueprint.configSchema
|
|
||||||
? decodeAndValidateStrict(blueprint.configSchema, overrideConfigMergedWithBaseConfig)
|
|
||||||
: overrideConfigMergedWithBaseConfig;
|
|
||||||
if (decodedOverrideConfig instanceof StrictValidationError) {
|
|
||||||
throw strictValidationErrorToConfigValidationError(decodedOverrideConfig);
|
|
||||||
}
|
|
||||||
decodedOverrides.push({
|
|
||||||
...override,
|
|
||||||
config: deepKeyIntersect(decodedOverrideConfig, override.config || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
config: decodedConfig,
|
|
||||||
overrides: decodedOverrides,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendSuccessMessage(
|
export async function sendSuccessMessage(
|
||||||
pluginData: AnyPluginData<any>,
|
pluginData: AnyPluginData<any>,
|
||||||
channel: TextChannel,
|
channel: TextBasedChannel,
|
||||||
body: string,
|
body: string,
|
||||||
allowedMentions?: MessageMentionOptions,
|
allowedMentions?: MessageMentionOptions,
|
||||||
): Promise<Message | undefined> {
|
): Promise<Message | undefined> {
|
||||||
const emoji = pluginData.fullConfig.success_emoji || undefined;
|
const emoji = pluginData.fullConfig.success_emoji || undefined;
|
||||||
const formattedBody = successMessage(body, emoji);
|
const formattedBody = successMessage(body, emoji);
|
||||||
const content: MessageOptions = allowedMentions
|
const content: MessageCreateOptions = allowedMentions
|
||||||
? { content: formattedBody, allowedMentions }
|
? { content: formattedBody, allowedMentions }
|
||||||
: { content: formattedBody };
|
: { content: formattedBody };
|
||||||
|
|
||||||
return channel
|
return channel
|
||||||
.send({ ...content }) // Force line break
|
.send({ ...content }) // Force line break
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
const channelInfo = channel.guild ? `${channel.id} (${channel.guild.id})` : channel.id;
|
const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id;
|
||||||
logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`);
|
logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`);
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
@ -209,20 +122,20 @@ export async function sendSuccessMessage(
|
||||||
|
|
||||||
export async function sendErrorMessage(
|
export async function sendErrorMessage(
|
||||||
pluginData: AnyPluginData<any>,
|
pluginData: AnyPluginData<any>,
|
||||||
channel: TextChannel,
|
channel: TextBasedChannel,
|
||||||
body: string,
|
body: string,
|
||||||
allowedMentions?: MessageMentionOptions,
|
allowedMentions?: MessageMentionOptions,
|
||||||
): Promise<Message | undefined> {
|
): Promise<Message | undefined> {
|
||||||
const emoji = pluginData.fullConfig.error_emoji || undefined;
|
const emoji = pluginData.fullConfig.error_emoji || undefined;
|
||||||
const formattedBody = errorMessage(body, emoji);
|
const formattedBody = errorMessage(body, emoji);
|
||||||
const content: MessageOptions = allowedMentions
|
const content: MessageCreateOptions = allowedMentions
|
||||||
? { content: formattedBody, allowedMentions }
|
? { content: formattedBody, allowedMentions }
|
||||||
: { content: formattedBody };
|
: { content: formattedBody };
|
||||||
|
|
||||||
return channel
|
return channel
|
||||||
.send({ ...content }) // Force line break
|
.send({ ...content }) // Force line break
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
const channelInfo = channel.guild ? `${channel.id} (${channel.guild.id})` : channel.id;
|
const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id;
|
||||||
logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`);
|
logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`);
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
@ -230,11 +143,13 @@ export async function sendErrorMessage(
|
||||||
|
|
||||||
export function getBaseUrl(pluginData: AnyPluginData<any>) {
|
export function getBaseUrl(pluginData: AnyPluginData<any>) {
|
||||||
const knub = pluginData.getKnubInstance() as TZeppelinKnub;
|
const knub = pluginData.getKnubInstance() as TZeppelinKnub;
|
||||||
|
// @ts-expect-error
|
||||||
return knub.getGlobalConfig().url;
|
return knub.getGlobalConfig().url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isOwner(pluginData: AnyPluginData<any>, userId: string) {
|
export function isOwner(pluginData: AnyPluginData<any>, userId: string) {
|
||||||
const knub = pluginData.getKnubInstance() as TZeppelinKnub;
|
const knub = pluginData.getKnubInstance() as TZeppelinKnub;
|
||||||
|
// @ts-expect-error
|
||||||
const owners = knub.getGlobalConfig()?.owners;
|
const owners = knub.getGlobalConfig()?.owners;
|
||||||
if (!owners) {
|
if (!owners) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { PluginOptions } from "knub";
|
import { PluginOptions } from "knub";
|
||||||
import { GuildLogs } from "../../data/GuildLogs";
|
import { GuildLogs } from "../../data/GuildLogs";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||||
|
import { makeIoTsConfigParser } from "../../pluginUtils";
|
||||||
import { LogsPlugin } from "../Logs/LogsPlugin";
|
import { LogsPlugin } from "../Logs/LogsPlugin";
|
||||||
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
|
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
|
||||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||||
|
@ -23,10 +24,11 @@ export const AutoDeletePlugin = zeppelinGuildPlugin<AutoDeletePluginType>()({
|
||||||
prettyName: "Auto-delete",
|
prettyName: "Auto-delete",
|
||||||
description: "Allows Zeppelin to auto-delete messages from a channel after a delay",
|
description: "Allows Zeppelin to auto-delete messages from a channel after a delay",
|
||||||
configurationGuide: "Maximum deletion delay is currently 5 minutes",
|
configurationGuide: "Maximum deletion delay is currently 5 minutes",
|
||||||
|
configSchema: ConfigSchema,
|
||||||
},
|
},
|
||||||
|
|
||||||
dependencies: () => [TimeAndDatePlugin, LogsPlugin],
|
dependencies: () => [TimeAndDatePlugin, LogsPlugin],
|
||||||
configSchema: ConfigSchema,
|
configParser: makeIoTsConfigParser(ConfigSchema),
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
|
|
||||||
beforeLoad(pluginData) {
|
beforeLoad(pluginData) {
|
||||||
|
@ -43,7 +45,7 @@ export const AutoDeletePlugin = zeppelinGuildPlugin<AutoDeletePluginType>()({
|
||||||
},
|
},
|
||||||
|
|
||||||
afterLoad(pluginData) {
|
afterLoad(pluginData) {
|
||||||
const { state, guild } = pluginData;
|
const { state } = pluginData;
|
||||||
|
|
||||||
state.onMessageCreateFn = (msg) => onMessageCreate(pluginData, msg);
|
state.onMessageCreateFn = (msg) => onMessageCreate(pluginData, msg);
|
||||||
state.guildSavedMessages.events.on("create", state.onMessageCreateFn);
|
state.guildSavedMessages.events.on("create", state.onMessageCreateFn);
|
||||||
|
@ -56,8 +58,10 @@ export const AutoDeletePlugin = zeppelinGuildPlugin<AutoDeletePluginType>()({
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnload(pluginData) {
|
beforeUnload(pluginData) {
|
||||||
pluginData.state.guildSavedMessages.events.off("create", pluginData.state.onMessageCreateFn);
|
const { state } = pluginData;
|
||||||
pluginData.state.guildSavedMessages.events.off("delete", pluginData.state.onMessageDeleteFn);
|
|
||||||
pluginData.state.guildSavedMessages.events.off("deleteBulk", pluginData.state.onMessageDeleteBulkFn);
|
state.guildSavedMessages.events.off("create", state.onMessageCreateFn);
|
||||||
|
state.guildSavedMessages.events.off("delete", state.onMessageDeleteFn);
|
||||||
|
state.guildSavedMessages.events.off("deleteBulk", state.onMessageDeleteBulkFn);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import { BasePluginType } from "knub";
|
import { BasePluginType } from "knub";
|
||||||
import { SavedMessage } from "../../data/entities/SavedMessage";
|
|
||||||
import { GuildLogs } from "../../data/GuildLogs";
|
import { GuildLogs } from "../../data/GuildLogs";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||||
|
import { SavedMessage } from "../../data/entities/SavedMessage";
|
||||||
import { MINUTES, tDelayString } from "../../utils";
|
import { MINUTES, tDelayString } from "../../utils";
|
||||||
import Timeout = NodeJS.Timeout;
|
import Timeout = NodeJS.Timeout;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Permissions, Snowflake, TextChannel } from "discord.js";
|
import { ChannelType, PermissionsBitField, Snowflake } from "discord.js";
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { channelToTemplateSafeChannel, userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
|
|
||||||
import { LogType } from "../../../data/LogType";
|
import { LogType } from "../../../data/LogType";
|
||||||
import { logger } from "../../../logger";
|
import { logger } from "../../../logger";
|
||||||
import { resolveUser, verboseChannelMention } from "../../../utils";
|
import { resolveUser, verboseChannelMention } from "../../../utils";
|
||||||
|
@ -17,8 +16,8 @@ export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePlugi
|
||||||
|
|
||||||
scheduleNextDeletion(pluginData);
|
scheduleNextDeletion(pluginData);
|
||||||
|
|
||||||
const channel = pluginData.guild.channels.cache.get(itemToDelete.message.channel_id as Snowflake) as TextChannel;
|
const channel = pluginData.guild.channels.cache.get(itemToDelete.message.channel_id as Snowflake);
|
||||||
if (!channel) {
|
if (!channel || channel.type === ChannelType.GuildCategory) {
|
||||||
// Channel was deleted, ignore
|
// Channel was deleted, ignore
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +25,9 @@ export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePlugi
|
||||||
const logs = pluginData.getPlugin(LogsPlugin);
|
const logs = pluginData.getPlugin(LogsPlugin);
|
||||||
const perms = channel.permissionsFor(pluginData.client.user!.id);
|
const perms = channel.permissionsFor(pluginData.client.user!.id);
|
||||||
|
|
||||||
if (!hasDiscordPermissions(perms, Permissions.FLAGS.VIEW_CHANNEL | Permissions.FLAGS.READ_MESSAGE_HISTORY)) {
|
if (
|
||||||
|
!hasDiscordPermissions(perms, PermissionsBitField.Flags.ViewChannel | PermissionsBitField.Flags.ReadMessageHistory)
|
||||||
|
) {
|
||||||
logs.logBotAlert({
|
logs.logBotAlert({
|
||||||
body: `Missing permissions to read messages or message history in auto-delete channel ${verboseChannelMention(
|
body: `Missing permissions to read messages or message history in auto-delete channel ${verboseChannelMention(
|
||||||
channel,
|
channel,
|
||||||
|
@ -35,7 +36,7 @@ export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePlugi
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasDiscordPermissions(perms, Permissions.FLAGS.MANAGE_MESSAGES)) {
|
if (!hasDiscordPermissions(perms, PermissionsBitField.Flags.ManageMessages)) {
|
||||||
logs.logBotAlert({
|
logs.logBotAlert({
|
||||||
body: `Missing permissions to delete messages in auto-delete channel ${verboseChannelMention(channel)}`,
|
body: `Missing permissions to delete messages in auto-delete channel ${verboseChannelMention(channel)}`,
|
||||||
});
|
});
|
||||||
|
@ -45,7 +46,7 @@ export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePlugi
|
||||||
const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
|
const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
|
||||||
|
|
||||||
pluginData.state.guildLogs.ignoreLog(LogType.MESSAGE_DELETE, itemToDelete.message.id);
|
pluginData.state.guildLogs.ignoreLog(LogType.MESSAGE_DELETE, itemToDelete.message.id);
|
||||||
(channel as TextChannel).messages.delete(itemToDelete.message.id as Snowflake).catch((err) => {
|
channel.messages.delete(itemToDelete.message.id as Snowflake).catch((err) => {
|
||||||
if (err.code === 10008) {
|
if (err.code === 10008) {
|
||||||
// "Unknown Message", probably already deleted by automod or another bot, ignore
|
// "Unknown Message", probably already deleted by automod or another bot, ignore
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
import { SavedMessage } from "../../../data/entities/SavedMessage";
|
import { SavedMessage } from "../../../data/entities/SavedMessage";
|
||||||
import { LogType } from "../../../data/LogType";
|
|
||||||
import { convertDelayStringToMS, resolveMember } from "../../../utils";
|
import { convertDelayStringToMS, resolveMember } from "../../../utils";
|
||||||
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
import { AutoDeletePluginType, MAX_DELAY } from "../types";
|
import { AutoDeletePluginType, MAX_DELAY } from "../types";
|
||||||
import { addMessageToDeletionQueue } from "./addMessageToDeletionQueue";
|
import { addMessageToDeletionQueue } from "./addMessageToDeletionQueue";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
|
||||||
|
|
||||||
export async function onMessageCreate(pluginData: GuildPluginData<AutoDeletePluginType>, msg: SavedMessage) {
|
export async function onMessageCreate(pluginData: GuildPluginData<AutoDeletePluginType>, msg: SavedMessage) {
|
||||||
const member = await resolveMember(pluginData.client, pluginData.guild, msg.user_id);
|
const member = await resolveMember(pluginData.client, pluginData.guild, msg.user_id);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { PluginOptions } from "knub";
|
import { PluginOptions } from "knub";
|
||||||
import { GuildAutoReactions } from "../../data/GuildAutoReactions";
|
import { GuildAutoReactions } from "../../data/GuildAutoReactions";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||||
|
import { makeIoTsConfigParser } from "../../pluginUtils";
|
||||||
import { trimPluginDescription } from "../../utils";
|
import { trimPluginDescription } from "../../utils";
|
||||||
import { LogsPlugin } from "../Logs/LogsPlugin";
|
import { LogsPlugin } from "../Logs/LogsPlugin";
|
||||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||||
|
@ -31,6 +32,7 @@ export const AutoReactionsPlugin = zeppelinGuildPlugin<AutoReactionsPluginType>(
|
||||||
description: trimPluginDescription(`
|
description: trimPluginDescription(`
|
||||||
Allows setting up automatic reactions to all new messages on a channel
|
Allows setting up automatic reactions to all new messages on a channel
|
||||||
`),
|
`),
|
||||||
|
configSchema: ConfigSchema,
|
||||||
},
|
},
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
|
@ -38,11 +40,11 @@ export const AutoReactionsPlugin = zeppelinGuildPlugin<AutoReactionsPluginType>(
|
||||||
LogsPlugin,
|
LogsPlugin,
|
||||||
],
|
],
|
||||||
|
|
||||||
configSchema: ConfigSchema,
|
configParser: makeIoTsConfigParser(ConfigSchema),
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
commands: [
|
messageCommands: [
|
||||||
NewAutoReactionsCmd,
|
NewAutoReactionsCmd,
|
||||||
DisableAutoReactionsCmd,
|
DisableAutoReactionsCmd,
|
||||||
],
|
],
|
||||||
|
@ -53,8 +55,10 @@ export const AutoReactionsPlugin = zeppelinGuildPlugin<AutoReactionsPluginType>(
|
||||||
],
|
],
|
||||||
|
|
||||||
beforeLoad(pluginData) {
|
beforeLoad(pluginData) {
|
||||||
pluginData.state.savedMessages = GuildSavedMessages.getGuildInstance(pluginData.guild.id);
|
const { state, guild } = pluginData;
|
||||||
pluginData.state.autoReactions = GuildAutoReactions.getGuildInstance(pluginData.guild.id);
|
|
||||||
pluginData.state.cache = new Map();
|
state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id);
|
||||||
|
state.autoReactions = GuildAutoReactions.getGuildInstance(guild.id);
|
||||||
|
state.cache = new Map();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { GuildChannel, Permissions } from "discord.js";
|
import { PermissionsBitField } from "discord.js";
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||||
import { canUseEmoji, customEmojiRegex, isEmoji } from "../../../utils";
|
import { canUseEmoji, customEmojiRegex, isEmoji } from "../../../utils";
|
||||||
|
@ -7,7 +7,7 @@ import { missingPermissionError } from "../../../utils/missingPermissionError";
|
||||||
import { readChannelPermissions } from "../../../utils/readChannelPermissions";
|
import { readChannelPermissions } from "../../../utils/readChannelPermissions";
|
||||||
import { autoReactionsCmd } from "../types";
|
import { autoReactionsCmd } from "../types";
|
||||||
|
|
||||||
const requiredPermissions = readChannelPermissions | Permissions.FLAGS.ADD_REACTIONS;
|
const requiredPermissions = readChannelPermissions | PermissionsBitField.Flags.AddReactions;
|
||||||
|
|
||||||
export const NewAutoReactionsCmd = autoReactionsCmd({
|
export const NewAutoReactionsCmd = autoReactionsCmd({
|
||||||
trigger: "auto_reactions",
|
trigger: "auto_reactions",
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { GuildChannel, GuildTextBasedChannel, Permissions } from "discord.js";
|
import { GuildTextBasedChannel, PermissionsBitField } from "discord.js";
|
||||||
import { LogType } from "../../../data/LogType";
|
import { AutoReaction } from "../../../data/entities/AutoReaction";
|
||||||
import { isDiscordAPIError } from "../../../utils";
|
import { isDiscordAPIError } from "../../../utils";
|
||||||
import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions";
|
import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions";
|
||||||
import { missingPermissionError } from "../../../utils/missingPermissionError";
|
import { missingPermissionError } from "../../../utils/missingPermissionError";
|
||||||
import { readChannelPermissions } from "../../../utils/readChannelPermissions";
|
import { readChannelPermissions } from "../../../utils/readChannelPermissions";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
import { autoReactionsEvt } from "../types";
|
import { autoReactionsEvt } from "../types";
|
||||||
import { AutoReaction } from "../../../data/entities/AutoReaction";
|
|
||||||
|
|
||||||
const p = Permissions.FLAGS;
|
const p = PermissionsBitField.Flags;
|
||||||
|
|
||||||
export const AddReactionsEvt = autoReactionsEvt({
|
export const AddReactionsEvt = autoReactionsEvt({
|
||||||
event: "messageCreate",
|
event: "messageCreate",
|
||||||
|
@ -40,7 +39,7 @@ export const AddReactionsEvt = autoReactionsEvt({
|
||||||
|
|
||||||
const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!;
|
const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!;
|
||||||
if (me) {
|
if (me) {
|
||||||
const missingPermissions = getMissingChannelPermissions(me, channel, readChannelPermissions | p.ADD_REACTIONS);
|
const missingPermissions = getMissingChannelPermissions(me, channel, readChannelPermissions | p.AddReactions);
|
||||||
if (missingPermissions) {
|
if (missingPermissions) {
|
||||||
const logs = pluginData.getPlugin(LogsPlugin);
|
const logs = pluginData.getPlugin(LogsPlugin);
|
||||||
logs.logBotAlert({
|
logs.logBotAlert({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub";
|
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub";
|
||||||
import { GuildAutoReactions } from "../../data/GuildAutoReactions";
|
import { GuildAutoReactions } from "../../data/GuildAutoReactions";
|
||||||
import { GuildLogs } from "../../data/GuildLogs";
|
import { GuildLogs } from "../../data/GuildLogs";
|
||||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||||
|
@ -20,5 +20,5 @@ export interface AutoReactionsPluginType extends BasePluginType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const autoReactionsCmd = typedGuildCommand<AutoReactionsPluginType>();
|
export const autoReactionsCmd = guildPluginMessageCommand<AutoReactionsPluginType>();
|
||||||
export const autoReactionsEvt = typedGuildEventListener<AutoReactionsPluginType>();
|
export const autoReactionsEvt = guildPluginEventListener<AutoReactionsPluginType>();
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { configUtils, CooldownManager } from "knub";
|
import { configUtils, CooldownManager } from "knub";
|
||||||
import { ConfigPreprocessorFn } from "knub/dist/config/configTypes";
|
|
||||||
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
|
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
|
||||||
import { GuildArchives } from "../../data/GuildArchives";
|
import { GuildArchives } from "../../data/GuildArchives";
|
||||||
import { GuildLogs } from "../../data/GuildLogs";
|
import { GuildLogs } from "../../data/GuildLogs";
|
||||||
|
@ -9,11 +8,14 @@ import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners";
|
||||||
import { MINUTES, SECONDS } from "../../utils";
|
import { MINUTES, SECONDS } from "../../utils";
|
||||||
import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap";
|
import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap";
|
||||||
import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap";
|
import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap";
|
||||||
import { StrictValidationError } from "../../validatorUtils";
|
import { parseIoTsSchema, StrictValidationError } from "../../validatorUtils";
|
||||||
import { CountersPlugin } from "../Counters/CountersPlugin";
|
import { CountersPlugin } from "../Counters/CountersPlugin";
|
||||||
|
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin";
|
||||||
import { LogsPlugin } from "../Logs/LogsPlugin";
|
import { LogsPlugin } from "../Logs/LogsPlugin";
|
||||||
import { ModActionsPlugin } from "../ModActions/ModActionsPlugin";
|
import { ModActionsPlugin } from "../ModActions/ModActionsPlugin";
|
||||||
import { MutesPlugin } from "../Mutes/MutesPlugin";
|
import { MutesPlugin } from "../Mutes/MutesPlugin";
|
||||||
|
import { PhishermanPlugin } from "../Phisherman/PhishermanPlugin";
|
||||||
|
import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin";
|
||||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||||
import { availableActions } from "./actions/availableActions";
|
import { availableActions } from "./actions/availableActions";
|
||||||
import { AntiraidClearCmd } from "./commands/AntiraidClearCmd";
|
import { AntiraidClearCmd } from "./commands/AntiraidClearCmd";
|
||||||
|
@ -35,8 +37,6 @@ import { clearOldRecentSpam } from "./functions/clearOldRecentSpam";
|
||||||
import { pluginInfo } from "./info";
|
import { pluginInfo } from "./info";
|
||||||
import { availableTriggers } from "./triggers/availableTriggers";
|
import { availableTriggers } from "./triggers/availableTriggers";
|
||||||
import { AutomodPluginType, ConfigSchema } from "./types";
|
import { AutomodPluginType, ConfigSchema } from "./types";
|
||||||
import { PhishermanPlugin } from "../Phisherman/PhishermanPlugin";
|
|
||||||
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin";
|
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
config: {
|
config: {
|
||||||
|
@ -63,13 +63,15 @@ const defaultOptions = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config preprocessor to set default values for triggers and perform extra validation
|
* Config preprocessor to set default values for triggers and perform extra validation
|
||||||
|
* TODO: Separate input and output types
|
||||||
*/
|
*/
|
||||||
const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) => {
|
const configParser = (input: unknown) => {
|
||||||
if (options.config?.rules) {
|
const rules = (input as any).rules;
|
||||||
|
if (rules) {
|
||||||
// Loop through each rule
|
// Loop through each rule
|
||||||
for (const [name, rule] of Object.entries(options.config.rules)) {
|
for (const [name, rule] of Object.entries(rules)) {
|
||||||
if (rule == null) {
|
if (rule == null) {
|
||||||
delete options.config.rules[name];
|
delete rules[name];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +99,7 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
|
||||||
for (const triggerObj of rule["triggers"]) {
|
for (const triggerObj of rule["triggers"]) {
|
||||||
for (const triggerName in triggerObj) {
|
for (const triggerName in triggerObj) {
|
||||||
if (!availableTriggers[triggerName]) {
|
if (!availableTriggers[triggerName]) {
|
||||||
throw new StrictValidationError([`Unknown trigger '${triggerName}' in rule '${rule.name}'`]);
|
throw new StrictValidationError([`Unknown trigger '${triggerName}' in rule '${rule["name"]}'`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerBlueprint = availableTriggers[triggerName];
|
const triggerBlueprint = availableTriggers[triggerName];
|
||||||
|
@ -117,11 +119,11 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
|
||||||
|
|
||||||
if (white && black) {
|
if (white && black) {
|
||||||
throw new StrictValidationError([
|
throw new StrictValidationError([
|
||||||
`Cannot have both blacklist and whitelist enabled at rule <${rule.name}/match_attachment_type>`,
|
`Cannot have both blacklist and whitelist enabled at rule <${rule["name"]}/match_attachment_type>`,
|
||||||
]);
|
]);
|
||||||
} else if (!white && !black) {
|
} else if (!white && !black) {
|
||||||
throw new StrictValidationError([
|
throw new StrictValidationError([
|
||||||
`Must have either blacklist or whitelist enabled at rule <${rule.name}/match_attachment_type>`,
|
`Must have either blacklist or whitelist enabled at rule <${rule["name"]}/match_attachment_type>`,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,11 +134,11 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
|
||||||
|
|
||||||
if (white && black) {
|
if (white && black) {
|
||||||
throw new StrictValidationError([
|
throw new StrictValidationError([
|
||||||
`Cannot have both blacklist and whitelist enabled at rule <${rule.name}/match_mime_type>`,
|
`Cannot have both blacklist and whitelist enabled at rule <${rule["name"]}/match_mime_type>`,
|
||||||
]);
|
]);
|
||||||
} else if (!white && !black) {
|
} else if (!white && !black) {
|
||||||
throw new StrictValidationError([
|
throw new StrictValidationError([
|
||||||
`Must have either blacklist or whitelist enabled at rule <${rule.name}/match_mime_type>`,
|
`Must have either blacklist or whitelist enabled at rule <${rule["name"]}/match_mime_type>`,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,7 +149,7 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
|
||||||
if (rule["actions"]) {
|
if (rule["actions"]) {
|
||||||
for (const actionName in rule["actions"]) {
|
for (const actionName in rule["actions"]) {
|
||||||
if (!availableActions[actionName]) {
|
if (!availableActions[actionName]) {
|
||||||
throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule.name}'`]);
|
throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule["name"]}'`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionBlueprint = availableActions[actionName];
|
const actionBlueprint = availableActions[actionName];
|
||||||
|
@ -163,9 +165,9 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
|
||||||
|
|
||||||
// Enable logging of automod actions by default
|
// Enable logging of automod actions by default
|
||||||
if (rule["actions"]) {
|
if (rule["actions"]) {
|
||||||
for (const actionName in rule.actions) {
|
for (const actionName in rule["actions"]) {
|
||||||
if (!availableActions[actionName]) {
|
if (!availableActions[actionName]) {
|
||||||
throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule.name}'`]);
|
throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule["name"]}'`]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,13 +175,13 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = (options) =>
|
||||||
rule["actions"]["log"] = true;
|
rule["actions"]["log"] = true;
|
||||||
}
|
}
|
||||||
if (rule["actions"]["clean"] && rule["actions"]["start_thread"]) {
|
if (rule["actions"]["clean"] && rule["actions"]["start_thread"]) {
|
||||||
throw new StrictValidationError([`Cannot have both clean and start_thread at rule '${rule.name}'`]);
|
throw new StrictValidationError([`Cannot have both clean and start_thread at rule '${rule["name"]}'`]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return parseIoTsSchema(ConfigSchema, input);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
|
export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
|
||||||
|
@ -195,11 +197,11 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
|
||||||
CountersPlugin,
|
CountersPlugin,
|
||||||
PhishermanPlugin,
|
PhishermanPlugin,
|
||||||
InternalPosterPlugin,
|
InternalPosterPlugin,
|
||||||
|
RoleManagerPlugin,
|
||||||
],
|
],
|
||||||
|
|
||||||
configSchema: ConfigSchema,
|
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
configPreprocessor,
|
configParser,
|
||||||
|
|
||||||
customOverrideCriteriaFunctions: {
|
customOverrideCriteriaFunctions: {
|
||||||
antiraid_level: (pluginData, matchParams, value) => {
|
antiraid_level: (pluginData, matchParams, value) => {
|
||||||
|
@ -218,118 +220,126 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
|
||||||
// Messages use message events from SavedMessages, see onLoad below
|
// Messages use message events from SavedMessages, see onLoad below
|
||||||
],
|
],
|
||||||
|
|
||||||
commands: [AntiraidClearCmd, SetAntiraidCmd, ViewAntiraidCmd],
|
messageCommands: [AntiraidClearCmd, SetAntiraidCmd, ViewAntiraidCmd],
|
||||||
|
|
||||||
async beforeLoad(pluginData) {
|
async beforeLoad(pluginData) {
|
||||||
pluginData.state.queue = new Queue();
|
const { state, guild } = pluginData;
|
||||||
|
|
||||||
pluginData.state.regexRunner = getRegExpRunner(`guild-${pluginData.guild.id}`);
|
state.queue = new Queue();
|
||||||
|
|
||||||
pluginData.state.recentActions = [];
|
state.regexRunner = getRegExpRunner(`guild-${guild.id}`);
|
||||||
|
|
||||||
pluginData.state.recentSpam = [];
|
state.recentActions = [];
|
||||||
|
|
||||||
pluginData.state.recentNicknameChanges = new Map();
|
state.recentSpam = [];
|
||||||
|
|
||||||
pluginData.state.ignoredRoleChanges = new Set();
|
state.recentNicknameChanges = new Map();
|
||||||
|
|
||||||
pluginData.state.cooldownManager = new CooldownManager();
|
state.ignoredRoleChanges = new Set();
|
||||||
|
|
||||||
pluginData.state.logs = new GuildLogs(pluginData.guild.id);
|
state.cooldownManager = new CooldownManager();
|
||||||
pluginData.state.savedMessages = GuildSavedMessages.getGuildInstance(pluginData.guild.id);
|
|
||||||
pluginData.state.antiraidLevels = GuildAntiraidLevels.getGuildInstance(pluginData.guild.id);
|
|
||||||
pluginData.state.archives = GuildArchives.getGuildInstance(pluginData.guild.id);
|
|
||||||
|
|
||||||
pluginData.state.cachedAntiraidLevel = await pluginData.state.antiraidLevels.get();
|
state.logs = new GuildLogs(guild.id);
|
||||||
|
state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id);
|
||||||
|
state.antiraidLevels = GuildAntiraidLevels.getGuildInstance(guild.id);
|
||||||
|
state.archives = GuildArchives.getGuildInstance(guild.id);
|
||||||
|
|
||||||
|
state.cachedAntiraidLevel = await state.antiraidLevels.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
async afterLoad(pluginData) {
|
async afterLoad(pluginData) {
|
||||||
pluginData.state.clearRecentActionsInterval = setInterval(() => clearOldRecentActions(pluginData), 1 * MINUTES);
|
const { state } = pluginData;
|
||||||
pluginData.state.clearRecentSpamInterval = setInterval(() => clearOldRecentSpam(pluginData), 1 * SECONDS);
|
|
||||||
pluginData.state.clearRecentNicknameChangesInterval = setInterval(
|
state.clearRecentActionsInterval = setInterval(() => clearOldRecentActions(pluginData), 1 * MINUTES);
|
||||||
|
state.clearRecentSpamInterval = setInterval(() => clearOldRecentSpam(pluginData), 1 * SECONDS);
|
||||||
|
state.clearRecentNicknameChangesInterval = setInterval(
|
||||||
() => clearOldRecentNicknameChanges(pluginData),
|
() => clearOldRecentNicknameChanges(pluginData),
|
||||||
30 * SECONDS,
|
30 * SECONDS,
|
||||||
);
|
);
|
||||||
|
|
||||||
pluginData.state.onMessageCreateFn = (message) => runAutomodOnMessage(pluginData, message, false);
|
state.onMessageCreateFn = (message) => runAutomodOnMessage(pluginData, message, false);
|
||||||
pluginData.state.savedMessages.events.on("create", pluginData.state.onMessageCreateFn);
|
state.savedMessages.events.on("create", state.onMessageCreateFn);
|
||||||
|
|
||||||
pluginData.state.onMessageUpdateFn = (message) => runAutomodOnMessage(pluginData, message, true);
|
|
||||||
pluginData.state.savedMessages.events.on("update", pluginData.state.onMessageUpdateFn);
|
|
||||||
|
|
||||||
|
state.onMessageUpdateFn = (message) => runAutomodOnMessage(pluginData, message, true);
|
||||||
|
state.savedMessages.events.on("update", state.onMessageUpdateFn);
|
||||||
const countersPlugin = pluginData.getPlugin(CountersPlugin);
|
const countersPlugin = pluginData.getPlugin(CountersPlugin);
|
||||||
|
|
||||||
pluginData.state.onCounterTrigger = (name, triggerName, channelId, userId) => {
|
state.onCounterTrigger = (name, triggerName, channelId, userId) => {
|
||||||
runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, false);
|
runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
pluginData.state.onCounterReverseTrigger = (name, triggerName, channelId, userId) => {
|
state.onCounterReverseTrigger = (name, triggerName, channelId, userId) => {
|
||||||
runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, true);
|
runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, true);
|
||||||
};
|
};
|
||||||
|
countersPlugin.onCounterEvent("trigger", state.onCounterTrigger);
|
||||||
countersPlugin.onCounterEvent("trigger", pluginData.state.onCounterTrigger);
|
countersPlugin.onCounterEvent("reverseTrigger", state.onCounterReverseTrigger);
|
||||||
countersPlugin.onCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger);
|
|
||||||
|
|
||||||
const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter();
|
const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter();
|
||||||
pluginData.state.modActionsListeners = new Map();
|
state.modActionsListeners = new Map();
|
||||||
pluginData.state.modActionsListeners.set("note", (userId: string) =>
|
state.modActionsListeners.set("note", (userId: string) => runAutomodOnModAction(pluginData, "note", userId));
|
||||||
runAutomodOnModAction(pluginData, "note", userId),
|
state.modActionsListeners.set("warn", (userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
||||||
|
runAutomodOnModAction(pluginData, "warn", userId, reason, isAutomodAction),
|
||||||
);
|
);
|
||||||
pluginData.state.modActionsListeners.set(
|
state.modActionsListeners.set("kick", (userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
||||||
"warn",
|
runAutomodOnModAction(pluginData, "kick", userId, reason, isAutomodAction),
|
||||||
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
|
||||||
runAutomodOnModAction(pluginData, "warn", userId, reason, isAutomodAction),
|
|
||||||
);
|
);
|
||||||
pluginData.state.modActionsListeners.set(
|
state.modActionsListeners.set("ban", (userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
||||||
"kick",
|
runAutomodOnModAction(pluginData, "ban", userId, reason, isAutomodAction),
|
||||||
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
|
||||||
runAutomodOnModAction(pluginData, "kick", userId, reason, isAutomodAction),
|
|
||||||
);
|
);
|
||||||
pluginData.state.modActionsListeners.set(
|
state.modActionsListeners.set("unban", (userId: string) => runAutomodOnModAction(pluginData, "unban", userId));
|
||||||
"ban",
|
registerEventListenersFromMap(modActionsEvents, state.modActionsListeners);
|
||||||
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
|
||||||
runAutomodOnModAction(pluginData, "ban", userId, reason, isAutomodAction),
|
|
||||||
);
|
|
||||||
pluginData.state.modActionsListeners.set("unban", (userId: string) =>
|
|
||||||
runAutomodOnModAction(pluginData, "unban", userId),
|
|
||||||
);
|
|
||||||
registerEventListenersFromMap(modActionsEvents, pluginData.state.modActionsListeners);
|
|
||||||
|
|
||||||
const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter();
|
const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter();
|
||||||
pluginData.state.mutesListeners = new Map();
|
state.mutesListeners = new Map();
|
||||||
pluginData.state.mutesListeners.set(
|
state.mutesListeners.set("mute", (userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
||||||
"mute",
|
runAutomodOnModAction(pluginData, "mute", userId, reason, isAutomodAction),
|
||||||
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
|
||||||
runAutomodOnModAction(pluginData, "mute", userId, reason, isAutomodAction),
|
|
||||||
);
|
);
|
||||||
pluginData.state.mutesListeners.set("unmute", (userId: string) =>
|
state.mutesListeners.set("unmute", (userId: string) => runAutomodOnModAction(pluginData, "unmute", userId));
|
||||||
runAutomodOnModAction(pluginData, "unmute", userId),
|
registerEventListenersFromMap(mutesEvents, state.mutesListeners);
|
||||||
);
|
|
||||||
registerEventListenersFromMap(mutesEvents, pluginData.state.mutesListeners);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async beforeUnload(pluginData) {
|
async beforeUnload(pluginData) {
|
||||||
|
const { state, guild } = pluginData;
|
||||||
|
|
||||||
const countersPlugin = pluginData.getPlugin(CountersPlugin);
|
const countersPlugin = pluginData.getPlugin(CountersPlugin);
|
||||||
countersPlugin.offCounterEvent("trigger", pluginData.state.onCounterTrigger);
|
if (state.onCounterTrigger) {
|
||||||
countersPlugin.offCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger);
|
countersPlugin.offCounterEvent("trigger", state.onCounterTrigger);
|
||||||
|
}
|
||||||
|
if (state.onCounterReverseTrigger) {
|
||||||
|
countersPlugin.offCounterEvent("reverseTrigger", state.onCounterReverseTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter();
|
const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter();
|
||||||
unregisterEventListenersFromMap(modActionsEvents, pluginData.state.modActionsListeners);
|
if (state.modActionsListeners) {
|
||||||
|
unregisterEventListenersFromMap(modActionsEvents, state.modActionsListeners);
|
||||||
|
}
|
||||||
|
|
||||||
const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter();
|
const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter();
|
||||||
unregisterEventListenersFromMap(mutesEvents, pluginData.state.mutesListeners);
|
if (state.mutesListeners) {
|
||||||
|
unregisterEventListenersFromMap(mutesEvents, state.mutesListeners);
|
||||||
|
}
|
||||||
|
|
||||||
pluginData.state.queue.clear();
|
state.queue.clear();
|
||||||
|
|
||||||
discardRegExpRunner(`guild-${pluginData.guild.id}`);
|
discardRegExpRunner(`guild-${guild.id}`);
|
||||||
|
|
||||||
clearInterval(pluginData.state.clearRecentActionsInterval);
|
if (state.clearRecentActionsInterval) {
|
||||||
|
clearInterval(state.clearRecentActionsInterval);
|
||||||
|
}
|
||||||
|
|
||||||
clearInterval(pluginData.state.clearRecentSpamInterval);
|
if (state.clearRecentSpamInterval) {
|
||||||
|
clearInterval(state.clearRecentSpamInterval);
|
||||||
|
}
|
||||||
|
|
||||||
clearInterval(pluginData.state.clearRecentNicknameChangesInterval);
|
if (state.clearRecentNicknameChangesInterval) {
|
||||||
|
clearInterval(state.clearRecentNicknameChangesInterval);
|
||||||
|
}
|
||||||
|
|
||||||
pluginData.state.savedMessages.events.off("create", pluginData.state.onMessageCreateFn);
|
if (state.onMessageCreateFn) {
|
||||||
pluginData.state.savedMessages.events.off("update", pluginData.state.onMessageUpdateFn);
|
state.savedMessages.events.off("create", state.onMessageCreateFn);
|
||||||
|
}
|
||||||
|
if (state.onMessageUpdateFn) {
|
||||||
|
state.savedMessages.events.off("update", state.onMessageUpdateFn);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { Permissions, Snowflake } from "discord.js";
|
import { PermissionFlagsBits, Snowflake } from "discord.js";
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import { LogType } from "../../../data/LogType";
|
|
||||||
import { nonNullish, unique } from "../../../utils";
|
import { nonNullish, unique } from "../../../utils";
|
||||||
import { canAssignRole } from "../../../utils/canAssignRole";
|
import { canAssignRole } from "../../../utils/canAssignRole";
|
||||||
import { getMissingPermissions } from "../../../utils/getMissingPermissions";
|
import { getMissingPermissions } from "../../../utils/getMissingPermissions";
|
||||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
|
||||||
import { missingPermissionError } from "../../../utils/missingPermissionError";
|
import { missingPermissionError } from "../../../utils/missingPermissionError";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
|
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||||
import { ignoreRoleChange } from "../functions/ignoredRoleChanges";
|
import { ignoreRoleChange } from "../functions/ignoredRoleChanges";
|
||||||
import { automodAction } from "../helpers";
|
import { automodAction } from "../helpers";
|
||||||
|
|
||||||
const p = Permissions.FLAGS;
|
const p = PermissionFlagsBits;
|
||||||
|
|
||||||
export const AddRolesAction = automodAction({
|
export const AddRolesAction = automodAction({
|
||||||
configType: t.array(t.string),
|
configType: t.array(t.string),
|
||||||
|
@ -20,7 +19,7 @@ export const AddRolesAction = automodAction({
|
||||||
const members = unique(contexts.map((c) => c.member).filter(nonNullish));
|
const members = unique(contexts.map((c) => c.member).filter(nonNullish));
|
||||||
const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!;
|
const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!;
|
||||||
|
|
||||||
const missingPermissions = getMissingPermissions(me.permissions, p.MANAGE_ROLES);
|
const missingPermissions = getMissingPermissions(me.permissions, p.ManageRoles);
|
||||||
if (missingPermissions) {
|
if (missingPermissions) {
|
||||||
const logs = pluginData.getPlugin(LogsPlugin);
|
const logs = pluginData.getPlugin(LogsPlugin);
|
||||||
logs.logBotAlert({
|
logs.logBotAlert({
|
||||||
|
@ -53,25 +52,14 @@ export const AddRolesAction = automodAction({
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
members.map(async (member) => {
|
members.map(async (member) => {
|
||||||
const memberRoles = new Set(member.roles.cache.keys());
|
const currentMemberRoles = new Set(member.roles.cache.keys());
|
||||||
for (const roleId of rolesToAssign) {
|
for (const roleId of rolesToAssign) {
|
||||||
memberRoles.add(roleId as Snowflake);
|
if (!currentMemberRoles.has(roleId)) {
|
||||||
ignoreRoleChange(pluginData, member.id, roleId);
|
pluginData.getPlugin(RoleManagerPlugin).addRole(member.id, roleId);
|
||||||
|
// TODO: Remove this and just ignore bot changes in general?
|
||||||
|
ignoreRoleChange(pluginData, member.id, roleId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (memberRoles.size === member.roles.cache.size) {
|
|
||||||
// No role changes
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
|
|
||||||
|
|
||||||
const rolesArr = Array.from(memberRoles.values());
|
|
||||||
await member.edit({
|
|
||||||
roles: rolesArr,
|
|
||||||
});
|
|
||||||
|
|
||||||
memberRoleLock.unlock();
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
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