3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-07-08 03:27:20 +00:00

Merge branch 'master' into patch-1

This commit is contained in:
Almeida 2023-12-29 12:31:16 +00:00 committed by GitHub
commit e839841109
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
682 changed files with 24590 additions and 35635 deletions

View file

@ -1,10 +0,0 @@
PORT=
CLIENT_ID=
CLIENT_SECRET=
OAUTH_CALLBACK_URL=
DASHBOARD_URL=
DB_HOST=
DB_USER=
DB_PASSWORD=
DB_DATABASE=
STAFF=

View file

@ -1,7 +0,0 @@
TOKEN=
DB_HOST=
DB_USER=
DB_PASSWORD=
DB_DATABASE=
PROFILING=false
PHISHERMAN_API_KEY=

View file

@ -1,65 +0,0 @@
const fs = require("fs");
const path = require("path");
const pkgUp = require("pkg-up");
const closestPackageJson = pkgUp.sync();
if (!closestPackageJson) {
throw new Error("Could not find project root from ormconfig.js");
}
const backendRoot = path.dirname(closestPackageJson);
try {
fs.accessSync(path.resolve(backendRoot, "bot.env"));
require("dotenv").config({ path: path.resolve(backendRoot, "bot.env") });
} catch {
try {
fs.accessSync(path.resolve(backendRoot, "api.env"));
require("dotenv").config({ path: path.resolve(backendRoot, "api.env") });
} catch {
throw new Error("bot.env or api.env required");
}
}
const moment = require("moment-timezone");
moment.tz.setDefault("UTC");
const entities = path.relative(process.cwd(), path.resolve(backendRoot, "dist/backend/src/data/entities/*.js"));
const migrations = path.relative(process.cwd(), path.resolve(backendRoot, "dist/backend/src/migrations/*.js"));
const migrationsDir = path.relative(process.cwd(), path.resolve(backendRoot, "src/migrations"));
module.exports = {
type: "mysql",
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
charset: "utf8mb4",
supportBigNumbers: true,
bigNumberStrings: true,
dateStrings: true,
synchronize: false,
connectTimeout: 2000,
logging: ["error", "warn"],
// Entities
entities: [entities],
// Pool options
extra: {
typeCast(field, next) {
if (field.type === "DATETIME") {
const val = field.string();
return val != null ? moment.utc(val).format("YYYY-MM-DD HH:mm:ss") : null;
}
return next();
},
},
// Migrations
migrations: [migrations],
cli: {
migrationsDir,
},
};

14803
backend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,16 +8,22 @@
"watch-yaml-parse-test": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node dist/backend/src/yamlParseTest.js\"",
"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-debug": "NODE_ENV=development DEBUG=true 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-debug": "NODE_ENV=production DEBUG=true 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\"",
"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 DEBUG=true 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-debug": "NODE_ENV=production DEBUG=true 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\"",
"typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js",
"migrate-prod": "npm run typeorm -- migration:run",
"migrate-dev": "npm run build && npm run typeorm -- migration:run",
"migrate-rollback-prod": "npm run typeorm -- migration:revert",
"migrate-rollback-dev": "npm run build && npm run typeorm -- migration:revert",
"migrate": "npm run typeorm -- migration:run -d dist/backend/src/data/dataSource.js",
"migrate-prod": "cross-env NODE_ENV=production npm run migrate",
"migrate-dev": "cross-env NODE_ENV=development npm run build && npm run migrate",
"migrate-rollback": "npm run typeorm -- migration:revert -d dist/backend/src/data/dataSource.js",
"migrate-rollback-prod": "cross-env NODE_ENV=production npm run migrate",
"migrate-rollback-dev": "cross-env NODE_ENV=development npm run build && npm run migrate",
"test": "npm run build && npm run run-tests",
"run-tests": "ava",
"test-watch": "tsc-watch --onSuccess \"npx ava\""
@ -25,21 +31,21 @@
"dependencies": {
"@silvia-odwyer/photon-node": "^0.3.1",
"bufferutil": "^4.0.3",
"clinic": "^13.0.0",
"cors": "^2.8.5",
"cross-env": "^5.2.0",
"cross-env": "^7.0.3",
"deep-diff": "^1.0.2",
"discord-api-types": "^0.22.0",
"discord.js": "^13.3.1",
"discord.js": "^14.11.0",
"dotenv": "^4.0.0",
"emoji-regex": "^8.0.0",
"erlpack": "github:almeidx/erlpack#f0c535f73817fd914806d6ca26a7730c14e0fb7c",
"erlpack": "github:discord/erlpack",
"escape-string-regexp": "^1.0.5",
"express": "^4.17.0",
"fp-ts": "^2.0.1",
"humanize-duration": "^3.15.0",
"io-ts": "^2.0.0",
"js-yaml": "^3.13.1",
"knub": "^30.0.0-beta.46",
"knub": "^32.0.0-next.16",
"knub-command-manager": "^9.1.0",
"last-commit-log": "^2.1.0",
"lodash.chunk": "^4.2.0",
@ -49,13 +55,12 @@
"lodash.isequal": "^4.5.0",
"lodash.pick": "^4.4.0",
"moment-timezone": "^0.5.21",
"multer": "^1.4.3",
"multer": "^1.4.5-lts.1",
"mysql": "^2.16.0",
"node-fetch": "^2.6.5",
"parse-color": "^1.0.0",
"passport": "^0.4.0",
"passport": "^0.6.0",
"passport-custom": "^1.0.5",
"passport-oauth2": "^1.5.0",
"passport-oauth2": "^1.6.1",
"pkg-up": "^3.1.0",
"reflect-metadata": "^0.1.12",
"regexp-worker": "^1.1.0",
@ -67,9 +72,9 @@
"tmp": "0.0.33",
"tsconfig-paths": "^3.9.0",
"twemoji": "^12.1.4",
"typeorm": "^0.2.31",
"typeorm": "^0.3.17",
"utf-8-validate": "^5.0.5",
"uuid": "^3.3.2",
"uuid": "^9.0.0",
"yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build",
"zlib-sync": "^0.1.7",
"zod": "^3.7.2"
@ -82,18 +87,17 @@
"@types/lodash.at": "^4.6.3",
"@types/moment-timezone": "^0.5.6",
"@types/multer": "^1.4.7",
"@types/node": "^14.0.14",
"@types/node-fetch": "^2.5.12",
"@types/node": "^18.16.3",
"@types/passport": "^1.0.0",
"@types/passport-oauth2": "^1.4.8",
"@types/passport-strategy": "^0.2.35",
"@types/safe-regex": "^1.1.2",
"@types/tmp": "0.0.33",
"@types/twemoji": "^12.1.0",
"ava": "^3.10.0",
"@types/uuid": "^9.0.2",
"ava": "^5.3.1",
"rimraf": "^2.6.2",
"source-map-support": "^0.5.16",
"tsc-watch": "^4.0.0"
"source-map-support": "^0.5.16"
},
"ava": {
"files": [

40
backend/src/Blocker.ts Normal file
View 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();
}
}

View file

@ -9,6 +9,8 @@ export enum ERRORS {
INVALID_USER,
INVALID_MUTE_ROLE_ID,
MUTE_ROLE_ABOVE_ZEP,
USER_ABOVE_ZEP,
USER_NOT_MODERATABLE,
}
export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
@ -20,6 +22,8 @@ export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
[ERRORS.INVALID_USER]: "Invalid user",
[ERRORS.INVALID_MUTE_ROLE_ID]: "Specified mute role is not valid",
[ERRORS.MUTE_ROLE_ABOVE_ZEP]: "Specified mute role is above Zeppelin in the role hierarchy",
[ERRORS.USER_ABOVE_ZEP]: "Cannot mute user, specified user is above Zeppelin in the role hierarchy",
[ERRORS.USER_NOT_MODERATABLE]: "Cannot mute user, specified user is not moderatable",
};
export class RecoverablePluginError extends Error {

View file

@ -7,7 +7,7 @@ export class SimpleError extends Error {
super(message);
}
[util.inspect.custom](depth, options) {
[util.inspect.custom]() {
return `Error: ${this.message}`;
}
}

View file

@ -8,6 +8,7 @@ import { ApiLogins } from "../data/ApiLogins";
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
import { ApiUserInfo } from "../data/ApiUserInfo";
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
import { env } from "../env";
import { ok } from "./responses";
interface IPassportApiUser {
@ -17,7 +18,6 @@ interface IPassportApiUser {
declare global {
namespace Express {
// tslint:disable-next-line:no-empty-interface
interface User extends IPassportApiUser {}
}
}
@ -54,24 +54,8 @@ function simpleDiscordAPIRequest(bearerToken, path): Promise<any> {
export function initAuth(app: express.Express) {
app.use(passport.initialize());
if (!process.env.CLIENT_ID) {
throw new Error("Auth: CLIENT ID missing");
}
if (!process.env.CLIENT_SECRET) {
throw new Error("Auth: CLIENT SECRET missing");
}
if (!process.env.OAUTH_CALLBACK_URL) {
throw new Error("Auth: OAUTH CALLBACK URL missing");
}
if (!process.env.DASHBOARD_URL) {
throw new Error("DASHBOARD_URL missing!");
}
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 apiUserInfo = new ApiUserInfo();
@ -101,9 +85,9 @@ export function initAuth(app: express.Express) {
{
authorizationURL: "https://discord.com/api/oauth2/authorize",
tokenURL: "https://discord.com/api/oauth2/token",
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.OAUTH_CALLBACK_URL,
clientID: env.CLIENT_ID,
clientSecret: env.CLIENT_SECRET,
callbackURL: `${env.API_URL}/auth/oauth-callback`,
scope: ["identify"],
},
async (accessToken, refreshToken, profile, cb) => {
@ -132,9 +116,9 @@ export function initAuth(app: express.Express) {
passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
(req: Request, res: Response) => {
if (req.user && req.user.apiKey) {
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
res.redirect(`${env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
} else {
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?error=noAccess`);
res.redirect(`${env.DASHBOARD_URL}/login-callback/?error=noAccess`);
}
},
);
@ -165,7 +149,8 @@ export function initAuth(app: express.Express) {
export function apiTokenAuthHandlers() {
return [
passport.authenticate("api-token", { failWithError: true }),
passport.authenticate("api-token", { failWithError: true, session: false }),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(err, req: Request, res: Response, next) => {
return res.status(401).json({ error: err.message });
},

View file

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

View file

@ -1,20 +1,20 @@
import { ApiPermissions } from "@shared/apiPermissions";
import express, { Request, Response } from "express";
import { YAMLException } from "js-yaml";
import moment from "moment-timezone";
import { Queue } from "../Queue";
import { validateGuildConfig } from "../configValidator";
import { AllowedGuilds } from "../data/AllowedGuilds";
import { ApiAuditLog } from "../data/ApiAuditLog";
import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments";
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 { hasGuildPermission, requireGuildPermission } from "./permissions";
import { clientError, ok, serverError, unauthorized } from "./responses";
import { loadYamlSafely } from "../utils/loadYamlSafely";
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
import { isSnowflake } from "../utils";
import moment from "moment-timezone";
import { ApiAuditLog } from "../data/ApiAuditLog";
import { AuditLogEventTypes } from "../data/apiAuditLogTypes";
import { Queue } from "../Queue";
const apiPermissionAssignments = new ApiPermissionAssignments();
const auditLog = new ApiAuditLog();
@ -126,7 +126,7 @@ export function initGuildsAPI(app: express.Express) {
if (type !== ApiPermissionTypes.User) {
return clientError(res, "Invalid type");
}
if (!isSnowflake(targetId)) {
if (!isSnowflake(targetId) || targetId === req.user!.userId) {
return clientError(res, "Invalid targetId");
}
const validPermissions = new Set(Object.values(ApiPermissions));

View file

@ -1,13 +1,13 @@
import { ApiPermissions } from "@shared/apiPermissions";
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 { 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([
z.literal("replace"),
@ -50,7 +50,7 @@ export function initGuildsImportExportAPI(guildRouter: express.Router) {
importExportRouter.get(
"/:guildId/pre-import",
requireGuildPermission(ApiPermissions.ManageAccess),
async (req: Request, res: Response) => {
async (req: Request) => {
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
const minNum = await guildCases.getMinCaseNumber();
const maxNum = await guildCases.getMaxCaseNumber();

View file

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

View file

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

View file

@ -1,8 +1,12 @@
import { connect } from "../data/db";
import { setIsAPI } from "../globals";
import "./loadEnv";
// KEEP THIS AS FIRST IMPORT
// See comment in module for details
import "../threadsSignalFix";
if (!process.env.KEY) {
import { connect } from "../data/db";
import { env } from "../env";
import { setIsAPI } from "../globals";
if (!env.KEY) {
// tslint:disable-next-line:no-console
console.error("Project root .env with KEY is required!");
process.exit(1);
@ -20,5 +24,5 @@ setIsAPI(true);
// 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
connect().then(() => {
import("./start");
import("./start.js");
});

View file

@ -1,4 +0,0 @@
import path from "path";
require("dotenv").config({ path: path.resolve(process.cwd(), "../.env") });
require("dotenv").config({ path: path.resolve(process.cwd(), "api.env") });

View file

@ -4,7 +4,7 @@ export function unauthorized(res: Response) {
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 });
}

View file

@ -1,19 +1,20 @@
import cors from "cors";
import express from "express";
import multer from "multer";
import { TokenError } from "passport-oauth2";
import { env } from "../env";
import { initArchives } from "./archives";
import { initAuth } from "./auth";
import { initDocs } from "./docs";
import { initGuildsAPI } from "./guilds/index";
import { clientError, error, notFound } from "./responses";
import { startBackgroundTasks } from "./tasks";
import multer from "multer";
const app = express();
app.use(
cors({
origin: process.env.DASHBOARD_URL,
origin: env.DASHBOARD_URL,
}),
);
app.use(
@ -34,6 +35,7 @@ app.get("/", (req, res) => {
});
// Error response
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((err, req, res, next) => {
if (err instanceof TokenError) {
clientError(res, "Invalid code");
@ -44,11 +46,12 @@ app.use((err, req, res, next) => {
});
// 404 response
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((req, res, next) => {
return notFound(res);
});
const port = (process.env.PORT && parseInt(process.env.PORT, 10)) || 3000;
const port = env.API_PORT;
app.listen(port, "0.0.0.0", () => console.log(`API server listening on port ${port}`)); // tslint:disable-line
startBackgroundTasks();

View file

@ -1,5 +1,18 @@
import { GuildChannel, GuildMember, Snowflake, Util, User } from "discord.js";
import { baseCommandParameterTypeHelpers, baseTypeConverters, CommandContext, TypeConversionError } from "knub";
import {
escapeCodeBlock,
escapeInlineCode,
GuildChannel,
GuildMember,
GuildTextBasedChannel,
Snowflake,
User,
} from "discord.js";
import {
baseCommandParameterTypeHelpers,
CommandContext,
messageCommandBaseTypeConverters,
TypeConversionError,
} from "knub";
import { createTypeHelper } from "knub-command-manager";
import {
channelMentionRegex,
@ -16,7 +29,7 @@ import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarge
import { inputPatternToRegExp } from "./validatorUtils";
export const commandTypes = {
...baseTypeConverters,
...messageCommandBaseTypeConverters,
delay(value) {
const result = convertDelayStringToMS(value);
@ -30,7 +43,7 @@ export const commandTypes = {
async resolvedUser(value, context: CommandContext<any>) {
const result = await resolveUser(context.pluginData.client, value);
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;
},
@ -38,7 +51,7 @@ export const commandTypes = {
async resolvedUserLoose(value, context: CommandContext<any>) {
const result = await resolveUser(context.pluginData.client, value);
if (result == null) {
throw new TypeConversionError(`Invalid user: \`${Util.escapeCodeBlock(value)}\``);
throw new TypeConversionError(`Invalid user: \`${escapeCodeBlock(value)}\``);
}
return result;
},
@ -50,9 +63,7 @@ export const commandTypes = {
const result = await resolveMember(context.pluginData.client, context.message.channel.guild, value);
if (result == null) {
throw new TypeConversionError(
`Member \`${Util.escapeCodeBlock(value)}\` was not found or they have left the server`,
);
throw new TypeConversionError(`Member \`${escapeCodeBlock(value)}\` was not found or they have left the server`);
}
return result;
},
@ -62,7 +73,7 @@ export const commandTypes = {
const result = await resolveMessageTarget(context.pluginData, value);
if (!result) {
throw new TypeConversionError(`Unknown message \`${Util.escapeInlineCode(value)}\``);
throw new TypeConversionError(`Unknown message \`${escapeInlineCode(value)}\``);
}
return result;
@ -82,24 +93,28 @@ export const commandTypes = {
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 {
return inputPatternToRegExp(value);
} 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) {
if (!isValidTimezone(value)) {
throw new TypeConversionError(`Invalid timezone: ${Util.escapeInlineCode(value)}`);
throw new TypeConversionError(`Invalid timezone: ${escapeInlineCode(value)}`);
}
return value;
},
guildTextBasedChannel(value: string, context: CommandContext<any>) {
return messageCommandBaseTypeConverters.textChannel(value, context);
},
};
export const commandTypeHelpers = {
@ -113,4 +128,5 @@ export const commandTypeHelpers = {
anyId: createTypeHelper<Promise<Snowflake>>(commandTypes.anyId),
regex: createTypeHelper<RegExp>(commandTypes.regex),
timezone: createTypeHelper<string>(commandTypes.timezone),
guildTextBasedChannel: createTypeHelper<GuildTextBasedChannel>(commandTypes.guildTextBasedChannel),
};

View file

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

View file

@ -1,19 +1,20 @@
import { getRepository, Repository } from "typeorm";
import moment from "moment-timezone";
import { Repository } from "typeorm";
import { DBDateFormat } from "../utils";
import { ApiPermissionTypes } from "./ApiPermissionAssignments";
import { BaseRepository } from "./BaseRepository";
import { dataSource } from "./dataSource";
import { AllowedGuild } from "./entities/AllowedGuild";
import moment from "moment-timezone";
import { DBDateFormat } from "../utils";
export class AllowedGuilds extends BaseRepository {
private allowedGuilds: Repository<AllowedGuild>;
constructor() {
super();
this.allowedGuilds = getRepository(AllowedGuild);
this.allowedGuilds = dataSource.getRepository(AllowedGuild);
}
async isAllowed(guildId) {
async isAllowed(guildId: string) {
const count = await this.allowedGuilds.count({
where: {
id: guildId,
@ -22,11 +23,15 @@ export class AllowedGuilds extends BaseRepository {
return count !== 0;
}
find(guildId) {
return this.allowedGuilds.findOne(guildId);
find(guildId: string) {
return this.allowedGuilds.findOne({
where: {
id: guildId,
},
});
}
getForApiUser(userId) {
getForApiUser(userId: string) {
return this.allowedGuilds
.createQueryBuilder("allowed_guilds")
.innerJoin(

View file

@ -1,15 +1,15 @@
import { Repository } from "typeorm/index";
import { BaseRepository } from "./BaseRepository";
import { getRepository, Repository } from "typeorm/index";
import { ApiAuditLogEntry } from "./entities/ApiAuditLogEntry";
import { ApiLogin } from "./entities/ApiLogin";
import { AuditLogEventData, AuditLogEventType } from "./apiAuditLogTypes";
import { dataSource } from "./dataSource";
import { ApiAuditLogEntry } from "./entities/ApiAuditLogEntry";
export class ApiAuditLog extends BaseRepository {
private auditLog: Repository<ApiAuditLogEntry<any>>;
constructor() {
super();
this.auditLog = getRepository(ApiAuditLogEntry);
this.auditLog = dataSource.getRepository(ApiAuditLogEntry);
}
addEntry<TEventType extends AuditLogEventType>(

View file

@ -1,10 +1,11 @@
import crypto from "crypto";
import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
// tslint:disable-next-line:no-submodule-imports
import uuidv4 from "uuid/v4";
import { v4 as uuidv4 } from "uuid";
import { DAYS, DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { dataSource } from "./dataSource";
import { ApiLogin } from "./entities/ApiLogin";
const LOGIN_EXPIRY_TIME = 1 * DAYS;
@ -14,7 +15,7 @@ export class ApiLogins extends BaseRepository {
constructor() {
super();
this.apiLogins = getRepository(ApiLogin);
this.apiLogins = dataSource.getRepository(ApiLogin);
}
async getUserIdByApiKey(apiKey: string): Promise<string | null> {
@ -90,10 +91,15 @@ export class ApiLogins extends BaseRepository {
const [loginId, token] = apiKey.split(".");
if (!loginId || !token) return;
const updatedTime = moment().utc().add(LOGIN_EXPIRY_TIME, "ms");
const login = await this.apiLogins.createQueryBuilder().where("id = :id", { id: loginId }).getOne();
if (!login || moment.utc(login.expires_at).isSameOrAfter(updatedTime)) return;
await this.apiLogins.update(
{ id: loginId },
{
expires_at: moment().utc().add(LOGIN_EXPIRY_TIME, "ms").format(DBDateFormat),
expires_at: updatedTime.format(DBDateFormat),
},
);
}

View file

@ -1,10 +1,10 @@
import { ApiPermissions } from "@shared/apiPermissions";
import { getRepository, Repository } from "typeorm";
import { BaseRepository } from "./BaseRepository";
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
import { Permissions } from "discord.js";
import { Repository } from "typeorm";
import { ApiAuditLog } from "./ApiAuditLog";
import { BaseRepository } from "./BaseRepository";
import { AuditLogEventTypes } from "./apiAuditLogTypes";
import { dataSource } from "./dataSource";
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
export enum ApiPermissionTypes {
User = "USER",
@ -17,7 +17,7 @@ export class ApiPermissionAssignments extends BaseRepository {
constructor() {
super();
this.apiPermissions = getRepository(ApiPermissionAssignment);
this.apiPermissions = dataSource.getRepository(ApiPermissionAssignment);
this.auditLogs = new ApiAuditLog();
}
@ -80,7 +80,8 @@ export class ApiPermissionAssignments extends BaseRepository {
.createQueryBuilder()
.where("expires_at IS NOT NULL")
.andWhere("expires_at <= NOW()")
.delete();
.delete()
.execute();
}
async applyOwnerChange(guildId: string, newOwnerId: string) {

View file

@ -1,16 +1,16 @@
import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { connection } from "./db";
import { ApiUserInfo as ApiUserInfoEntity, ApiUserInfoData } from "./entities/ApiUserInfo";
import { dataSource } from "./dataSource";
import { ApiUserInfoData, ApiUserInfo as ApiUserInfoEntity } from "./entities/ApiUserInfo";
export class ApiUserInfo extends BaseRepository {
private apiUserInfo: Repository<ApiUserInfoEntity>;
constructor() {
super();
this.apiUserInfo = getRepository(ApiUserInfoEntity);
this.apiUserInfo = dataSource.getRepository(ApiUserInfoEntity);
}
get(id) {
@ -22,7 +22,7 @@ export class ApiUserInfo extends BaseRepository {
}
update(id, data: ApiUserInfoData) {
return connection.transaction(async (entityManager) => {
return dataSource.transaction(async (entityManager) => {
const repo = entityManager.getRepository(ApiUserInfoEntity);
const existingInfo = await repo.findOne({ where: { id } });

View file

@ -1,13 +1,14 @@
import { getRepository, Repository } from "typeorm";
import { ArchiveEntry } from "./entities/ArchiveEntry";
import { Repository } from "typeorm";
import { BaseRepository } from "./BaseRepository";
import { dataSource } from "./dataSource";
import { ArchiveEntry } from "./entities/ArchiveEntry";
export class Archives extends BaseRepository {
protected archives: Repository<ArchiveEntry>;
constructor() {
super();
this.archives = getRepository(ArchiveEntry);
this.archives = dataSource.getRepository(ArchiveEntry);
}
public deleteExpiredArchives() {

View file

@ -1,6 +1,6 @@
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>;
protected guildId: string;

View file

@ -1,6 +1,6 @@
import { asyncMap } from "../utils/async";
export class BaseRepository<TEntity extends unknown = unknown> {
export class BaseRepository<TEntity = unknown> {
private nextRelations: string[];
constructor() {
@ -40,7 +40,7 @@ export class BaseRepository<TEntity extends unknown = unknown> {
return entity;
}
protected async processEntityFromDB<T extends TEntity | undefined>(entity: T): Promise<T> {
protected async processEntityFromDB<T extends TEntity | null>(entity: T): Promise<T> {
return this._processEntityFromDB(entity);
}

View file

@ -1,20 +1,21 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { isAPI } from "../globals";
import { HOURS, SECONDS } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { cleanupConfigs } from "./cleanup/configs";
import { connection } from "./db";
import { dataSource } from "./dataSource";
import { Config } from "./entities/Config";
const CLEANUP_INTERVAL = 1 * HOURS;
async function cleanup() {
await cleanupConfigs();
setTimeout(cleanup, CLEANUP_INTERVAL);
}
if (isAPI()) {
const CLEANUP_INTERVAL = 1 * HOURS;
async function cleanup() {
await cleanupConfigs();
setTimeout(cleanup, CLEANUP_INTERVAL);
}
// Start first cleanup 30 seconds after startup
// TODO: Move to bot startup code
setTimeout(cleanup, 30 * SECONDS);
}
@ -23,7 +24,7 @@ export class Configs extends BaseRepository {
constructor() {
super();
this.configs = getRepository(Config);
this.configs = dataSource.getRepository(Config);
}
getActiveByKey(key) {
@ -36,7 +37,7 @@ export class Configs extends BaseRepository {
}
async getHighestId(): Promise<number> {
const rows = await connection.query("SELECT MAX(id) AS highest_id FROM configs");
const rows = await dataSource.query("SELECT MAX(id) AS highest_id FROM configs");
return (rows.length && rows[0].highest_id) || 0;
}
@ -61,7 +62,7 @@ export class Configs extends BaseRepository {
}
async saveNewRevision(key, config, editedBy) {
return connection.transaction(async (entityManager) => {
return dataSource.transaction(async (entityManager) => {
const repo = entityManager.getRepository(Config);
// Mark all old revisions inactive
await repo.update({ key }, { is_active: false });

View file

@ -11,10 +11,10 @@
"MEMBER_UNBAN": "{timestamp} 🔓 User (`{userId}`) was unbanned by {userMention(mod)}",
"MEMBER_FORCEBAN": "{timestamp} 🔨 User (`{userId}`) was forcebanned 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 <t:{account_age_ts}:R>)",
"MEMBER_LEAVE": "{timestamp} 📤 {userMention(member)} left the server",
"MEMBER_ROLE_ADD": "{timestamp} 🔑 {userMention(member)} received roles: **{roles}**",
"MEMBER_ROLE_REMOVE": "{timestamp} 🔑 {userMention(member)} lost roles: **{roles}**",
"MEMBER_ROLE_ADD": "{timestamp} 🔑 {userMention(mod)} added roles for {userMention(member)}: **{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_NICK_CHANGE": "{timestamp} ✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**",
"MEMBER_USERNAME_CHANGE": "{timestamp} ✏ {userMention(user)}: username changed from **{oldName}** to **{newName}**",

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { AntiraidLevel } from "./entities/AntiraidLevel";
export class GuildAntiraidLevels extends BaseGuildRepository {
@ -7,7 +8,7 @@ export class GuildAntiraidLevels extends BaseGuildRepository {
constructor(guildId: string) {
super(guildId);
this.antiraidLevels = getRepository(AntiraidLevel);
this.antiraidLevels = dataSource.getRepository(AntiraidLevel);
}
async get() {

View file

@ -1,18 +1,15 @@
import { Guild, Snowflake, User } from "discord.js";
import { Guild, Snowflake } from "discord.js";
import moment from "moment-timezone";
import { isDefaultSticker } from "src/utils/isDefaultSticker";
import { getRepository, Repository } from "typeorm";
import { renderTemplate, TemplateSafeValueContainer } from "../templateFormatter";
import { trimLines } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { ArchiveEntry } from "./entities/ArchiveEntry";
import {
channelToTemplateSafeChannel,
guildToTemplateSafeGuild,
userToTemplateSafeUser,
} from "../utils/templateSafeObjects";
import { SavedMessage } from "./entities/SavedMessage";
import { Repository } from "typeorm";
import { TemplateSafeValueContainer, renderTemplate } from "../templateFormatter";
import { renderUsername, trimLines } from "../utils";
import { decrypt, encrypt } from "../utils/crypt";
import { channelToTemplateSafeChannel, guildToTemplateSafeGuild } from "../utils/templateSafeObjects";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { ArchiveEntry } from "./entities/ArchiveEntry";
import { SavedMessage } from "./entities/SavedMessage";
const DEFAULT_EXPIRY_DAYS = 30;
@ -20,14 +17,14 @@ const MESSAGE_ARCHIVE_HEADER_FORMAT = trimLines(`
Server: {guild.name} ({guild.id})
`);
const MESSAGE_ARCHIVE_MESSAGE_FORMAT =
"[#{channel.name}] [{user.id}] [{timestamp}] {user.username}#{user.discriminator}: {content}{attachments}{stickers}";
"[#{channel.name}] [{user.id}] [{timestamp}] {username}: {content}{attachments}{stickers}";
export class GuildArchives extends BaseGuildRepository<ArchiveEntry> {
protected archives: Repository<ArchiveEntry>;
constructor(guildId) {
super(guildId);
this.archives = getRepository(ArchiveEntry);
this.archives = dataSource.getRepository(ArchiveEntry);
}
protected async _processEntityFromDB(entity: ArchiveEntry | undefined) {
@ -46,7 +43,7 @@ export class GuildArchives extends BaseGuildRepository<ArchiveEntry> {
return entity;
}
async find(id: string): Promise<ArchiveEntry | undefined> {
async find(id: string): Promise<ArchiveEntry | null> {
const result = await this.archives.findOne({
where: { id },
relations: this.getRelations(),
@ -101,6 +98,7 @@ export class GuildArchives extends BaseGuildRepository<ArchiveEntry> {
}),
user: partialUser,
channel: channel ? channelToTemplateSafeChannel(channel) : null,
username: renderUsername(msg.data.author.username, msg.data.author.discriminator),
}),
);

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { AutoReaction } from "./entities/AutoReaction";
export class GuildAutoReactions extends BaseGuildRepository {
@ -7,7 +8,7 @@ export class GuildAutoReactions extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.autoReactions = getRepository(AutoReaction);
this.autoReactions = dataSource.getRepository(AutoReaction);
}
async all(): Promise<AutoReaction[]> {
@ -18,7 +19,7 @@ export class GuildAutoReactions extends BaseGuildRepository {
});
}
async getForChannel(channelId: string): Promise<AutoReaction | undefined> {
async getForChannel(channelId: string): Promise<AutoReaction | null> {
return this.autoReactions.findOne({
where: {
guild_id: this.guildId,

View file

@ -12,15 +12,19 @@ export class GuildButtonRoles extends BaseGuildRepository {
async getForButtonId(buttonId: string) {
return this.buttonRoles.findOne({
guild_id: this.guildId,
button_id: buttonId,
where: {
guild_id: this.guildId,
button_id: buttonId,
},
});
}
async getAllForMessageId(messageId: string) {
return this.buttonRoles.find({
guild_id: this.guildId,
message_id: messageId,
where: {
guild_id: this.guildId,
message_id: messageId,
},
});
}
@ -40,8 +44,10 @@ export class GuildButtonRoles extends BaseGuildRepository {
async getForButtonGroup(buttonGroup: string) {
return this.buttonRoles.find({
guild_id: this.guildId,
button_group: buttonGroup,
where: {
guild_id: this.guildId,
button_group: buttonGroup,
},
});
}

View file

@ -1,14 +1,11 @@
import { getRepository, In, InsertResult, Repository } from "typeorm";
import { In, InsertResult, Repository } from "typeorm";
import { Queue } from "../Queue";
import { chunkArray } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { CaseTypes } from "./CaseTypes";
import { connection } from "./db";
import { dataSource } from "./dataSource";
import { Case } from "./entities/Case";
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 {
private cases: Repository<Case>;
@ -18,8 +15,8 @@ export class GuildCases extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.cases = getRepository(Case);
this.caseNotes = getRepository(CaseNote);
this.cases = dataSource.getRepository(Case);
this.caseNotes = dataSource.getRepository(CaseNote);
this.createQueue = new Queue();
}
@ -33,7 +30,7 @@ export class GuildCases extends BaseGuildRepository {
});
}
async find(id: number): Promise<Case | undefined> {
async find(id: number): Promise<Case | null> {
return this.cases.findOne({
relations: this.getRelations(),
where: {
@ -43,7 +40,7 @@ export class GuildCases extends BaseGuildRepository {
});
}
async findByCaseNumber(caseNumber: number): Promise<Case | undefined> {
async findByCaseNumber(caseNumber: number): Promise<Case | null> {
return this.cases.findOne({
relations: this.getRelations(),
where: {
@ -53,7 +50,7 @@ export class GuildCases extends BaseGuildRepository {
});
}
async findLatestByModId(modId: string): Promise<Case | undefined> {
async findLatestByModId(modId: string): Promise<Case | null> {
return this.cases.findOne({
relations: this.getRelations(),
where: {
@ -66,7 +63,7 @@ export class GuildCases extends BaseGuildRepository {
});
}
async findByAuditLogId(auditLogId: string): Promise<Case | undefined> {
async findByAuditLogId(auditLogId: string): Promise<Case | null> {
return this.cases.findOne({
relations: this.getRelations(),
where: {
@ -91,7 +88,7 @@ export class GuildCases extends BaseGuildRepository {
where: {
guild_id: this.guildId,
mod_id: modId,
is_hidden: 0,
is_hidden: false,
},
});
}
@ -102,7 +99,7 @@ export class GuildCases extends BaseGuildRepository {
where: {
guild_id: this.guildId,
mod_id: modId,
is_hidden: 0,
is_hidden: false,
},
skip,
take: count,
@ -184,7 +181,7 @@ export class GuildCases extends BaseGuildRepository {
}
async softDelete(id: number, deletedById: string, deletedByName: string, deletedByText: string) {
return connection.transaction(async (entityManager) => {
return dataSource.transaction(async (entityManager) => {
const cases = entityManager.getRepository(Case);
const caseNotes = entityManager.getRepository(CaseNote);

View file

@ -1,5 +1,6 @@
import { DeleteResult, getRepository, InsertResult, Repository } from "typeorm";
import { DeleteResult, InsertResult, Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { ContextMenuLink } from "./entities/ContextMenuLink";
export class GuildContextMenuLinks extends BaseGuildRepository {
@ -7,10 +8,10 @@ export class GuildContextMenuLinks extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.contextLinks = getRepository(ContextMenuLink);
this.contextLinks = dataSource.getRepository(ContextMenuLink);
}
async get(id: string): Promise<ContextMenuLink | undefined> {
async get(id: string): Promise<ContextMenuLink | null> {
return this.contextLinks.findOne({
where: {
guild_id: this.guildId,

View file

@ -1,11 +1,11 @@
import moment from "moment-timezone";
import { FindConditions, getRepository, In, IsNull, Not, Repository } from "typeorm";
import { FindOptionsWhere, In, IsNull, Not, Repository } from "typeorm";
import { Queue } from "../Queue";
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { connection } from "./db";
import { dataSource } from "./dataSource";
import { Counter } from "./entities/Counter";
import { CounterTrigger, isValidCounterComparisonOp, TriggerComparisonOp } from "./entities/CounterTrigger";
import { CounterTrigger, TriggerComparisonOp, isValidCounterComparisonOp } from "./entities/CounterTrigger";
import { CounterTriggerState } from "./entities/CounterTriggerState";
import { CounterValue } from "./entities/CounterValue";
@ -17,11 +17,11 @@ const MAX_COUNTER_VALUE = 2147483647; // 2^31-1, for MySQL INT
const decayQueue = new Queue();
async function deleteCountersMarkedToBeDeleted(): Promise<void> {
await getRepository(Counter).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
await dataSource.getRepository(Counter).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
}
async function deleteTriggersMarkedToBeDeleted(): Promise<void> {
await getRepository(CounterTrigger).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
await dataSource.getRepository(CounterTrigger).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
}
setInterval(deleteCountersMarkedToBeDeleted, 1 * HOURS);
@ -38,10 +38,10 @@ export class GuildCounters extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.counters = getRepository(Counter);
this.counterValues = getRepository(CounterValue);
this.counterTriggers = getRepository(CounterTrigger);
this.counterTriggerStates = getRepository(CounterTriggerState);
this.counters = dataSource.getRepository(Counter);
this.counterValues = dataSource.getRepository(CounterValue);
this.counterTriggers = dataSource.getRepository(CounterTrigger);
this.counterTriggerStates = dataSource.getRepository(CounterTriggerState);
}
async findOrCreateCounter(name: string, perChannel: boolean, perUser: boolean): Promise<Counter> {
@ -80,7 +80,7 @@ export class GuildCounters extends BaseGuildRepository {
}
async markUnusedCountersToBeDeleted(idsToKeep: number[]): Promise<void> {
const criteria: FindConditions<Counter> = {
const criteria: FindOptionsWhere<Counter> = {
guild_id: this.guildId,
delete_at: IsNull(),
};
@ -161,7 +161,7 @@ export class GuildCounters extends BaseGuildRepository {
}
const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
if (decayAmountToApply === 0) {
if (decayAmountToApply === 0 || Number.isNaN(decayAmountToApply)) {
return;
}
@ -256,10 +256,12 @@ export class GuildCounters extends BaseGuildRepository {
throw new Error(`Invalid comparison value: ${reverseComparisonValue}`);
}
return connection.transaction(async (entityManager) => {
return dataSource.transaction(async (entityManager) => {
const existing = await entityManager.findOne(CounterTrigger, {
counter_id: counterId,
name: triggerName,
where: {
counter_id: counterId,
name: triggerName,
},
});
if (existing) {
@ -283,7 +285,11 @@ export class GuildCounters extends BaseGuildRepository {
reverse_comparison_value: reverseComparisonValue,
});
return (await entityManager.findOne(CounterTrigger, insertResult.identifiers[0].id))!;
return (await entityManager.findOne(CounterTrigger, {
where: {
id: insertResult.identifiers[0].id,
},
}))!;
});
}
@ -308,11 +314,13 @@ export class GuildCounters extends BaseGuildRepository {
channelId = channelId || "0";
userId = userId || "0";
return connection.transaction(async (entityManager) => {
return dataSource.transaction(async (entityManager) => {
const previouslyTriggered = await entityManager.findOne(CounterTriggerState, {
trigger_id: counterTrigger.id,
user_id: userId!,
channel_id: channelId!,
where: {
trigger_id: counterTrigger.id,
user_id: userId!,
channel_id: channelId!,
},
});
if (previouslyTriggered) {
@ -356,7 +364,7 @@ export class GuildCounters extends BaseGuildRepository {
async checkAllValuesForTrigger(
counterTrigger: CounterTrigger,
): Promise<Array<{ channelId: string; userId: string }>> {
return connection.transaction(async (entityManager) => {
return dataSource.transaction(async (entityManager) => {
const matchingValues = await entityManager
.createQueryBuilder(CounterValue, "cv")
.leftJoin(
@ -407,7 +415,7 @@ export class GuildCounters extends BaseGuildRepository {
channelId = channelId || "0";
userId = userId || "0";
return connection.transaction(async (entityManager) => {
return dataSource.transaction(async (entityManager) => {
const matchingValue = await entityManager
.createQueryBuilder(CounterValue, "cv")
.innerJoin(
@ -446,7 +454,7 @@ export class GuildCounters extends BaseGuildRepository {
async checkAllValuesForReverseTrigger(
counterTrigger: CounterTrigger,
): Promise<Array<{ channelId: string; userId: string }>> {
return connection.transaction(async (entityManager) => {
return dataSource.transaction(async (entityManager) => {
const matchingValues: Array<{
id: string;
triggerStateId: string;

View file

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

View file

@ -0,0 +1,102 @@
import moment from "moment-timezone";
import { Repository } from "typeorm";
import { Blocker } from "../Blocker";
import { DBDateFormat, MINUTES } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
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 = dataSource.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,
},
);
}
}

View file

@ -1,6 +1,6 @@
import { getRepository, Repository } from "typeorm/index";
import { Repository } from "typeorm/index";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { connection } from "./db";
import { dataSource } from "./dataSource";
import { MemberTimezone } from "./entities/MemberTimezone";
export class GuildMemberTimezones extends BaseGuildRepository {
@ -8,22 +8,26 @@ export class GuildMemberTimezones extends BaseGuildRepository {
constructor(guildId: string) {
super(guildId);
this.memberTimezones = getRepository(MemberTimezone);
this.memberTimezones = dataSource.getRepository(MemberTimezone);
}
get(memberId: string) {
return this.memberTimezones.findOne({
guild_id: this.guildId,
member_id: memberId,
where: {
guild_id: this.guildId,
member_id: memberId,
},
});
}
async set(memberId, timezone: string) {
await connection.transaction(async (entityManager) => {
await dataSource.transaction(async (entityManager) => {
const repo = entityManager.getRepository(MemberTimezone);
const existingRow = await repo.findOne({
guild_id: this.guildId,
member_id: memberId,
where: {
guild_id: this.guildId,
member_id: memberId,
},
});
if (existingRow) {

View file

@ -1,14 +1,26 @@
import moment from "moment-timezone";
import { Brackets, getRepository, Repository } from "typeorm";
import { Brackets, Repository } from "typeorm";
import { DBDateFormat } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { MuteTypes } from "./MuteTypes";
import { dataSource } from "./dataSource";
import { Mute } from "./entities/Mute";
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 {
private mutes: Repository<Mute>;
constructor(guildId) {
super(guildId);
this.mutes = getRepository(Mute);
this.mutes = dataSource.getRepository(Mute);
}
async getExpiredMutes(): Promise<Mute[]> {
@ -20,7 +32,7 @@ export class GuildMutes extends BaseGuildRepository {
.getMany();
}
async findExistingMuteForUserId(userId: string): Promise<Mute | undefined> {
async findExistingMuteForUserId(userId: string): Promise<Mute | null> {
return this.mutes.findOne({
where: {
guild_id: this.guildId,
@ -34,14 +46,18 @@ export class GuildMutes extends BaseGuildRepository {
return mute != null;
}
async addMute(userId, expiryTime, rolesToRestore?: string[]): Promise<Mute> {
const expiresAt = expiryTime ? moment.utc().add(expiryTime, "ms").format("YYYY-MM-DD HH:mm:ss") : null;
async addMute(params: AddMuteParams): Promise<Mute> {
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({
guild_id: this.guildId,
user_id: userId,
user_id: params.userId,
type: params.type,
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] }))!;
@ -74,6 +90,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[]> {
return this.mutes
.createQueryBuilder("mutes")
@ -104,4 +146,16 @@ export class GuildMutes extends BaseGuildRepository {
user_id: userId,
});
}
async fillMissingMuteRole(muteRole: string): Promise<void> {
await this.mutes
.createQueryBuilder()
.where("guild_id = :guild_id", { guild_id: this.guildId })
.andWhere("type = :type", { type: MuteTypes.Role })
.andWhere("mute_role IS NULL")
.update({
mute_role: muteRole,
})
.execute();
}
}

View file

@ -1,19 +1,21 @@
import { getRepository, In, Repository } from "typeorm";
import { In, Repository } from "typeorm";
import { isAPI } from "../globals";
import { MINUTES, SECONDS } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { cleanupNicknames } from "./cleanup/nicknames";
import { dataSource } from "./dataSource";
import { NicknameHistoryEntry } from "./entities/NicknameHistoryEntry";
const CLEANUP_INTERVAL = 5 * MINUTES;
async function cleanup() {
await cleanupNicknames();
setTimeout(cleanup, CLEANUP_INTERVAL);
}
if (!isAPI()) {
const CLEANUP_INTERVAL = 5 * MINUTES;
async function cleanup() {
await cleanupNicknames();
setTimeout(cleanup, CLEANUP_INTERVAL);
}
// Start first cleanup 30 seconds after startup
// TODO: Move to bot startup code
setTimeout(cleanup, 30 * SECONDS);
}
@ -24,7 +26,7 @@ export class GuildNicknameHistory extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.nicknameHistory = getRepository(NicknameHistoryEntry);
this.nicknameHistory = dataSource.getRepository(NicknameHistoryEntry);
}
async getByUserId(userId): Promise<NicknameHistoryEntry[]> {
@ -39,7 +41,7 @@ export class GuildNicknameHistory extends BaseGuildRepository {
});
}
getLastEntry(userId): Promise<NicknameHistoryEntry | undefined> {
getLastEntry(userId): Promise<NicknameHistoryEntry | null> {
return this.nicknameHistory.findOne({
where: {
guild_id: this.guildId,

View file

@ -1,18 +1,14 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { PersistedData } from "./entities/PersistedData";
export interface IPartialPersistData {
roles?: string[];
nickname?: string;
}
export class GuildPersistedData extends BaseGuildRepository {
private persistedData: Repository<PersistedData>;
constructor(guildId) {
super(guildId);
this.persistedData = getRepository(PersistedData);
this.persistedData = dataSource.getRepository(PersistedData);
}
async find(userId: string) {
@ -24,11 +20,7 @@ export class GuildPersistedData extends BaseGuildRepository {
});
}
async set(userId: string, data: IPartialPersistData = {}) {
const finalData: any = {};
if (data.roles) finalData.roles = data.roles.join(",");
if (data.nickname) finalData.nickname = data.nickname;
async set(userId: string, data: Partial<PersistedData> = {}) {
const existing = await this.find(userId);
if (existing) {
await this.persistedData.update(
@ -36,11 +28,11 @@ export class GuildPersistedData extends BaseGuildRepository {
guild_id: this.guildId,
user_id: userId,
},
finalData,
data,
);
} else {
await this.persistedData.insert({
...finalData,
...data,
guild_id: this.guildId,
user_id: userId,
});

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { PingableRole } from "./entities/PingableRole";
export class GuildPingableRoles extends BaseGuildRepository {
@ -7,7 +8,7 @@ export class GuildPingableRoles extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.pingableRoles = getRepository(PingableRole);
this.pingableRoles = dataSource.getRepository(PingableRole);
}
async all(): Promise<PingableRole[]> {
@ -27,7 +28,7 @@ export class GuildPingableRoles extends BaseGuildRepository {
});
}
async getByChannelAndRoleId(channelId: string, roleId: string): Promise<PingableRole | undefined> {
async getByChannelAndRoleId(channelId: string, roleId: string): Promise<PingableRole | null> {
return this.pingableRoles.findOne({
where: {
guild_id: this.guildId,

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { ReactionRole } from "./entities/ReactionRole";
export class GuildReactionRoles extends BaseGuildRepository {
@ -7,7 +8,7 @@ export class GuildReactionRoles extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.reactionRoles = getRepository(ReactionRole);
this.reactionRoles = dataSource.getRepository(ReactionRole);
}
async all(): Promise<ReactionRole[]> {
@ -30,7 +31,7 @@ export class GuildReactionRoles extends BaseGuildRepository {
});
}
async getByMessageAndEmoji(messageId: string, emoji: string): Promise<ReactionRole | undefined> {
async getByMessageAndEmoji(messageId: string, emoji: string): Promise<ReactionRole | null> {
return this.reactionRoles.findOne({
where: {
guild_id: this.guildId,

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { Reminder } from "./entities/Reminder";
export class GuildReminders extends BaseGuildRepository {
@ -7,7 +8,7 @@ export class GuildReminders extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.reminders = getRepository(Reminder);
this.reminders = dataSource.getRepository(Reminder);
}
async getDueReminders(): Promise<Reminder[]> {
@ -28,7 +29,9 @@ export class GuildReminders extends BaseGuildRepository {
}
find(id: number) {
return this.reminders.findOne({ id });
return this.reminders.findOne({
where: { id },
});
}
async delete(id) {

View file

@ -0,0 +1,38 @@
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { RoleButtonsItem } from "./entities/RoleButtonsItem";
export class GuildRoleButtons extends BaseGuildRepository {
private roleButtons: Repository<RoleButtonsItem>;
constructor(guildId) {
super(guildId);
this.roleButtons = dataSource.getRepository(RoleButtonsItem);
}
getSavedRoleButtons(): Promise<RoleButtonsItem[]> {
return this.roleButtons.find({
where: {
guild_id: this.guildId,
},
});
}
async deleteRoleButtonItem(name: string): Promise<void> {
await this.roleButtons.delete({
guild_id: this.guildId,
name,
});
}
async saveRoleButtonItem(name: string, channelId: string, messageId: string, hash: string): Promise<void> {
await this.roleButtons.insert({
guild_id: this.guildId,
name,
channel_id: channelId,
message_id: messageId,
hash,
});
}
}

View file

@ -0,0 +1,44 @@
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { RoleQueueItem } from "./entities/RoleQueueItem";
export class GuildRoleQueue extends BaseGuildRepository {
private roleQueue: Repository<RoleQueueItem>;
constructor(guildId) {
super(guildId);
this.roleQueue = dataSource.getRepository(RoleQueueItem);
}
consumeNextRoleAssignments(count: number): Promise<RoleQueueItem[]> {
return dataSource.transaction(async (entityManager) => {
const repository = entityManager.getRepository(RoleQueueItem);
const nextAssignments = await repository
.createQueryBuilder()
.where("guild_id = :guildId", { guildId: this.guildId })
.addOrderBy("priority", "DESC")
.addOrderBy("id", "ASC")
.take(count)
.getMany();
if (nextAssignments.length > 0) {
const ids = nextAssignments.map((assignment) => assignment.id);
await repository.createQueryBuilder().where("id IN (:ids)", { ids }).delete().execute();
}
return nextAssignments;
});
}
async addQueueItem(userId: string, roleId: string, shouldAdd: boolean, priority = 0) {
await this.roleQueue.insert({
guild_id: this.guildId,
user_id: userId,
role_id: roleId,
should_add: shouldAdd,
priority,
});
}
}

View file

@ -1,15 +1,14 @@
import { GuildChannel, Message } from "discord.js";
import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { Repository } from "typeorm";
import { QueuedEventEmitter } from "../QueuedEventEmitter";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
import { buildEntity } from "./buildEntity";
import { noop } from "../utils";
import { decrypt } from "../utils/crypt";
import { decryptJson, encryptJson } from "../utils/cryptHelpers";
import { asyncMap } from "../utils/async";
import { decryptJson, encryptJson } from "../utils/cryptHelpers";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { buildEntity } from "./buildEntity";
import { dataSource } from "./dataSource";
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
private messages: Repository<SavedMessage>;
@ -19,7 +18,7 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
constructor(guildId) {
super(guildId);
this.messages = getRepository(SavedMessage);
this.messages = dataSource.getRepository(SavedMessage);
this.events = new QueuedEventEmitter();
this.toBePermanent = new Set();
@ -53,13 +52,13 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
title: embed.title,
description: embed.description,
url: embed.url,
timestamp: embed.timestamp,
timestamp: embed.timestamp ? Date.parse(embed.timestamp) : null,
color: embed.color,
fields: embed.fields.map((field) => ({
name: field.name,
value: field.value,
inline: field.inline,
inline: field.inline ?? false,
})),
author: embed.author
@ -128,7 +127,7 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
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;
}
@ -139,7 +138,7 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
return entity;
}
async find(id: string, includeDeleted = false): Promise<SavedMessage | undefined> {
async find(id: string, includeDeleted = false): Promise<SavedMessage | null> {
let query = this.messages
.createQueryBuilder()
.where("guild_id = :guild_id", { guild_id: this.guildId })

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { ScheduledPost } from "./entities/ScheduledPost";
export class GuildScheduledPosts extends BaseGuildRepository {
@ -7,7 +8,7 @@ export class GuildScheduledPosts extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.scheduledPosts = getRepository(ScheduledPost);
this.scheduledPosts = dataSource.getRepository(ScheduledPost);
}
all(): Promise<ScheduledPost[]> {
@ -23,7 +24,11 @@ export class GuildScheduledPosts extends BaseGuildRepository {
}
find(id: number) {
return this.scheduledPosts.findOne({ id });
return this.scheduledPosts.findOne({
where: {
id,
},
});
}
async delete(id) {

View file

@ -1,6 +1,7 @@
import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { SlowmodeChannel } from "./entities/SlowmodeChannel";
import { SlowmodeUser } from "./entities/SlowmodeUser";
@ -10,11 +11,11 @@ export class GuildSlowmodes extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.slowmodeChannels = getRepository(SlowmodeChannel);
this.slowmodeUsers = getRepository(SlowmodeUser);
this.slowmodeChannels = dataSource.getRepository(SlowmodeChannel);
this.slowmodeUsers = dataSource.getRepository(SlowmodeUser);
}
async getChannelSlowmode(channelId): Promise<SlowmodeChannel | undefined> {
async getChannelSlowmode(channelId): Promise<SlowmodeChannel | null> {
return this.slowmodeChannels.findOne({
where: {
guild_id: this.guildId,
@ -51,11 +52,13 @@ export class GuildSlowmodes extends BaseGuildRepository {
});
}
async getChannelSlowmodeUser(channelId, userId): Promise<SlowmodeUser | undefined> {
async getChannelSlowmodeUser(channelId, userId): Promise<SlowmodeUser | null> {
return this.slowmodeUsers.findOne({
guild_id: this.guildId,
channel_id: channelId,
user_id: userId,
where: {
guild_id: this.guildId,
channel_id: channelId,
user_id: userId,
},
});
}

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { StarboardMessage } from "./entities/StarboardMessage";
export class GuildStarboardMessages extends BaseGuildRepository {
@ -7,7 +8,7 @@ export class GuildStarboardMessages extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.allStarboardMessages = getRepository(StarboardMessage);
this.allStarboardMessages = dataSource.getRepository(StarboardMessage);
}
async getStarboardMessagesForMessageId(messageId: string) {

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { StarboardReaction } from "./entities/StarboardReaction";
export class GuildStarboardReactions extends BaseGuildRepository {
@ -7,7 +8,7 @@ export class GuildStarboardReactions extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.allStarboardReactions = getRepository(StarboardReaction);
this.allStarboardReactions = dataSource.getRepository(StarboardReaction);
}
async getAllReactionsForMessageId(messageId: string) {

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { StatValue } from "./entities/StatValue";
export class GuildStats extends BaseGuildRepository {
@ -7,7 +8,7 @@ export class GuildStats extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.stats = getRepository(StatValue);
this.stats = dataSource.getRepository(StatValue);
}
async saveValue(source: string, key: string, value: number): Promise<void> {

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { Tag } from "./entities/Tag";
import { TagResponse } from "./entities/TagResponse";
@ -9,8 +10,8 @@ export class GuildTags extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.tags = getRepository(Tag);
this.tagResponses = getRepository(TagResponse);
this.tags = dataSource.getRepository(Tag);
this.tagResponses = dataSource.getRepository(TagResponse);
}
async all(): Promise<Tag[]> {
@ -21,7 +22,7 @@ export class GuildTags extends BaseGuildRepository {
});
}
async find(tag): Promise<Tag | undefined> {
async find(tag): Promise<Tag | null> {
return this.tags.findOne({
where: {
guild_id: this.guildId,
@ -61,7 +62,7 @@ export class GuildTags extends BaseGuildRepository {
});
}
async findResponseByCommandMessageId(messageId: string): Promise<TagResponse | undefined> {
async findResponseByCommandMessageId(messageId: string): Promise<TagResponse | null> {
return this.tagResponses.findOne({
where: {
guild_id: this.guildId,
@ -70,7 +71,7 @@ export class GuildTags extends BaseGuildRepository {
});
}
async findResponseByResponseMessageId(messageId: string): Promise<TagResponse | undefined> {
async findResponseByResponseMessageId(messageId: string): Promise<TagResponse | null> {
return this.tagResponses.findOne({
where: {
guild_id: this.guildId,

View file

@ -1,6 +1,7 @@
import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { Tempban } from "./entities/Tempban";
export class GuildTempbans extends BaseGuildRepository {
@ -8,7 +9,7 @@ export class GuildTempbans extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.tempbans = getRepository(Tempban);
this.tempbans = dataSource.getRepository(Tempban);
}
async getExpiredTempbans(): Promise<Tempban[]> {
@ -20,7 +21,7 @@ export class GuildTempbans extends BaseGuildRepository {
.getMany();
}
async findExistingTempbanForUserId(userId: string): Promise<Tempban | undefined> {
async findExistingTempbanForUserId(userId: string): Promise<Tempban | null> {
return this.tempbans.findOne({
where: {
guild_id: this.guildId,

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource";
import { VCAlert } from "./entities/VCAlert";
export class GuildVCAlerts extends BaseGuildRepository {
@ -7,7 +8,7 @@ export class GuildVCAlerts extends BaseGuildRepository {
constructor(guildId) {
super(guildId);
this.allAlerts = getRepository(VCAlert);
this.allAlerts = dataSource.getRepository(VCAlert);
}
async getOutdatedAlerts(): Promise<VCAlert[]> {
@ -41,7 +42,9 @@ export class GuildVCAlerts extends BaseGuildRepository {
}
find(id: number) {
return this.allAlerts.findOne({ id });
return this.allAlerts.findOne({
where: { id },
});
}
async delete(id) {

View file

@ -0,0 +1,30 @@
import moment from "moment-timezone";
import { Repository } from "typeorm";
import { DAYS } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { dataSource } from "./dataSource";
import { MemberCacheItem } from "./entities/MemberCacheItem";
const STALE_PERIOD = 90 * DAYS;
export class MemberCache extends BaseRepository {
#memberCache: Repository<MemberCacheItem>;
constructor() {
super();
this.#memberCache = dataSource.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();
}
}

View file

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

View file

@ -1,20 +1,35 @@
import moment from "moment-timezone";
import { Brackets, getRepository, Repository } from "typeorm";
import { Mute } from "./entities/Mute";
import { Repository } from "typeorm";
import { DAYS, DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { MuteTypes } from "./MuteTypes";
import { dataSource } from "./dataSource";
import { Mute } from "./entities/Mute";
const OLD_EXPIRED_MUTE_THRESHOLD = 7 * DAYS;
export const MAX_TIMEOUT_DURATION = 27 * 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 {
private mutes: Repository<Mute>;
constructor() {
super();
this.mutes = getRepository(Mute);
this.mutes = dataSource.getRepository(Mute);
}
async getSoonExpiringMutes(threshold: number): Promise<Mute[]> {
findMute(guildId: string, userId: string): Promise<Mute | null> {
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);
return this.mutes
.createQueryBuilder("mutes")
@ -23,6 +38,16 @@ export class Mutes extends BaseRepository {
.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> {
const thresholdDateStr = moment.utc().subtract(OLD_EXPIRED_MUTE_THRESHOLD, "ms").format(DBDateFormat);
await this.mutes

View file

@ -1,14 +1,15 @@
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 moment from "moment-timezone";
import { Repository } from "typeorm";
import { env } from "../env";
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
import { dataSource } from "./dataSource";
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry";
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman";
const API_URL = "https://api.phisherman.gg";
const MASTER_API_KEY = process.env.PHISHERMAN_API_KEY;
const MASTER_API_KEY = env.PHISHERMAN_API_KEY;
let caughtDomainTrackingMap: Map<string, Map<string, number[]>> = new Map();
@ -39,7 +40,7 @@ const KEY_VALIDITY_LIFETIME = 24 * HOURS;
let cacheRepository: Repository<PhishermanCacheEntry> | null = null;
function getCacheRepository(): Repository<PhishermanCacheEntry> {
if (cacheRepository == null) {
cacheRepository = getRepository(PhishermanCacheEntry);
cacheRepository = dataSource.getRepository(PhishermanCacheEntry);
}
return cacheRepository;
}
@ -47,7 +48,7 @@ function getCacheRepository(): Repository<PhishermanCacheEntry> {
let keyCacheRepository: Repository<PhishermanKeyCacheEntry> | null = null;
function getKeyCacheRepository(): Repository<PhishermanKeyCacheEntry> {
if (keyCacheRepository == null) {
keyCacheRepository = getRepository(PhishermanKeyCacheEntry);
keyCacheRepository = dataSource.getRepository(PhishermanKeyCacheEntry);
}
return keyCacheRepository;
}
@ -153,7 +154,9 @@ export async function getPhishermanDomainInfo(domain: string): Promise<Phisherma
}
const dbCache = getCacheRepository();
const existingCachedEntry = await dbCache.findOne({ domain });
const existingCachedEntry = await dbCache.findOne({
where: { domain },
});
if (existingCachedEntry) {
return existingCachedEntry.data;
}
@ -196,7 +199,9 @@ export async function phishermanApiKeyIsValid(apiKey: string): Promise<boolean>
const keyCache = getKeyCacheRepository();
const hash = crypto.createHash("sha256").update(apiKey).digest("hex");
const entry = await keyCache.findOne({ hash });
const entry = await keyCache.findOne({
where: { hash },
});
if (entry) {
return entry.is_valid;
}

View file

@ -1,15 +1,16 @@
import { getRepository, Repository } from "typeorm";
import { Reminder } from "./entities/Reminder";
import { BaseRepository } from "./BaseRepository";
import moment from "moment-timezone";
import { Repository } from "typeorm";
import { DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { dataSource } from "./dataSource";
import { Reminder } from "./entities/Reminder";
export class Reminders extends BaseRepository {
private reminders: Repository<Reminder>;
constructor() {
super();
this.reminders = getRepository(Reminder);
this.reminders = dataSource.getRepository(Reminder);
}
async getRemindersDueSoon(threshold: number): Promise<Reminder[]> {

View file

@ -1,15 +1,16 @@
import { getRepository, Repository } from "typeorm";
import { ScheduledPost } from "./entities/ScheduledPost";
import { BaseRepository } from "./BaseRepository";
import moment from "moment-timezone";
import { Repository } from "typeorm";
import { DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { dataSource } from "./dataSource";
import { ScheduledPost } from "./entities/ScheduledPost";
export class ScheduledPosts extends BaseRepository {
private scheduledPosts: Repository<ScheduledPost>;
constructor() {
super();
this.scheduledPosts = getRepository(ScheduledPost);
this.scheduledPosts = dataSource.getRepository(ScheduledPost);
}
getScheduledPostsDueSoon(threshold: number): Promise<ScheduledPost[]> {

View file

@ -1,5 +1,6 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { BaseRepository } from "./BaseRepository";
import { dataSource } from "./dataSource";
import { Supporter } from "./entities/Supporter";
export class Supporters extends BaseRepository {
@ -7,7 +8,7 @@ export class Supporters extends BaseRepository {
constructor() {
super();
this.supporters = getRepository(Supporter);
this.supporters = dataSource.getRepository(Supporter);
}
getAll() {

View file

@ -1,15 +1,16 @@
import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm";
import { Tempban } from "./entities/Tempban";
import { BaseRepository } from "./BaseRepository";
import { Repository } from "typeorm";
import { DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { dataSource } from "./dataSource";
import { Tempban } from "./entities/Tempban";
export class Tempbans extends BaseRepository {
private tempbans: Repository<Tempban>;
constructor() {
super();
this.tempbans = getRepository(Tempban);
this.tempbans = dataSource.getRepository(Tempban);
}
getSoonExpiringTempbans(threshold: number): Promise<Tempban[]> {

View file

@ -1,19 +1,21 @@
import { getRepository, In, Repository } from "typeorm";
import { In, Repository } from "typeorm";
import { isAPI } from "../globals";
import { MINUTES, SECONDS } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { cleanupUsernames } from "./cleanup/usernames";
import { dataSource } from "./dataSource";
import { UsernameHistoryEntry } from "./entities/UsernameHistoryEntry";
const CLEANUP_INTERVAL = 5 * MINUTES;
async function cleanup() {
await cleanupUsernames();
setTimeout(cleanup, CLEANUP_INTERVAL);
}
if (!isAPI()) {
const CLEANUP_INTERVAL = 5 * MINUTES;
async function cleanup() {
await cleanupUsernames();
setTimeout(cleanup, CLEANUP_INTERVAL);
}
// Start first cleanup 30 seconds after startup
// TODO: Move to bot startup code
setTimeout(cleanup, 30 * SECONDS);
}
@ -24,7 +26,7 @@ export class UsernameHistory extends BaseRepository {
constructor() {
super();
this.usernameHistory = getRepository(UsernameHistoryEntry);
this.usernameHistory = dataSource.getRepository(UsernameHistoryEntry);
}
async getByUserId(userId): Promise<UsernameHistoryEntry[]> {
@ -39,7 +41,7 @@ export class UsernameHistory extends BaseRepository {
});
}
getLastEntry(userId): Promise<UsernameHistoryEntry | undefined> {
getLastEntry(userId): Promise<UsernameHistoryEntry | null> {
return this.usernameHistory.findOne({
where: {
user_id: userId,

View file

@ -1,15 +1,16 @@
import { getRepository, Repository } from "typeorm";
import { VCAlert } from "./entities/VCAlert";
import { BaseRepository } from "./BaseRepository";
import moment from "moment-timezone";
import { Repository } from "typeorm";
import { DBDateFormat } from "../utils";
import { BaseRepository } from "./BaseRepository";
import { dataSource } from "./dataSource";
import { VCAlert } from "./entities/VCAlert";
export class VCAlerts extends BaseRepository {
private allAlerts: Repository<VCAlert>;
constructor() {
super();
this.allAlerts = getRepository(VCAlert);
this.allAlerts = dataSource.getRepository(VCAlert);
}
async getSoonExpiringAlerts(threshold: number): Promise<VCAlert[]> {

View file

@ -1,10 +1,11 @@
import { getRepository, Repository } from "typeorm";
import { Webhook } from "./entities/Webhook";
import { BaseRepository } from "./BaseRepository";
import { Repository } from "typeorm";
import { decrypt, encrypt } from "../utils/crypt";
import { BaseRepository } from "./BaseRepository";
import { dataSource } from "./dataSource";
import { Webhook } from "./entities/Webhook";
export class Webhooks extends BaseRepository {
repository: Repository<Webhook> = getRepository(Webhook);
repository: Repository<Webhook> = dataSource.getRepository(Webhook);
protected async _processEntityFromDB(entity) {
entity.token = await decrypt(entity.token);

View file

@ -19,8 +19,6 @@ export type RemoveApiPermissionEventData = {
target_id: string;
};
export type EditConfigEventData = {};
export interface AuditLogEventData extends Record<AuditLogEventType, unknown> {
ADD_API_PERMISSION: {
type: ApiPermissionTypes;
@ -41,7 +39,7 @@ export interface AuditLogEventData extends Record<AuditLogEventType, unknown> {
target_id: string;
};
EDIT_CONFIG: {};
EDIT_CONFIG: Record<string, never>;
}
export type AnyAuditLogEventData = AuditLogEventData[AuditLogEventType];

View file

@ -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();
for (const [key, value] of Object.entries(data)) {
instance[key] = value;

View file

@ -1,13 +1,16 @@
import moment from "moment-timezone";
import { getRepository, In } from "typeorm";
import { In } from "typeorm";
import { DBDateFormat } from "../../utils";
import { connection } from "../db";
import { dataSource } from "../dataSource";
import { Config } from "../entities/Config";
const CLEAN_PER_LOOP = 50;
export async function cleanupConfigs() {
const configRepository = getRepository(Config);
const configRepository = dataSource.getRepository(Config);
// FIXME: The query below doesn't work on MySQL 8.0. Pending an update.
return;
let cleaned = 0;
let rows;
@ -15,7 +18,7 @@ export async function cleanupConfigs() {
// >1 month old: 1 config retained per month
const oneMonthCutoff = moment.utc().subtract(30, "days").format(DBDateFormat);
do {
rows = await connection.query(
rows = await dataSource.query(
`
WITH _configs
AS (
@ -53,7 +56,7 @@ export async function cleanupConfigs() {
// >2 weeks old: 1 config retained per day
const twoWeekCutoff = moment.utc().subtract(2, "weeks").format(DBDateFormat);
do {
rows = await connection.query(
rows = await dataSource.query(
`
WITH _configs
AS (

View file

@ -1,7 +1,7 @@
import moment from "moment-timezone";
import { getRepository, In } from "typeorm";
import { In } from "typeorm";
import { DAYS, DBDateFormat, MINUTES, SECONDS, sleep } from "../../utils";
import { connection } from "../db";
import { dataSource } from "../dataSource";
import { SavedMessage } from "../entities/SavedMessage";
/**
@ -16,7 +16,7 @@ const CLEAN_PER_LOOP = 100;
export async function cleanupMessages(): Promise<number> {
let cleaned = 0;
const messagesRepository = getRepository(SavedMessage);
const messagesRepository = dataSource.getRepository(SavedMessage);
const deletedAtThreshold = moment.utc().subtract(DELETED_MESSAGE_RETENTION_PERIOD, "ms").format(DBDateFormat);
const postedAtThreshold = moment.utc().subtract(RETENTION_PERIOD, "ms").format(DBDateFormat);
@ -27,7 +27,7 @@ export async function cleanupMessages(): Promise<number> {
// when a message was being inserted at the same time
let ids: string[];
do {
const deletedMessageRows = await connection.query(
const deletedMessageRows = await dataSource.query(
`
SELECT id
FROM messages
@ -40,7 +40,7 @@ export async function cleanupMessages(): Promise<number> {
[deletedAtThreshold],
);
const oldPostedRows = await connection.query(
const oldPostedRows = await dataSource.query(
`
SELECT id
FROM messages
@ -53,7 +53,7 @@ export async function cleanupMessages(): Promise<number> {
[postedAtThreshold],
);
const oldBotPostedRows = await connection.query(
const oldBotPostedRows = await dataSource.query(
`
SELECT id
FROM messages

View file

@ -1,7 +1,7 @@
import moment from "moment-timezone";
import { getRepository, In } from "typeorm";
import { In } from "typeorm";
import { DAYS, DBDateFormat } from "../../utils";
import { connection } from "../db";
import { dataSource } from "../dataSource";
import { NicknameHistoryEntry } from "../entities/NicknameHistoryEntry";
export const NICKNAME_RETENTION_PERIOD = 30 * DAYS;
@ -10,13 +10,13 @@ const CLEAN_PER_LOOP = 500;
export async function cleanupNicknames(): Promise<number> {
let cleaned = 0;
const nicknameHistoryRepository = getRepository(NicknameHistoryEntry);
const nicknameHistoryRepository = dataSource.getRepository(NicknameHistoryEntry);
const dateThreshold = moment.utc().subtract(NICKNAME_RETENTION_PERIOD, "ms").format(DBDateFormat);
// Clean old nicknames (NICKNAME_RETENTION_PERIOD)
let rows;
do {
rows = await connection.query(
rows = await dataSource.query(
`
SELECT id
FROM nickname_history

View file

@ -1,7 +1,7 @@
import moment from "moment-timezone";
import { getRepository, In } from "typeorm";
import { In } from "typeorm";
import { DAYS, DBDateFormat } from "../../utils";
import { connection } from "../db";
import { dataSource } from "../dataSource";
import { UsernameHistoryEntry } from "../entities/UsernameHistoryEntry";
export const USERNAME_RETENTION_PERIOD = 30 * DAYS;
@ -10,13 +10,13 @@ const CLEAN_PER_LOOP = 500;
export async function cleanupUsernames(): Promise<number> {
let cleaned = 0;
const usernameHistoryRepository = getRepository(UsernameHistoryEntry);
const usernameHistoryRepository = dataSource.getRepository(UsernameHistoryEntry);
const dateThreshold = moment.utc().subtract(USERNAME_RETENTION_PERIOD, "ms").format(DBDateFormat);
// Clean old usernames (USERNAME_RETENTION_PERIOD)
let rows;
do {
rows = await connection.query(
rows = await dataSource.query(
`
SELECT id
FROM username_history

View file

@ -0,0 +1,45 @@
import moment from "moment-timezone";
import path from "path";
import { DataSource } from "typeorm";
import { env } from "../env";
import { backendDir } from "../paths";
moment.tz.setDefault("UTC");
const entities = path.relative(process.cwd(), path.resolve(backendDir, "dist/backend/src/data/entities/*.js"));
const migrations = path.relative(process.cwd(), path.resolve(backendDir, "dist/backend/src/migrations/*.js"));
export const dataSource = new DataSource({
type: "mysql",
host: env.DB_HOST,
port: env.DB_PORT,
username: env.DB_USER,
password: env.DB_PASSWORD,
database: env.DB_DATABASE,
charset: "utf8mb4",
supportBigNumbers: true,
bigNumberStrings: true,
dateStrings: true,
synchronize: false,
connectTimeout: 2000,
logging: ["error", "warn"],
// Entities
entities: [entities],
// Pool options
extra: {
typeCast(field, next) {
if (field.type === "DATETIME") {
const val = field.string();
return val != null ? moment.utc(val).format("YYYY-MM-DD HH:mm:ss") : null;
}
return next();
},
},
// Migrations
migrations: [migrations],
});

View file

@ -1,28 +1,15 @@
import { Connection, createConnection } from "typeorm";
import { SimpleError } from "../SimpleError";
import connectionOptions from "../../ormconfig";
import { QueryLogger } from "./queryLogger";
import { dataSource } from "./dataSource";
let connectionPromise: Promise<Connection>;
export let connection: Connection;
let connectionPromise: Promise<void>;
export function connect() {
if (!connectionPromise) {
connectionPromise = createConnection({
...(connectionOptions as any),
logging: ["query", "error"],
logger: new QueryLogger(),
}).then((newConnection) => {
// Verify the DB timezone is set to UTC
return newConnection.query("SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP) AS tz").then((r) => {
if (r[0].tz !== "00:00:00") {
throw new SimpleError(`Database timezone must be UTC (detected ${r[0].tz})`);
}
connection = newConnection;
return newConnection;
});
connectionPromise = dataSource.initialize().then(async (initializedDataSource) => {
const tzResult = await initializedDataSource.query("SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP) AS tz");
if (tzResult[0].tz !== "00:00:00") {
throw new SimpleError(`Database timezone must be UTC (detected ${tzResult[0].tz})`);
}
});
}

View file

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

View file

@ -19,7 +19,7 @@ export class ApiLogin {
@Column()
expires_at: string;
@ManyToOne((type) => ApiUserInfo, (userInfo) => userInfo.logins)
@ManyToOne(() => ApiUserInfo, (userInfo) => userInfo.logins)
@JoinColumn({ name: "user_id" })
userInfo: ApiUserInfo;
}

View file

@ -1,6 +1,6 @@
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
import { ApiUserInfo } from "./ApiUserInfo";
import { ApiPermissionTypes } from "../ApiPermissionAssignments";
import { ApiUserInfo } from "./ApiUserInfo";
@Entity("api_permissions")
export class ApiPermissionAssignment {
@ -22,7 +22,7 @@ export class ApiPermissionAssignment {
@Column({ type: String, nullable: true })
expires_at: string | null;
@ManyToOne((type) => ApiUserInfo, (userInfo) => userInfo.permissionAssignments)
@ManyToOne(() => ApiUserInfo, (userInfo) => userInfo.permissionAssignments)
@JoinColumn({ name: "target_id" })
userInfo: ApiUserInfo;
}

View file

@ -20,9 +20,9 @@ export class ApiUserInfo {
@Column()
updated_at: string;
@OneToMany((type) => ApiLogin, (login) => login.userInfo)
@OneToMany(() => ApiLogin, (login) => login.userInfo)
logins: ApiLogin[];
@OneToMany((type) => ApiPermissionAssignment, (p) => p.userInfo)
@OneToMany(() => ApiPermissionAssignment, (p) => p.userInfo)
permissionAssignments: ApiPermissionAssignment[];
}

View file

@ -35,6 +35,6 @@ export class Case {
*/
@Column({ type: String, nullable: true }) log_message_id: string | null;
@OneToMany((type) => CaseNote, (note) => note.case)
@OneToMany(() => CaseNote, (note) => note.case)
notes: CaseNote[];
}

View file

@ -15,7 +15,7 @@ export class CaseNote {
@Column() created_at: string;
@ManyToOne((type) => Case, (theCase) => theCase.notes)
@ManyToOne(() => Case, (theCase) => theCase.notes)
@JoinColumn({ name: "case_id" })
case: Case;
}

View file

@ -22,7 +22,7 @@ export class Config {
@Column()
edited_at: string;
@ManyToOne((type) => ApiUserInfo)
@ManyToOne(() => ApiUserInfo)
@JoinColumn({ name: "edited_by" })
userInfo: ApiUserInfo;
}

View file

@ -2,7 +2,7 @@ import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
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> = {
"=": "!=",

View 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;
}

View file

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

View file

@ -14,5 +14,5 @@ export class PersistedData {
@Column() nickname: string;
@Column() is_voice_muted: number;
@Column({ type: "boolean" }) is_voice_muted: boolean;
}

View file

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

View file

@ -0,0 +1,16 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity("role_buttons")
export class RoleButtonsItem {
@PrimaryGeneratedColumn() id: number;
@Column() guild_id: string;
@Column() name: string;
@Column() channel_id: string;
@Column() message_id: string;
@Column() hash: string;
}

View file

@ -0,0 +1,16 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity("role_queue")
export class RoleQueueItem {
@PrimaryGeneratedColumn() id: number;
@Column() guild_id: string;
@Column() user_id: string;
@Column() role_id: string;
@Column() should_add: boolean;
@Column() priority: number;
}

View file

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

View file

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

View file

@ -16,7 +16,7 @@ export class StarboardMessage {
@Column()
guild_id: string;
@OneToOne((type) => SavedMessage)
@OneToOne(() => SavedMessage)
@JoinColumn({ name: "message_id" })
message: SavedMessage;
}

View file

@ -16,7 +16,7 @@ export class StarboardReaction {
@Column()
reactor_id: string;
@OneToOne((type) => SavedMessage)
@OneToOne(() => SavedMessage)
@JoinColumn({ name: "message_id" })
message: SavedMessage;
}

View file

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

View file

@ -1,14 +1,15 @@
import { getRepository, Repository } from "typeorm";
import { Repository } from "typeorm";
import { dataSource } from "./dataSource";
import { SavedMessage } from "./entities/SavedMessage";
let repository: Repository<SavedMessage>;
export async function getChannelIdFromMessageId(messageId: string): Promise<string | null> {
if (!repository) {
repository = getRepository(SavedMessage);
repository = dataSource.getRepository(SavedMessage);
}
const savedMessage = await repository.findOne(messageId);
const savedMessage = await repository.findOne({ where: { id: messageId } });
if (savedMessage) {
return savedMessage.channel_id;
}

View file

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

View 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);
}

View file

@ -1,11 +1,11 @@
// tslint:disable:no-console
import { lazyMemoize, memoize, MINUTES } from "../../utils";
import { Mutes } from "../Mutes";
import Timeout = NodeJS.Timeout;
import moment from "moment-timezone";
import { lazyMemoize, MINUTES, SECONDS } from "../../utils";
import { Mute } from "../entities/Mute";
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
import { Mutes, TIMEOUT_RENEWAL_THRESHOLD } from "../Mutes";
import Timeout = NodeJS.Timeout;
const LOOP_INTERVAL = 15 * MINUTES;
const MAX_TRIES_PER_SERVER = 3;
@ -16,14 +16,24 @@ function muteToKey(mute: Mute) {
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}`);
if (!hasGuildEventListener(mute.guild_id, "expiredMute")) {
// If there are no listeners registered for the server yet, try again in a bit
if (tries < MAX_TRIES_PER_SERVER) {
timeouts.set(
muteToKey(mute),
setTimeout(() => broadcastExpiredMute(mute, tries + 1), 1 * MINUTES),
setTimeout(() => broadcastExpiredMute(guildId, userId, tries + 1), 1 * MINUTES),
);
}
return;
@ -31,6 +41,21 @@ function broadcastExpiredMute(mute: Mute, tries = 0) {
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() {
console.log("[EXPIRING MUTES LOOP] Clearing old timeouts");
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()));
timeouts.set(
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");
setTimeout(() => runExpiringMutesLoop(), LOOP_INTERVAL);
}
@ -69,7 +100,7 @@ export function registerExpiringMute(mute: Mute) {
timeouts.set(
muteToKey(mute),
setTimeout(() => broadcastExpiredMute(mute), remaining),
setTimeout(() => broadcastExpiredMute(mute.guild_id, mute.user_id), remaining),
);
}

View file

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

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