3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-18 15:00:00 +00:00

Merge remote-tracking branch 'upstream/master' into defaultreason

This commit is contained in:
iamshoXy 2024-04-19 15:24:27 +02:00
commit 7be9274610
352 changed files with 24828 additions and 29302 deletions

View file

@ -23,12 +23,15 @@
"iamshoXy", "iamshoXy",
"Scraayp", "Scraayp",
"app/dependabot", "app/dependabot",
"dependabot[bot]",
"zayKenyon", "zayKenyon",
"rukogit", "rukogit",
"Obliie", "Obliie",
"brawaru", "brawaru",
"Benricheson101", "Benricheson101",
"hawkeye7662" "hawkeye7662",
"LilyBergonzat",
"martinbndr"
], ],
"message": "Thank you for contributing to Zeppelin! We require contributors to sign our Contributor License Agreement (CLA). To let us review and merge your code, please visit https://github.com/ZeppelinBot/CLA to sign the CLA!" "message": "Thank you for contributing to Zeppelin! We require contributors to sign our Contributor License Agreement (CLA). To let us review and merge your code, please visit https://github.com/ZeppelinBot/CLA to sign the CLA!"
} }

View file

@ -5,5 +5,5 @@
"service": "devenv", "service": "devenv",
"remoteUser": "ubuntu", "remoteUser": "ubuntu",
"workspaceFolder": "/home/ubuntu/zeppelin" "workspaceFolder": "/workspace/zeppelin"
} }

View file

@ -1,11 +1,44 @@
.git **/.git
.github **/.github
.idea **/.idea
.devcontainer **/.devcontainer
/docker/development/data /docker/development/data
/docker/production/data /docker/production/data
node_modules **/node_modules
/backend/dist **/dist
/dashboard/dist
**/*.log
**/npm-debug.log*
**/yarn-debug.log*
**/yarn-error.log*
**/.clinic
**/.clinic-bot
**/.clinic-api
# dotenv environment variables file
**/*.env
**/.env
# windows folder options
**/desktop.ini
# PHPStorm
**/.idea
# Misc
**/npm-ls.txt
**/npm-audit.txt
**/.cache
# Debug files
**/*.debug.ts
**/*.debug.js
**/.vscode
config-errors.txt
/config-schema.json
**/*.tsbuildinfo

View file

@ -1,3 +1,7 @@
# ==========================
# GENERAL OPTIONS
# ==========================
# 32 character encryption key # 32 character encryption key
KEY= KEY=
@ -17,58 +21,64 @@ STAFF=
# A comma-separated list of server IDs that should be allowed by default # A comma-separated list of server IDs that should be allowed by default
DEFAULT_ALLOWED_SERVERS= DEFAULT_ALLOWED_SERVERS=
# When using the Docker-based development environment, this is only used internally. The API will be available at localhost:DOCKER_DEV_WEB_PORT/api.
API_PORT=3000
# Only required if relevant feature is used # Only required if relevant feature is used
#PHISHERMAN_API_KEY= #PHISHERMAN_API_KEY=
# The user ID and group ID that should be used within the Docker containers
# This should match your own user ID and group ID. Run `id -u` and `id -g` to find them.
DOCKER_USER_UID=
DOCKER_USER_GID=
# # ==========================
# DOCKER (DEVELOPMENT) # DEVELOPMENT
# NOTE: You only need to fill in these values for running the development environment. See production config further below. # NOTE: You only need to fill in these values for running the development environment
# # ==========================
DOCKER_DEV_WEB_PORT=3300 DEVELOPMENT_WEB_PORT=3300
# The MySQL database running in the container is exposed to the host on this port, # The MySQL database running in the container is exposed to the host on this port,
# allowing access with database tools such as DBeaver # allowing access with database tools such as DBeaver
DOCKER_DEV_MYSQL_PORT=3001 DEVELOPMENT_MYSQL_PORT=3356
# Password for the Zeppelin database user # Password for the Zeppelin database user
DOCKER_DEV_MYSQL_PASSWORD= DEVELOPMENT_MYSQL_PASSWORD=password
# Password for the MySQL root user # Password for the MySQL root user
DOCKER_DEV_MYSQL_ROOT_PASSWORD= DEVELOPMENT_MYSQL_ROOT_PASSWORD=password
# The development environment container has an SSH server that you can connect to. # The development environment container has an SSH server that you can connect to.
# This is the port that server is exposed to the host on. # This is the port that server is exposed to the host on.
DOCKER_DEV_SSH_PORT=3002 DEVELOPMENT_SSH_PORT=3022
DOCKER_DEV_SSH_PASSWORD=password DEVELOPMENT_SSH_PASSWORD=password
# If your user has a different UID than 1000, you might have to fill that in here to avoid permission issues # If your user has a different UID than 1000, you might have to fill that in here to avoid permission issues
#DOCKER_DEV_UID=1000 #DEVELOPMENT_UID=1000
#
# DOCKER (PRODUCTION)
# NOTE: You only need to fill in these values for running the production environment. See development config above.
#
DOCKER_PROD_DOMAIN= # ==========================
DOCKER_PROD_WEB_PORT=443 # PRODUCTION - STANDALONE
# NOTE: You only need to fill in these values for running the standalone production environment
# ==========================
STANDALONE_WEB_PORT=80
# The MySQL database running in the container is exposed to the host on this port, # The MySQL database running in the container is exposed to the host on this port,
# allowing access with database tools such as DBeaver # allowing access with database tools such as DBeaver
DOCKER_PROD_MYSQL_PORT=3001 STANDALONE_MYSQL_PORT=3356
# Password for the Zeppelin database user # Password for the Zeppelin database user
DOCKER_PROD_MYSQL_PASSWORD= STANDALONE_MYSQL_PASSWORD=
# Password for the MySQL root user # Password for the MySQL root user
DOCKER_PROD_MYSQL_ROOT_PASSWORD= STANDALONE_MYSQL_ROOT_PASSWORD=
# You only need to set these if you're running an external database.
# In a standard setup, the database is run in a docker container. # ==========================
#DB_HOST= # PRODUCTION - LIGHTWEIGHT
#DB_USER= # NOTE: You only need to fill in these values for running the lightweight production environment
#DB_PASSWORD= # ==========================
#DB_DATABASE=
# Ports where the API/dashboard are exposed on the host
LIGHTWEIGHT_API_PORT=3001
LIGHTWEIGHT_DASHBOARD_PORT=3002
LIGHTWEIGHT_DB_HOST=
LIGHTWEIGHT_DB_PORT=
LIGHTWEIGHT_DB_USER=
LIGHTWEIGHT_DB_PASSWORD=
LIGHTWEIGHT_DB_DATABASE=
# If you want to add a prefix to API paths, such as /api, you can set that here
LIGHTWEIGHT_API_PATH_PREFIX=

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
package-lock.json binary

9
.gitignore vendored
View file

@ -82,3 +82,12 @@ npm-audit.txt
*.debug.js *.debug.js
.vscode/ .vscode/
config-errors.txt
/config-schema.json
*.tsbuildinfo
# Legacy data folders
/docker/development/data
/docker/production/data

View file

@ -1,71 +1 @@
# Zeppelin development environment Moved to [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)
Zeppelin's development environment runs entirely within a Docker container.
Below you can find instructions for setting up the environment and getting started with development!
**Note:** If you'd just like to run the bot for your own server, see 👉 **[PRODUCTION.md](./PRODUCTION.md)** 👈
## Starting the development environment
### Using VSCode devcontainers
1. Install Docker
2. Make a copy of `.env.example` called `.env`
3. Fill in the missing values in `.env`
4. In VSCode: Install the `Remote - Containers` plugin
5. In VSCode: Run `Remote-Containers: Open Folder in Container...` and select the Zeppelin folder
### Using VSCode remote SSH plugin
1. Install Docker
2. Make a copy of `.env.example` called `.env`
3. Fill in the missing values in `.env`
4. Run `docker compose -f docker-compose.development.yml up` to start the development environment
5. In VSCode: Install the `Remote - SSH` plugin
6. In VSCode: Run `Remote-SSH: Connect to Host...`
* As the address, use `ubuntu@127.0.0.1:3002` (where `3002` matches `DOCKER_DEV_SSH_PORT` in `.env`)
* Use the password specified in `.env` as `DOCKER_DEV_SSH_PASSWORD`
7. In VSCode: Once connected, click `Open folder...` and select `/home/ubuntu/zeppelin`
### Using JetBrains Gateway
1. Install Docker
2. Make a copy of `.env.example` called `.env`
3. Fill in the missing values in `.env`
4. Run `docker compose -f docker-compose.development.yml up` to start the development environment
5. Choose `Connect via SSH` and create a new connection:
* Username: `ubuntu`
* Host: `127.0.0.1`
* Port: `3002` (matching the `DOCKER_DEV_SSH_PORT` value in `.env`)
6. Click `Check Connection and Continue` and enter the password specified in `.env` as `DOCKER_DEV_SSH_PASSWORD` when asked
7. In the next pane:
* IDE version: WebStorm, PHPStorm, or IntelliJ IDEA
* Project directory: `/home/ubuntu/zeppelin`
8. Click `Download and Start IDE`
### Using any other IDE with SSH development support
1. Install Docker
2. Make a copy of `.env.example` called `.env`
3. Fill in the missing values in `.env`
4. Run `docker compose -f docker-compose.development.yml up` to start the development environment
5. Use the following credentials for connecting with your IDE:
* Host: `127.0.0.1`
* Port: `3002` (matching the `DOCKER_DEV_SSH_PORT` value in `.env`)
* Username: `ubuntu`
* Password: As specified in `.env` as `DOCKER_DEV_SSH_PASSWORD`
## Starting the project
### Starting the backend (bot + api)
These commands are run inside the dev container. You should be able to open a terminal in your IDE after connecting.
1. `cd ~/zeppelin/backend`
2. `npm ci`
3. `npm run migrate-dev`
4. `npm run watch`
### Starting the dashboard
These commands are run inside the dev container. You should be able to open a terminal in your IDE after connecting.
1. `cd ~/zeppelin/dashboard`
2. `npm ci`
3. `npm run watch-build`
### Opening the dashboard
Browse to https://localhost:3300 to view the dashboard

34
Dockerfile Normal file
View file

@ -0,0 +1,34 @@
FROM node:20
RUN mkdir /zeppelin
RUN chown node:node /zeppelin
USER node
ARG API_URL
# Install dependencies before copying over any other files
COPY --chown=node:node package.json package-lock.json /zeppelin
RUN mkdir /zeppelin/backend
COPY --chown=node:node backend/package.json /zeppelin/backend
RUN mkdir /zeppelin/shared
COPY --chown=node:node shared/package.json /zeppelin/shared
RUN mkdir /zeppelin/dashboard
COPY --chown=node:node dashboard/package.json /zeppelin/dashboard
WORKDIR /zeppelin
RUN npm ci
COPY --chown=node:node . /zeppelin
# Build backend
WORKDIR /zeppelin/backend
RUN npm run build
# Build dashboard
WORKDIR /zeppelin/dashboard
RUN npm run build
# Prune dev dependencies
WORKDIR /zeppelin
RUN npm prune --omit=dev

View file

@ -1,34 +1 @@
# Management Moved to [docs/MANAGEMENT.md](docs/MANAGEMENT.md)
After starting Zeppelin -- either in the [development](./DEVELOPMENT.md) or [production](./PRODUCTION.md) environment -- you have several tools available to manage it.
## Note
Make sure to add yourself to the list of staff members (`STAFF`) in `.env` and allow at least one server by default (`DEFAULT_ALLOWED_SERVERS`). Then, invite the bot to the server.
In all examples below, `@Bot` refers to a user mention of the bot user. Make sure to run the commands on a server with the bot, in a channel that the bot can see.
In the command parameters, `<this>` refers to a required parameter (don't include the `< >` symbols) and `[this]` refers to an optional parameter (don't include the `[ ]` symbols). `<this...>` refers to being able to list multiple values, e.g. `value1 value2 value3`.
## Allow a server to invite the bot
Run the following command:
```
@Bot allow_server <serverId> [userId]
```
When specifying a user ID, that user will be given "Bot manager" level access to the server's dashboard, allowing them to manage access for other users.
## Disallow a server
Run the following command:
```
@Bot disallow_server <serverId>
```
## Grant access to a server's dashboard
Run the following command:
```
@Bot add_dashboard_user <serverId> <userId...>
```
## Remove access to a server's dashboard
Run the following command:
```
@Bot remove_dashboard_user <serverId> <userId...>
```

View file

@ -1,34 +1 @@
# Zeppelin production environment Moved to [docs/PRODUCTION.md](docs/PRODUCTION.md)
Zeppelin's production environment - that is, the **bot, API, and dashboard** - uses Docker.
## Starting the production environment
1. Install Docker on the machine running the bot
2. Make a copy of `.env.example` called `.env`
3. Fill in the missing values in `.env`
4. Run `docker compose -f docker-compose.production.yml build`
5. Run `docker compose -f docker-compose.production.yml up -d`
**Note:** The dashboard and API are exposed with a self-signed certificate. It is recommended to set up a proxy with a proper certificate in front of them. Cloudflare is a popular choice here.
## Updating the bot
### One-click script
If you've downloaded the bot's files by cloning the git repository, you can use `update.sh` to update the bot.
### Manual instructions
1. Shut the bot down: `docker compose -f docker-compose.production.yml down`
2. Update the files (e.g. `git pull`)
3. Build new images: `docker compose -f docker-compose.production.yml build`
3. Start the bot again: `docker compose -f docker-compose.production.yml up -d`
### Ephemeral hotfixes
If you need to make a hotfix to the bot's source files directly on the server:
1. Shut the bot down: `docker compose -f docker-compose.production.yml down`
2. Make your edits
3. Build new images: `docker compose -f docker-compose.production.yml build`
4. Start the bot again: `docker compose -f docker-compose.production.yml up -d`
Make sure to revert any hotfixes before updating the bot normally.
## View logs
To view real-time logs, run `docker compose -f docker-compose.production.yml logs -t -f`

View file

@ -23,11 +23,11 @@ See https://zeppelin.gg/ for more details.
For information on how to use the bot, see https://zeppelin.gg/docs For information on how to use the bot, see https://zeppelin.gg/docs
## Development ## Development
See [DEVELOPMENT.md](./DEVELOPMENT.md) for instructions on running the development environment. See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for instructions on running the development environment.
Once you have the environment up and running, see [MANAGEMENT.md](./MANAGEMENT.md) for how to manage your bot. Once you have the environment up and running, see [docs/MANAGEMENT.md](docs/MANAGEMENT.md) for how to manage your bot.
## Production ## Production
See [PRODUCTION.md](./PRODUCTION.md) for instructions on how to run the bot in production. See [docs/PRODUCTION.md](docs/PRODUCTION.md) for instructions on how to run the bot in production.
Once you have the environment up and running, see [MANAGEMENT.md](./MANAGEMENT.md) for how to manage your bot. Once you have the environment up and running, see [docs/MANAGEMENT.md](docs/MANAGEMENT.md) for how to manage your bot.

10212
backend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,32 +1,34 @@
{ {
"name": "@zeppelin/backend", "name": "@zeppelinbot/backend",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"private": true, "private": true,
"scripts": { "scripts": {
"watch": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node start-dev.js\"", "watch": "tsc-watch --build --onSuccess \"node start-dev.js\"",
"watch-yaml-parse-test": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node dist/backend/src/yamlParseTest.js\"", "watch-yaml-parse-test": "tsc-watch --build --onSuccess \"node dist/yamlParseTest.js\"",
"build": "rimraf dist && tsc", "build": "tsc --build",
"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": "node --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/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-dev-debug": "DEBUG=true clinic heapprofiler --collect-only --dest .clinic-bot -- node --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/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": "node --enable-source-maps --stack-trace-limit=30 dist/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", "start-bot-prod-debug": "DEBUG=true clinic heapprofiler --collect-only --dest .clinic-bot -- node --enable-source-maps --stack-trace-limit=30 dist/index.js",
"watch-bot": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-bot-dev\"", "watch-bot": "tsc-watch --build --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": "node --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/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-dev-debug": "DEBUG=true clinic heapprofiler --collect-only --dest .clinic-api -- node --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/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": "node --enable-source-maps --stack-trace-limit=30 dist/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", "start-api-prod-debug": "clinic heapprofiler --collect-only --dest .clinic-api -- node --enable-source-maps --stack-trace-limit=30 dist/api/index.js",
"watch-api": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-api-dev\"", "watch-api": "tsc-watch --build --onSuccess \"npm run start-api-dev\"",
"typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js", "typeorm": "node ../node_modules/typeorm/cli.js",
"migrate": "npm run typeorm -- migration:run -d dist/backend/src/data/dataSource.js", "migrate": "npm run typeorm -- migration:run -d dist/data/dataSource.js",
"migrate-prod": "cross-env NODE_ENV=production npm run migrate", "migrate-prod": "npm run migrate",
"migrate-dev": "cross-env NODE_ENV=development npm run build && npm run migrate", "migrate-dev": "npm run build && npm run migrate",
"migrate-rollback": "npm run typeorm -- migration:revert -d dist/backend/src/data/dataSource.js", "migrate-rollback": "npm run typeorm -- migration:revert -d dist/data/dataSource.js",
"migrate-rollback-prod": "cross-env NODE_ENV=production npm run migrate", "migrate-rollback-prod": "npm run migrate-rollback",
"migrate-rollback-dev": "cross-env NODE_ENV=development npm run build && npm run migrate", "migrate-rollback-dev": "npm run build && npm run migrate-rollback",
"validate-active-configs": "node --enable-source-maps dist/validateActiveConfigs.js > ../config-errors.txt",
"export-config-json-schema": "node --enable-source-maps dist/exportSchemas.js > ../config-schema.json",
"test": "npm run build && npm run run-tests", "test": "npm run build && npm run run-tests",
"run-tests": "ava", "run-tests": "ava",
"test-watch": "tsc-watch --onSuccess \"npx ava\"" "test-watch": "tsc-watch --build --onSuccess \"npx ava\""
}, },
"dependencies": { "dependencies": {
"@silvia-odwyer/photon-node": "^0.3.1", "@silvia-odwyer/photon-node": "^0.3.1",
@ -35,17 +37,15 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"deep-diff": "^1.0.2", "deep-diff": "^1.0.2",
"discord.js": "^14.11.0", "discord.js": "^14.14.1",
"dotenv": "^4.0.0", "dotenv": "^4.0.0",
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
"erlpack": "github:discord/erlpack",
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
"express": "^4.17.0", "express": "^4.17.0",
"fp-ts": "^2.0.1", "fp-ts": "^2.0.1",
"humanize-duration": "^3.15.0", "humanize-duration": "^3.15.0",
"io-ts": "^2.0.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"knub": "^32.0.0-next.16", "knub": "^32.0.0-next.21",
"knub-command-manager": "^9.1.0", "knub-command-manager": "^9.1.0",
"last-commit-log": "^2.1.0", "last-commit-log": "^2.1.0",
"lodash.chunk": "^4.2.0", "lodash.chunk": "^4.2.0",
@ -56,7 +56,7 @@
"lodash.pick": "^4.4.0", "lodash.pick": "^4.4.0",
"moment-timezone": "^0.5.21", "moment-timezone": "^0.5.21",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql": "^2.16.0", "mysql2": "^3.9.3",
"parse-color": "^1.0.0", "parse-color": "^1.0.0",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-custom": "^1.0.5", "passport-custom": "^1.0.5",
@ -97,11 +97,12 @@
"@types/uuid": "^9.0.2", "@types/uuid": "^9.0.2",
"ava": "^5.3.1", "ava": "^5.3.1",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"source-map-support": "^0.5.16" "source-map-support": "^0.5.16",
"zod-to-json-schema": "^3.22.3"
}, },
"ava": { "ava": {
"files": [ "files": [
"dist/backend/src/**/*.test.js" "dist/**/*.test.js"
], ],
"require": [ "require": [
"./register-tsconfig-paths.js" "./register-tsconfig-paths.js"

View file

@ -11,6 +11,7 @@ export enum ERRORS {
MUTE_ROLE_ABOVE_ZEP, MUTE_ROLE_ABOVE_ZEP,
USER_ABOVE_ZEP, USER_ABOVE_ZEP,
USER_NOT_MODERATABLE, USER_NOT_MODERATABLE,
TEMPLATE_PARSE_ERROR,
} }
export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = { export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
@ -24,6 +25,7 @@ export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
[ERRORS.MUTE_ROLE_ABOVE_ZEP]: "Specified mute role is above Zeppelin in the role hierarchy", [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_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", [ERRORS.USER_NOT_MODERATABLE]: "Cannot mute user, specified user is not moderatable",
[ERRORS.TEMPLATE_PARSE_ERROR]: "Template parse error",
}; };
export class RecoverablePluginError extends Error { export class RecoverablePluginError extends Error {

View file

@ -3,15 +3,15 @@ import moment from "moment-timezone";
import { GuildArchives } from "../data/GuildArchives"; import { GuildArchives } from "../data/GuildArchives";
import { notFound } from "./responses"; import { notFound } from "./responses";
export function initArchives(app: express.Express) { export function initArchives(router: express.Router) {
const archives = new GuildArchives(null); const archives = new GuildArchives(null);
// Legacy redirect // Legacy redirect
app.get("/spam-logs/:id", (req: Request, res: Response) => { router.get("/spam-logs/:id", (req: Request, res: Response) => {
res.redirect("/archives/" + req.params.id); res.redirect("/archives/" + req.params.id);
}); });
app.get("/archives/:id", async (req: Request, res: Response) => { router.get("/archives/:id", async (req: Request, res: Response) => {
const archive = await archives.find(req.params.id); const archive = await archives.find(req.params.id);
if (!archive) return notFound(res); if (!archive) return notFound(res);

View file

@ -51,8 +51,8 @@ function simpleDiscordAPIRequest(bearerToken, path): Promise<any> {
}); });
} }
export function initAuth(app: express.Express) { export function initAuth(router: express.Router) {
app.use(passport.initialize()); router.use(passport.initialize());
passport.serializeUser((user, done) => done(null, user)); passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user as IPassportApiUser)); passport.deserializeUser((user, done) => done(null, user as IPassportApiUser));
@ -110,8 +110,8 @@ export function initAuth(app: express.Express) {
), ),
); );
app.get("/auth/login", passport.authenticate("oauth2")); router.get("/auth/login", passport.authenticate("oauth2"));
app.get( router.get(
"/auth/oauth-callback", "/auth/oauth-callback",
passport.authenticate("oauth2", { failureRedirect: "/", session: false }), passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
(req: Request, res: Response) => { (req: Request, res: Response) => {
@ -122,7 +122,7 @@ export function initAuth(app: express.Express) {
} }
}, },
); );
app.post("/auth/validate-key", async (req: Request, res: Response) => { router.post("/auth/validate-key", async (req: Request, res: Response) => {
const key = req.body.key; const key = req.body.key;
if (!key) { if (!key) {
return res.status(400).json({ error: "No key supplied" }); return res.status(400).json({ error: "No key supplied" });
@ -135,14 +135,14 @@ export function initAuth(app: express.Express) {
res.json({ valid: true, userId }); res.json({ valid: true, userId });
}); });
app.post("/auth/logout", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => { router.post("/auth/logout", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
await apiLogins.expireApiKey(req.user!.apiKey); await apiLogins.expireApiKey(req.user!.apiKey);
return ok(res); return ok(res);
}); });
// API route to refresh the given API token's expiry time // API route to refresh the given API token's expiry time
// The actual refreshing happens in the api-token passport strategy above, so we just return 200 OK here // The actual refreshing happens in the api-token passport strategy above, so we just return 200 OK here
app.post("/auth/refresh", ...apiTokenAuthHandlers(), (req, res) => { router.post("/auth/refresh", ...apiTokenAuthHandlers(), (req, res) => {
return ok(res); return ok(res);
}); });
} }

View file

@ -1,60 +1,127 @@
import express from "express"; import express from "express";
import z from "zod";
import { guildPlugins } from "../plugins/availablePlugins"; import { guildPlugins } from "../plugins/availablePlugins";
import { guildPluginInfo } from "../plugins/pluginInfo";
import { indentLines } from "../utils"; import { indentLines } from "../utils";
import { notFound } from "./responses"; import { notFound } from "./responses";
function formatConfigSchema(schema) { function isZodObject(schema: z.ZodTypeAny): schema is z.ZodObject<any> {
if (schema._tag === "InterfaceType" || schema._tag === "PartialType") { return schema._def.typeName === "ZodObject";
}
function isZodRecord(schema: z.ZodTypeAny): schema is z.ZodRecord<any> {
return schema._def.typeName === "ZodRecord";
}
function isZodEffects(schema: z.ZodTypeAny): schema is z.ZodEffects<any, any> {
return schema._def.typeName === "ZodEffects";
}
function isZodOptional(schema: z.ZodTypeAny): schema is z.ZodOptional<any> {
return schema._def.typeName === "ZodOptional";
}
function isZodArray(schema: z.ZodTypeAny): schema is z.ZodArray<any> {
return schema._def.typeName === "ZodArray";
}
function isZodUnion(schema: z.ZodTypeAny): schema is z.ZodUnion<any> {
return schema._def.typeName === "ZodUnion";
}
function isZodNullable(schema: z.ZodTypeAny): schema is z.ZodNullable<any> {
return schema._def.typeName === "ZodNullable";
}
function isZodDefault(schema: z.ZodTypeAny): schema is z.ZodDefault<any> {
return schema._def.typeName === "ZodDefault";
}
function isZodLiteral(schema: z.ZodTypeAny): schema is z.ZodLiteral<any> {
return schema._def.typeName === "ZodLiteral";
}
function isZodIntersection(schema: z.ZodTypeAny): schema is z.ZodIntersection<any, any> {
return schema._def.typeName === "ZodIntersection";
}
function formatZodConfigSchema(schema: z.ZodTypeAny) {
if (isZodObject(schema)) {
return ( return (
`{\n` + `{\n` +
Object.entries(schema.props) Object.entries(schema._def.shape())
.map(([k, value]) => indentLines(`${k}: ${formatConfigSchema(value)}`, 2)) .map(([k, value]) => indentLines(`${k}: ${formatZodConfigSchema(value as z.ZodTypeAny)}`, 2))
.join("\n") + .join("\n") +
"\n}" "\n}"
); );
} else if (schema._tag === "DictionaryType") {
return "{\n" + indentLines(`[string]: ${formatConfigSchema(schema.codomain)}`, 2) + "\n}";
} else if (schema._tag === "ArrayType") {
return `Array<${formatConfigSchema(schema.type)}>`;
} else if (schema._tag === "UnionType") {
if (schema.name.startsWith("Nullable<")) {
return `Nullable<${formatConfigSchema(schema.types[0])}>`;
} else if (schema.name.startsWith("Optional<")) {
return `Optional<${formatConfigSchema(schema.types[0])}>`;
} else {
return schema.types.map((t) => formatConfigSchema(t)).join(" | ");
} }
} else if (schema._tag === "IntersectionType") { if (isZodRecord(schema)) {
return schema.types.map((t) => formatConfigSchema(t)).join(" & "); return "{\n" + indentLines(`[string]: ${formatZodConfigSchema(schema._def.valueType)}`, 2) + "\n}";
} else {
return schema.name;
} }
if (isZodEffects(schema)) {
return formatZodConfigSchema(schema._def.schema);
}
if (isZodOptional(schema)) {
return `Optional<${formatZodConfigSchema(schema._def.innerType)}>`;
}
if (isZodArray(schema)) {
return `Array<${formatZodConfigSchema(schema._def.type)}>`;
}
if (isZodUnion(schema)) {
return schema._def.options.map((t) => formatZodConfigSchema(t)).join(" | ");
}
if (isZodNullable(schema)) {
return `Nullable<${formatZodConfigSchema(schema._def.innerType)}>`;
}
if (isZodDefault(schema)) {
return formatZodConfigSchema(schema._def.innerType);
}
if (isZodLiteral(schema)) {
return schema._def.value;
}
if (isZodIntersection(schema)) {
return [formatZodConfigSchema(schema._def.left), formatZodConfigSchema(schema._def.right)].join(" & ");
}
if (schema._def.typeName === "ZodString") {
return "string";
}
if (schema._def.typeName === "ZodNumber") {
return "number";
}
if (schema._def.typeName === "ZodBoolean") {
return "boolean";
}
if (schema._def.typeName === "ZodNever") {
return "never";
}
return "unknown";
} }
export function initDocs(app: express.Express) { export function initDocs(router: express.Router) {
const docsPlugins = guildPlugins.filter((plugin) => plugin.showInDocs); const docsPluginNames = Object.keys(guildPluginInfo).filter((k) => guildPluginInfo[k].showInDocs);
app.get("/docs/plugins", (req: express.Request, res: express.Response) => { router.get("/docs/plugins", (req: express.Request, res: express.Response) => {
res.json( res.json(
docsPlugins.map((plugin) => { docsPluginNames.map((pluginName) => {
const thinInfo = plugin.info ? { prettyName: plugin.info.prettyName, legacy: plugin.info.legacy ?? false } : {}; const info = guildPluginInfo[pluginName];
const thinInfo = info ? { prettyName: info.prettyName, legacy: info.legacy ?? false } : {};
return { return {
name: plugin.name, name: pluginName,
info: thinInfo, info: thinInfo,
}; };
}), }),
); );
}); });
app.get("/docs/plugins/:pluginName", (req: express.Request, res: express.Response) => { router.get("/docs/plugins/:pluginName", (req: express.Request, res: express.Response) => {
// prettier-ignore const name = req.params.pluginName;
const plugin = docsPlugins.find(_plugin => _plugin.name === req.params.pluginName); const baseInfo = guildPluginInfo[name];
if (!plugin) { if (!baseInfo) {
return notFound(res); return notFound(res);
} }
const name = plugin.name; const plugin = guildPlugins.find((p) => p.name === name)!;
const info = { ...(plugin.info || {}) }; const info = { ...baseInfo };
delete info.configSchema; delete info.configSchema;
const messageCommands = (plugin.messageCommands || []).map((cmd) => ({ const messageCommands = (plugin.messageCommands || []).map((cmd) => ({
@ -67,7 +134,7 @@ export function initDocs(app: express.Express) {
})); }));
const defaultOptions = plugin.defaultOptions || {}; const defaultOptions = plugin.defaultOptions || {};
const configSchema = plugin.info?.configSchema && formatConfigSchema(plugin.info.configSchema); const configSchema = info.configSchema && formatZodConfigSchema(info.configSchema);
res.json({ res.json({
name, name,

View file

@ -1,4 +1,4 @@
import { ApiPermissions } from "@shared/apiPermissions"; import { ApiPermissions } from "@zeppelinbot/shared";
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import { YAMLException } from "js-yaml"; import { YAMLException } from "js-yaml";
import moment from "moment-timezone"; import moment from "moment-timezone";

View file

@ -1,4 +1,4 @@
import { ApiPermissions } from "@shared/apiPermissions"; import { ApiPermissions } from "@zeppelinbot/shared";
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { z } from "zod"; import { z } from "zod";

View file

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

View file

@ -1,4 +1,4 @@
import { ApiPermissions } from "@shared/apiPermissions"; import { ApiPermissions } from "@zeppelinbot/shared";
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import { YAMLException } from "js-yaml"; import { YAMLException } from "js-yaml";
import moment from "moment-timezone"; import moment from "moment-timezone";

View file

@ -1,4 +1,4 @@
import { ApiPermissions, hasPermission, permissionArrToSet } from "@shared/apiPermissions"; import { ApiPermissions, hasPermission, permissionArrToSet } from "@zeppelinbot/shared";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments"; import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
import { isStaff } from "../staff"; import { isStaff } from "../staff";

View file

@ -10,6 +10,8 @@ import { initGuildsAPI } from "./guilds/index";
import { clientError, error, notFound } from "./responses"; import { clientError, error, notFound } from "./responses";
import { startBackgroundTasks } from "./tasks"; import { startBackgroundTasks } from "./tasks";
const apiPathPrefix = env.API_PATH_PREFIX || (env.NODE_ENV === "development" ? "/api" : "");
const app = express(); const app = express();
app.use( app.use(
@ -24,16 +26,20 @@ app.use(
); );
app.use(multer().none()); app.use(multer().none());
const rootRouter = express.Router();
initAuth(app); initAuth(app);
initGuildsAPI(app); initGuildsAPI(app);
initArchives(app); initArchives(app);
initDocs(app); initDocs(app);
// Default route // Default route
app.get("/", (req, res) => { rootRouter.get("/", (req, res) => {
res.json({ status: "cookies", with: "milk" }); res.json({ status: "cookies", with: "milk" });
}); });
app.use(apiPathPrefix, rootRouter);
// Error response // Error response
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
@ -51,7 +57,7 @@ app.use((req, res, next) => {
return notFound(res); return notFound(res);
}); });
const port = env.API_PORT; const port = 3001;
app.listen(port, "0.0.0.0", () => console.log(`API server listening on port ${port}`)); // tslint:disable-line app.listen(port, "0.0.0.0", () => console.log(`API server listening on port ${port}`)); // tslint:disable-line
startBackgroundTasks(); startBackgroundTasks();

View file

@ -17,6 +17,7 @@ import { createTypeHelper } from "knub-command-manager";
import { import {
channelMentionRegex, channelMentionRegex,
convertDelayStringToMS, convertDelayStringToMS,
inputPatternToRegExp,
isValidSnowflake, isValidSnowflake,
resolveMember, resolveMember,
resolveUser, resolveUser,
@ -26,7 +27,6 @@ import {
} from "./utils"; } from "./utils";
import { isValidTimezone } from "./utils/isValidTimezone"; import { isValidTimezone } from "./utils/isValidTimezone";
import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarget"; import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarget";
import { inputPatternToRegExp } from "./validatorUtils";
export const commandTypes = { export const commandTypes = {
...messageCommandBaseTypeConverters, ...messageCommandBaseTypeConverters,

View file

@ -1,18 +1,20 @@
import { ConfigValidationError, PluginConfigManager } from "knub"; import { ConfigValidationError, GuildPluginBlueprint, PluginConfigManager } from "knub";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin"; import { ZodError } from "zod";
import { guildPlugins } from "./plugins/availablePlugins"; import { guildPlugins } from "./plugins/availablePlugins";
import { PartialZeppelinGuildConfigSchema, ZeppelinGuildConfig } from "./types"; import { ZeppelinGuildConfig, zZeppelinGuildConfig } from "./types";
import { StrictValidationError, decodeAndValidateStrict } from "./validatorUtils"; import { formatZodIssue } from "./utils/formatZodIssue";
const pluginNameToPlugin = new Map<string, ZeppelinPlugin>(); const pluginNameToPlugin = new Map<string, GuildPluginBlueprint<any, any>>();
for (const plugin of guildPlugins) { for (const plugin of guildPlugins) {
pluginNameToPlugin.set(plugin.name, plugin); pluginNameToPlugin.set(plugin.name, plugin);
} }
export async function validateGuildConfig(config: any): Promise<string | null> { export async function validateGuildConfig(config: any): Promise<string | null> {
const validationResult = decodeAndValidateStrict(PartialZeppelinGuildConfigSchema, config); const validationResult = zZeppelinGuildConfig.safeParse(config);
if (validationResult instanceof StrictValidationError) return validationResult.getErrors(); if (!validationResult.success) {
return validationResult.error.issues.map(formatZodIssue).join("\n");
}
const guildConfig = config as ZeppelinGuildConfig; const guildConfig = config as ZeppelinGuildConfig;
@ -41,7 +43,10 @@ export async function validateGuildConfig(config: any): Promise<string | null> {
try { try {
await configManager.init(); await configManager.init();
} catch (err) { } catch (err) {
if (err instanceof ConfigValidationError || err instanceof StrictValidationError) { if (err instanceof ZodError) {
return `${pluginName}: ${err.issues.map(formatZodIssue).join("\n")}`;
}
if (err instanceof ConfigValidationError) {
return `${pluginName}: ${err.message}`; return `${pluginName}: ${err.message}`;
} }

View file

@ -1,4 +1,4 @@
import { ApiPermissions } from "@shared/apiPermissions"; import { ApiPermissions } from "@zeppelinbot/shared";
import { Repository } from "typeorm"; import { Repository } from "typeorm";
import { ApiAuditLog } from "./ApiAuditLog"; import { ApiAuditLog } from "./ApiAuditLog";
import { BaseRepository } from "./BaseRepository"; import { BaseRepository } from "./BaseRepository";

View file

@ -27,6 +27,12 @@ export class Configs extends BaseRepository {
this.configs = dataSource.getRepository(Config); this.configs = dataSource.getRepository(Config);
} }
getActive() {
return this.configs.find({
where: { is_active: true },
});
}
getActiveByKey(key) { getActiveByKey(key) {
return this.configs.findOne({ return this.configs.findOne({
where: { where: {

View file

@ -1,10 +1,10 @@
import { Guild, Snowflake } from "discord.js"; import { Guild, Snowflake } from "discord.js";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { isDefaultSticker } from "src/utils/isDefaultSticker";
import { Repository } from "typeorm"; import { Repository } from "typeorm";
import { TemplateSafeValueContainer, renderTemplate } from "../templateFormatter"; import { TemplateSafeValueContainer, renderTemplate } from "../templateFormatter";
import { renderUsername, trimLines } from "../utils"; import { renderUsername, trimLines } from "../utils";
import { decrypt, encrypt } from "../utils/crypt"; import { decrypt, encrypt } from "../utils/crypt";
import { isDefaultSticker } from "../utils/isDefaultSticker";
import { channelToTemplateSafeChannel, guildToTemplateSafeGuild } from "../utils/templateSafeObjects"; import { channelToTemplateSafeChannel, guildToTemplateSafeGuild } from "../utils/templateSafeObjects";
import { BaseGuildRepository } from "./BaseGuildRepository"; import { BaseGuildRepository } from "./BaseGuildRepository";
import { dataSource } from "./dataSource"; import { dataSource } from "./dataSource";

View file

@ -12,7 +12,8 @@ import { CounterValue } from "./entities/CounterValue";
const DELETE_UNUSED_COUNTERS_AFTER = 1 * DAYS; const DELETE_UNUSED_COUNTERS_AFTER = 1 * DAYS;
const DELETE_UNUSED_COUNTER_TRIGGERS_AFTER = 1 * DAYS; const DELETE_UNUSED_COUNTER_TRIGGERS_AFTER = 1 * DAYS;
const MAX_COUNTER_VALUE = 2147483647; // 2^31-1, for MySQL INT export const MIN_COUNTER_VALUE = 0;
export const MAX_COUNTER_VALUE = 2147483647; // 2^31-1, for MySQL INT
const decayQueue = new Queue(); const decayQueue = new Queue();
@ -115,7 +116,9 @@ export class GuildCounters extends BaseGuildRepository {
userId = userId || "0"; userId = userId || "0";
const rawUpdate = const rawUpdate =
change >= 0 ? `value = LEAST(value + ${change}, ${MAX_COUNTER_VALUE})` : `value = GREATEST(value ${change}, 0)`; change >= 0
? `value = LEAST(value + ${change}, ${MAX_COUNTER_VALUE})`
: `value = GREATEST(value ${change}, ${MIN_COUNTER_VALUE})`;
await this.counterValues.query( await this.counterValues.query(
` `
@ -173,7 +176,7 @@ export class GuildCounters extends BaseGuildRepository {
const rawUpdate = const rawUpdate =
decayAmountToApply >= 0 decayAmountToApply >= 0
? `GREATEST(value - ${decayAmountToApply}, 0)` ? `GREATEST(value - ${decayAmountToApply}, ${MIN_COUNTER_VALUE})`
: `LEAST(value + ${Math.abs(decayAmountToApply)}, ${MAX_COUNTER_VALUE})`; : `LEAST(value + ${Math.abs(decayAmountToApply)}, ${MAX_COUNTER_VALUE})`;
// Using an UPDATE with ORDER BY in an attempt to avoid deadlocks from simultaneous decays // Using an UPDATE with ORDER BY in an attempt to avoid deadlocks from simultaneous decays

View file

@ -5,7 +5,7 @@ import { LogType } from "./LogType";
const guildInstances: Map<string, GuildLogs> = new Map(); const guildInstances: Map<string, GuildLogs> = new Map();
interface IIgnoredLog { interface IIgnoredLog {
type: LogType; type: keyof typeof LogType;
ignoreId: any; ignoreId: any;
} }
@ -27,7 +27,7 @@ export class GuildLogs extends events.EventEmitter {
guildInstances.set(guildId, this); guildInstances.set(guildId, this);
} }
log(type: LogType, data: any, ignoreId?: string) { log(type: keyof typeof LogType, data: any, ignoreId?: string) {
if (ignoreId && this.isLogIgnored(type, ignoreId)) { if (ignoreId && this.isLogIgnored(type, ignoreId)) {
this.clearIgnoredLog(type, ignoreId); this.clearIgnoredLog(type, ignoreId);
return; return;
@ -36,7 +36,7 @@ export class GuildLogs extends events.EventEmitter {
this.emit("log", { type, data }); this.emit("log", { type, data });
} }
ignoreLog(type: LogType, ignoreId: any, timeout?: number) { ignoreLog(type: keyof typeof LogType, ignoreId: any, timeout?: number) {
this.ignoredLogs.push({ type, ignoreId }); this.ignoredLogs.push({ type, ignoreId });
// Clear after expiry (15sec by default) // Clear after expiry (15sec by default)
@ -45,11 +45,11 @@ export class GuildLogs extends events.EventEmitter {
}, timeout || 1000 * 15); }, timeout || 1000 * 15);
} }
isLogIgnored(type: LogType, ignoreId: any) { isLogIgnored(type: keyof typeof LogType, ignoreId: any) {
return this.ignoredLogs.some((info) => type === info.type && ignoreId === info.ignoreId); return this.ignoredLogs.some((info) => type === info.type && ignoreId === info.ignoreId);
} }
clearIgnoredLog(type: LogType, ignoreId: any) { clearIgnoredLog(type: keyof typeof LogType, ignoreId: any) {
this.ignoredLogs.splice( this.ignoredLogs.splice(
this.ignoredLogs.findIndex((info) => type === info.type && ignoreId === info.ignoreId), this.ignoredLogs.findIndex((info) => type === info.type && ignoreId === info.ignoreId),
1, 1,

View file

@ -1,102 +1,74 @@
export enum LogType { export const LogType = {
MEMBER_WARN = 1, MEMBER_WARN: "MEMBER_WARN",
MEMBER_MUTE, MEMBER_MUTE: "MEMBER_MUTE",
MEMBER_UNMUTE, MEMBER_UNMUTE: "MEMBER_UNMUTE",
MEMBER_MUTE_EXPIRED, MEMBER_MUTE_EXPIRED: "MEMBER_MUTE_EXPIRED",
MEMBER_KICK, MEMBER_KICK: "MEMBER_KICK",
MEMBER_BAN, MEMBER_BAN: "MEMBER_BAN",
MEMBER_UNBAN, MEMBER_UNBAN: "MEMBER_UNBAN",
MEMBER_FORCEBAN, MEMBER_FORCEBAN: "MEMBER_FORCEBAN",
MEMBER_SOFTBAN, MEMBER_SOFTBAN: "MEMBER_SOFTBAN",
MEMBER_JOIN, MEMBER_JOIN: "MEMBER_JOIN",
MEMBER_LEAVE, MEMBER_LEAVE: "MEMBER_LEAVE",
MEMBER_ROLE_ADD, MEMBER_ROLE_ADD: "MEMBER_ROLE_ADD",
MEMBER_ROLE_REMOVE, MEMBER_ROLE_REMOVE: "MEMBER_ROLE_REMOVE",
MEMBER_NICK_CHANGE, MEMBER_NICK_CHANGE: "MEMBER_NICK_CHANGE",
MEMBER_USERNAME_CHANGE, MEMBER_USERNAME_CHANGE: "MEMBER_USERNAME_CHANGE",
MEMBER_RESTORE, MEMBER_RESTORE: "MEMBER_RESTORE",
CHANNEL_CREATE: "CHANNEL_CREATE",
CHANNEL_CREATE, CHANNEL_DELETE: "CHANNEL_DELETE",
CHANNEL_DELETE, CHANNEL_UPDATE: "CHANNEL_UPDATE",
CHANNEL_UPDATE, THREAD_CREATE: "THREAD_CREATE",
THREAD_DELETE: "THREAD_DELETE",
THREAD_CREATE, THREAD_UPDATE: "THREAD_UPDATE",
THREAD_DELETE, ROLE_CREATE: "ROLE_CREATE",
THREAD_UPDATE, ROLE_DELETE: "ROLE_DELETE",
ROLE_UPDATE: "ROLE_UPDATE",
ROLE_CREATE, MESSAGE_EDIT: "MESSAGE_EDIT",
ROLE_DELETE, MESSAGE_DELETE: "MESSAGE_DELETE",
ROLE_UPDATE, MESSAGE_DELETE_BULK: "MESSAGE_DELETE_BULK",
MESSAGE_DELETE_BARE: "MESSAGE_DELETE_BARE",
MESSAGE_EDIT, VOICE_CHANNEL_JOIN: "VOICE_CHANNEL_JOIN",
MESSAGE_DELETE, VOICE_CHANNEL_LEAVE: "VOICE_CHANNEL_LEAVE",
MESSAGE_DELETE_BULK, VOICE_CHANNEL_MOVE: "VOICE_CHANNEL_MOVE",
MESSAGE_DELETE_BARE, STAGE_INSTANCE_CREATE: "STAGE_INSTANCE_CREATE",
STAGE_INSTANCE_DELETE: "STAGE_INSTANCE_DELETE",
VOICE_CHANNEL_JOIN, STAGE_INSTANCE_UPDATE: "STAGE_INSTANCE_UPDATE",
VOICE_CHANNEL_LEAVE, EMOJI_CREATE: "EMOJI_CREATE",
VOICE_CHANNEL_MOVE, EMOJI_DELETE: "EMOJI_DELETE",
EMOJI_UPDATE: "EMOJI_UPDATE",
STAGE_INSTANCE_CREATE, STICKER_CREATE: "STICKER_CREATE",
STAGE_INSTANCE_DELETE, STICKER_DELETE: "STICKER_DELETE",
STAGE_INSTANCE_UPDATE, STICKER_UPDATE: "STICKER_UPDATE",
COMMAND: "COMMAND",
EMOJI_CREATE, MESSAGE_SPAM_DETECTED: "MESSAGE_SPAM_DETECTED",
EMOJI_DELETE, CENSOR: "CENSOR",
EMOJI_UPDATE, CLEAN: "CLEAN",
CASE_CREATE: "CASE_CREATE",
STICKER_CREATE, MASSUNBAN: "MASSUNBAN",
STICKER_DELETE, MASSBAN: "MASSBAN",
STICKER_UPDATE, MASSMUTE: "MASSMUTE",
MEMBER_TIMED_MUTE: "MEMBER_TIMED_MUTE",
COMMAND, MEMBER_TIMED_UNMUTE: "MEMBER_TIMED_UNMUTE",
MEMBER_TIMED_BAN: "MEMBER_TIMED_BAN",
MESSAGE_SPAM_DETECTED, MEMBER_TIMED_UNBAN: "MEMBER_TIMED_UNBAN",
CENSOR, MEMBER_JOIN_WITH_PRIOR_RECORDS: "MEMBER_JOIN_WITH_PRIOR_RECORDS",
CLEAN, OTHER_SPAM_DETECTED: "OTHER_SPAM_DETECTED",
MEMBER_ROLE_CHANGES: "MEMBER_ROLE_CHANGES",
CASE_CREATE, VOICE_CHANNEL_FORCE_MOVE: "VOICE_CHANNEL_FORCE_MOVE",
VOICE_CHANNEL_FORCE_DISCONNECT: "VOICE_CHANNEL_FORCE_DISCONNECT",
MASSUNBAN, CASE_UPDATE: "CASE_UPDATE",
MASSBAN, MEMBER_MUTE_REJOIN: "MEMBER_MUTE_REJOIN",
MASSMUTE, SCHEDULED_MESSAGE: "SCHEDULED_MESSAGE",
POSTED_SCHEDULED_MESSAGE: "POSTED_SCHEDULED_MESSAGE",
MEMBER_TIMED_MUTE, BOT_ALERT: "BOT_ALERT",
MEMBER_TIMED_UNMUTE, AUTOMOD_ACTION: "AUTOMOD_ACTION",
MEMBER_TIMED_BAN, SCHEDULED_REPEATED_MESSAGE: "SCHEDULED_REPEATED_MESSAGE",
MEMBER_TIMED_UNBAN, REPEATED_MESSAGE: "REPEATED_MESSAGE",
MESSAGE_DELETE_AUTO: "MESSAGE_DELETE_AUTO",
MEMBER_JOIN_WITH_PRIOR_RECORDS, SET_ANTIRAID_USER: "SET_ANTIRAID_USER",
OTHER_SPAM_DETECTED, SET_ANTIRAID_AUTO: "SET_ANTIRAID_AUTO",
MEMBER_NOTE: "MEMBER_NOTE",
MEMBER_ROLE_CHANGES, CASE_DELETE: "CASE_DELETE",
VOICE_CHANNEL_FORCE_MOVE, DM_FAILED: "DM_FAILED",
VOICE_CHANNEL_FORCE_DISCONNECT, } as const;
CASE_UPDATE,
MEMBER_MUTE_REJOIN,
SCHEDULED_MESSAGE,
POSTED_SCHEDULED_MESSAGE,
BOT_ALERT,
AUTOMOD_ACTION,
SCHEDULED_REPEATED_MESSAGE,
REPEATED_MESSAGE,
MESSAGE_DELETE_AUTO,
SET_ANTIRAID_USER,
SET_ANTIRAID_AUTO,
MASS_ASSIGN_ROLES,
MASS_UNASSIGN_ROLES,
MEMBER_NOTE,
CASE_DELETE,
DM_FAILED,
}

View file

@ -6,16 +6,16 @@ import { backendDir } from "../paths";
moment.tz.setDefault("UTC"); moment.tz.setDefault("UTC");
const entities = path.relative(process.cwd(), path.resolve(backendDir, "dist/backend/src/data/entities/*.js")); const entities = path.relative(process.cwd(), path.resolve(backendDir, "dist/data/entities/*.js"));
const migrations = path.relative(process.cwd(), path.resolve(backendDir, "dist/backend/src/migrations/*.js")); const migrations = path.relative(process.cwd(), path.resolve(backendDir, "dist/migrations/*.js"));
export const dataSource = new DataSource({ export const dataSource = new DataSource({
type: "mysql", type: "mysql",
host: env.DB_HOST, host: env.DB_HOST || "mysql",
port: env.DB_PORT, port: env.DB_PORT || 3306,
username: env.DB_USER, username: env.DB_USER || "zeppelin",
password: env.DB_PASSWORD, password: env.DB_PASSWORD || env.DEVELOPMENT_MYSQL_PASSWORD,
database: env.DB_DATABASE, database: env.DB_DATABASE || "zeppelin",
charset: "utf8mb4", charset: "utf8mb4",
supportBigNumbers: true, supportBigNumbers: true,
bigNumberStrings: true, bigNumberStrings: true,

View file

@ -15,3 +15,9 @@ export function connect() {
return connectionPromise; return connectionPromise;
} }
export function disconnect() {
if (connectionPromise) {
connectionPromise.then(() => dataSource.destroy());
}
}

View file

@ -13,7 +13,6 @@ const envType = z.object({
DASHBOARD_URL: z.string().url(), DASHBOARD_URL: z.string().url(),
API_URL: z.string().url(), API_URL: z.string().url(),
API_PORT: z.preprocess((v) => Number(v), z.number().min(1).max(65535)).default(3000),
STAFF: z STAFF: z
.preprocess( .preprocess(
@ -39,22 +38,22 @@ const envType = z.object({
PHISHERMAN_API_KEY: z.string().optional(), PHISHERMAN_API_KEY: z.string().optional(),
DOCKER_DEV_MYSQL_PASSWORD: z.string().optional(), // Included here for the DB_PASSWORD default in development DB_HOST: z.string().optional(),
DOCKER_PROD_MYSQL_PASSWORD: z.string().optional(), // Included here for the DB_PASSWORD default in production DB_PORT: z.preprocess((v) => Number(v), z.number()).optional(),
DB_USER: z.string().optional(),
DB_PASSWORD: z.string().optional(),
DB_DATABASE: z.string().optional(),
DB_HOST: z.string().optional().default("mysql"), DEVELOPMENT_MYSQL_PASSWORD: z.string().optional(),
DB_PORT: z
.preprocess((v) => Number(v), z.number()) API_PATH_PREFIX: z.string().optional(),
.optional()
.default(3306),
DB_USER: z.string().optional().default("zeppelin"),
DB_PASSWORD: z.string().optional(), // Default is set to DOCKER_MYSQL_PASSWORD further below
DB_DATABASE: z.string().optional().default("zeppelin"),
DEBUG: z DEBUG: z
.string() .string()
.optional() .optional()
.transform((str) => str === "true"), .transform((str) => str === "true"),
NODE_ENV: z.string().default("development"),
}); });
let toValidate = { ...process.env }; let toValidate = { ...process.env };
@ -65,11 +64,3 @@ if (fs.existsSync(envPath)) {
} }
export const env = envType.parse(toValidate); export const env = envType.parse(toValidate);
if (!env.DB_PASSWORD) {
if (process.env.NODE_ENV === "production" && env.DOCKER_PROD_MYSQL_PASSWORD) {
env.DB_PASSWORD = env.DOCKER_PROD_MYSQL_PASSWORD;
} else if (env.DOCKER_DEV_MYSQL_PASSWORD) {
env.DB_PASSWORD = env.DOCKER_DEV_MYSQL_PASSWORD;
}
}

View file

@ -0,0 +1,23 @@
import { z } from "zod";
import zodToJsonSchema from "zod-to-json-schema";
import { guildPluginInfo } from "./plugins/pluginInfo";
import { zZeppelinGuildConfig } from "./types";
const pluginSchemaMap = Object.entries(guildPluginInfo).reduce((map, [pluginName, pluginInfo]) => {
if (pluginInfo.configSchema) {
map[pluginName] = pluginInfo.configSchema;
}
return map;
}, {});
const fullSchema = zZeppelinGuildConfig.omit({ plugins: true }).merge(
z.strictObject({
plugins: z.strictObject(pluginSchemaMap).partial(),
}),
);
const jsonSchema = zodToJsonSchema(fullSchema);
console.log(JSON.stringify(jsonSchema, null, 2));
process.exit(0);

View file

@ -164,7 +164,7 @@ if (process.env.NODE_ENV === "production") {
} }
// Verify required Node.js version // Verify required Node.js version
const REQUIRED_NODE_VERSION = "14.0.0"; const REQUIRED_NODE_VERSION = "16.9.0";
const requiredParts = REQUIRED_NODE_VERSION.split(".").map((v) => parseInt(v, 10)); const requiredParts = REQUIRED_NODE_VERSION.split(".").map((v) => parseInt(v, 10));
const actualVersionParts = process.versions.node.split(".").map((v) => parseInt(v, 10)); const actualVersionParts = process.versions.node.split(".").map((v) => parseInt(v, 10));
for (const [i, part] of actualVersionParts.entries()) { for (const [i, part] of actualVersionParts.entries()) {
@ -203,7 +203,7 @@ if (env.DEBUG) {
} }
logger.info("Connecting to database"); logger.info("Connecting to database");
connect().then(async (connection) => { connect().then(async () => {
const client = new Client({ const client = new Client({
partials: [Partials.User, Partials.Channel, Partials.GuildMember, Partials.Message, Partials.Reaction], partials: [Partials.User, Partials.Channel, Partials.GuildMember, Partials.Message, Partials.Reaction],
@ -445,7 +445,7 @@ connect().then(async (connection) => {
logger.info("Cleaning up before exit..."); logger.info("Cleaning up before exit...");
// Force exit after 10sec // Force exit after 10sec
setTimeout(() => process.exit(code), 10 * SECONDS); setTimeout(() => process.exit(code), 10 * SECONDS);
await bot.stop(); await bot.destroy();
await dataSource.destroy(); await dataSource.destroy();
logger.info("Done! Exiting now."); logger.info("Done! Exiting now.");
process.exit(code); process.exit(code);

View file

@ -10,11 +10,13 @@ export class CreateSlowmodeTables1544877081073 implements MigrationInterface {
name: "guild_id", name: "guild_id",
type: "bigint", type: "bigint",
unsigned: true, unsigned: true,
isPrimary: true,
}, },
{ {
name: "channel_id", name: "channel_id",
type: "bigint", type: "bigint",
unsigned: true, unsigned: true,
isPrimary: true,
}, },
{ {
name: "slowmode_seconds", name: "slowmode_seconds",
@ -25,7 +27,6 @@ export class CreateSlowmodeTables1544877081073 implements MigrationInterface {
indices: [], indices: [],
}), }),
); );
await queryRunner.createPrimaryKey("slowmode_channels", ["guild_id", "channel_id"]);
await queryRunner.createTable( await queryRunner.createTable(
new Table({ new Table({
@ -35,16 +36,19 @@ export class CreateSlowmodeTables1544877081073 implements MigrationInterface {
name: "guild_id", name: "guild_id",
type: "bigint", type: "bigint",
unsigned: true, unsigned: true,
isPrimary: true,
}, },
{ {
name: "channel_id", name: "channel_id",
type: "bigint", type: "bigint",
unsigned: true, unsigned: true,
isPrimary: true,
}, },
{ {
name: "user_id", name: "user_id",
type: "bigint", type: "bigint",
unsigned: true, unsigned: true,
isPrimary: true,
}, },
{ {
name: "expires_at", name: "expires_at",
@ -58,7 +62,6 @@ export class CreateSlowmodeTables1544877081073 implements MigrationInterface {
], ],
}), }),
); );
await queryRunner.createPrimaryKey("slowmode_users", ["guild_id", "channel_id", "user_id"]);
} }
public async down(queryRunner: QueryRunner): Promise<any> { public async down(queryRunner: QueryRunner): Promise<any> {

View file

@ -61,11 +61,13 @@ export class CreateStarboardTable1544887946307 implements MigrationInterface {
name: "starboard_id", name: "starboard_id",
type: "int", type: "int",
unsigned: true, unsigned: true,
isPrimary: true,
}, },
{ {
name: "message_id", name: "message_id",
type: "bigint", type: "bigint",
unsigned: true, unsigned: true,
isPrimary: true,
}, },
{ {
name: "starboard_message_id", name: "starboard_message_id",
@ -75,7 +77,6 @@ export class CreateStarboardTable1544887946307 implements MigrationInterface {
], ],
}), }),
); );
await queryRunner.createPrimaryKey("starboard_messages", ["starboard_id", "message_id"]);
} }
public async down(queryRunner: QueryRunner): Promise<any> { public async down(queryRunner: QueryRunner): Promise<any> {

View file

@ -10,11 +10,13 @@ export class CreateAutoReactionsTable1547290549908 implements MigrationInterface
name: "guild_id", name: "guild_id",
type: "bigint", type: "bigint",
unsigned: true, unsigned: true,
isPrimary: true,
}, },
{ {
name: "channel_id", name: "channel_id",
type: "bigint", type: "bigint",
unsigned: true, unsigned: true,
isPrimary: true,
}, },
{ {
name: "reactions", name: "reactions",
@ -23,7 +25,6 @@ export class CreateAutoReactionsTable1547290549908 implements MigrationInterface
], ],
}), }),
); );
await queryRunner.createPrimaryKey("auto_reactions", ["guild_id", "channel_id"]);
} }
public async down(queryRunner: QueryRunner): Promise<any> { public async down(queryRunner: QueryRunner): Promise<any> {

View file

@ -9,10 +9,12 @@ export class CreateDashboardUsersTable1558804449510 implements MigrationInterfac
{ {
name: "guild_id", name: "guild_id",
type: "bigint", type: "bigint",
isPrimary: true,
}, },
{ {
name: "user_id", name: "user_id",
type: "bigint", type: "bigint",
isPrimary: true,
}, },
{ {
name: "username", name: "username",
@ -28,7 +30,6 @@ export class CreateDashboardUsersTable1558804449510 implements MigrationInterfac
}), }),
); );
await queryRunner.createPrimaryKey("dashboard_users", ["guild_id", "user_id"]);
await queryRunner.createIndex( await queryRunner.createIndex(
"dashboard_users", "dashboard_users",
new TableIndex({ new TableIndex({

View file

@ -2,14 +2,10 @@ import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from "typeor
export class AddTypeAndPermissionsToApiPermissions1573158035867 implements MigrationInterface { export class AddTypeAndPermissionsToApiPermissions1573158035867 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> { public async up(queryRunner: QueryRunner): Promise<any> {
try { // We can't use a TableIndex object in dropIndex directly as the table name is included in the generated index name
await queryRunner.dropPrimaryKey("api_permissions"); // and the table name has changed since the original index was created
} catch {} // eslint-disable-line no-empty const originalIndexName = queryRunner.connection.namingStrategy.indexName("dashboard_users", ["user_id"]);
await queryRunner.dropIndex("api_permissions", originalIndexName);
const table = (await queryRunner.getTable("api_permissions"))!;
if (table.indices.length) {
await queryRunner.dropIndex("api_permissions", table.indices[0]);
}
await queryRunner.addColumn( await queryRunner.addColumn(
"api_permissions", "api_permissions",
@ -22,7 +18,11 @@ export class AddTypeAndPermissionsToApiPermissions1573158035867 implements Migra
await queryRunner.renameColumn("api_permissions", "user_id", "target_id"); await queryRunner.renameColumn("api_permissions", "user_id", "target_id");
await queryRunner.createPrimaryKey("api_permissions", ["guild_id", "type", "target_id"]); await queryRunner.query(`
ALTER TABLE api_permissions
DROP PRIMARY KEY,
ADD PRIMARY KEY(\`guild_id\`, \`type\`, \`target_id\`);
`);
await queryRunner.dropColumn("api_permissions", "role"); await queryRunner.dropColumn("api_permissions", "role");
@ -49,7 +49,12 @@ export class AddTypeAndPermissionsToApiPermissions1573158035867 implements Migra
} }
public async down(queryRunner: QueryRunner): Promise<any> { public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropIndex("api_permissions", "IDX_e06d750f13e6a4b4d3d6b847a9"); await queryRunner.dropIndex(
"api_permissions",
new TableIndex({
columnNames: ["type", "target_id"],
}),
);
await queryRunner.dropColumn("api_permissions", "permissions"); await queryRunner.dropColumn("api_permissions", "permissions");
@ -62,7 +67,11 @@ export class AddTypeAndPermissionsToApiPermissions1573158035867 implements Migra
}), }),
); );
await queryRunner.dropPrimaryKey("api_permissions"); await queryRunner.query(`
ALTER TABLE api_permissions
DROP PRIMARY KEY,
ADD PRIMARY KEY(\`guild_id\`, \`type\`);
`);
await queryRunner.renameColumn("api_permissions", "target_id", "user_id"); await queryRunner.renameColumn("api_permissions", "target_id", "user_id");
@ -74,7 +83,5 @@ export class AddTypeAndPermissionsToApiPermissions1573158035867 implements Migra
columnNames: ["user_id"], columnNames: ["user_id"],
}), }),
); );
await queryRunner.createPrimaryKey("api_permissions", ["guild_id", "user_id"]);
} }
} }

View file

@ -2,21 +2,19 @@ import { MigrationInterface, QueryRunner, Table, TableColumn } from "typeorm";
export class MoveStarboardsToConfig1573248462469 implements MigrationInterface { export class MoveStarboardsToConfig1573248462469 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> { public async up(queryRunner: QueryRunner): Promise<any> {
// Create the new column for the channels id // Create a new column for the channel's id
const chanid_column = new TableColumn({ await queryRunner.addColumn("starboard_messages", new TableColumn({
name: "starboard_channel_id", name: "starboard_channel_id",
type: "bigint", type: "bigint",
unsigned: true, unsigned: true,
}); }));
await queryRunner.addColumn("starboard_messages", chanid_column);
// Since we are removing the guild_id with the starboards table, we might want it here // Since we are removing the guild_id with the starboards table, we might want it here
const guid_column = new TableColumn({ await queryRunner.addColumn("starboard_messages", new TableColumn({
name: "guild_id", name: "guild_id",
type: "bigint", type: "bigint",
unsigned: true, unsigned: true,
}); }));
await queryRunner.addColumn("starboard_messages", guid_column);
// Migrate the old starboard_id to the new starboard_channel_id // Migrate the old starboard_id to the new starboard_channel_id
await queryRunner.query(` await queryRunner.query(`
@ -26,12 +24,18 @@ export class MoveStarboardsToConfig1573248462469 implements MigrationInterface {
SET sm.starboard_channel_id = sb.channel_id, sm.guild_id = sb.guild_id; SET sm.starboard_channel_id = sb.channel_id, sm.guild_id = sb.guild_id;
`); `);
// Drop the starboard_id column as it is now obsolete
await queryRunner.dropColumn("starboard_messages", "starboard_id");
// Set new Primary Key // Set new Primary Key
await queryRunner.dropPrimaryKey("starboard_messages"); await queryRunner.query(`
await queryRunner.createPrimaryKey("starboard_messages", ["starboard_message_id"]); ALTER TABLE starboard_messages
// Finally, drop the starboards channel as it is now obsolete DROP PRIMARY KEY,
ADD PRIMARY KEY(\`starboard_message_id\`);
`);
// Drop the starboard_id column as it is now obsolete
// We can't use queyrRunner.dropColumn() here because TypeORM helpfully thinks that
// starboard_id is still part of the primary key and tries to drop the PK first
await queryRunner.query("ALTER TABLE starboard_messages DROP COLUMN starboard_id");
// Finally, drop the starboards table as it is now obsolete
await queryRunner.dropTable("starboards", true); await queryRunner.dropTable("starboards", true);
} }
@ -39,15 +43,17 @@ export class MoveStarboardsToConfig1573248462469 implements MigrationInterface {
await queryRunner.dropColumn("starboard_messages", "starboard_channel_id"); await queryRunner.dropColumn("starboard_messages", "starboard_channel_id");
await queryRunner.dropColumn("starboard_messages", "guild_id"); await queryRunner.dropColumn("starboard_messages", "guild_id");
const sbId = new TableColumn({ await queryRunner.addColumn("starboard_messages", new TableColumn({
name: "starboard_id", name: "starboard_id",
type: "int", type: "int",
unsigned: true, unsigned: true,
}); }));
await queryRunner.addColumn("starboard_messages", sbId);
await queryRunner.dropPrimaryKey("starboard_messages"); await queryRunner.query(`
await queryRunner.createPrimaryKey("starboard_messages", ["starboard_id", "message_id"]); ALTER TABLE starboard_messages
DROP PRIMARY KEY,
ADD PRIMARY KEY(\`starboard_id\`, \`message_id\`);
`);
await queryRunner.createTable( await queryRunner.createTable(
new Table({ new Table({

View file

@ -10,22 +10,12 @@ import {
PermissionsBitField, PermissionsBitField,
TextBasedChannel, TextBasedChannel,
} from "discord.js"; } from "discord.js";
import * as t from "io-ts"; import { AnyPluginData, BasePluginData, CommandContext, ExtendedMatchParams, GuildPluginData, helpers } from "knub";
import {
AnyPluginData,
CommandContext,
ConfigValidationError,
ExtendedMatchParams,
GuildPluginData,
PluginOverrideCriteria,
helpers,
} from "knub";
import { logger } from "./logger"; import { logger } from "./logger";
import { isStaff } from "./staff"; import { isStaff } from "./staff";
import { TZeppelinKnub } from "./types"; import { TZeppelinKnub } from "./types";
import { errorMessage, successMessage, tNullable } from "./utils"; import { errorMessage, successMessage } from "./utils";
import { Tail } from "./utils/typeUtils"; import { Tail } from "./utils/typeUtils";
import { StrictValidationError, parseIoTsSchema } from "./validatorUtils";
const { getMemberLevel } = helpers; const { getMemberLevel } = helpers;
@ -59,46 +49,6 @@ export async function hasPermission(
return helpers.hasPermission(config, permission); return helpers.hasPermission(config, permission);
} }
const PluginOverrideCriteriaType: t.Type<PluginOverrideCriteria<unknown>> = t.recursion(
"PluginOverrideCriteriaType",
() =>
t.partial({
channel: tNullable(t.union([t.string, t.array(t.string)])),
category: tNullable(t.union([t.string, t.array(t.string)])),
level: tNullable(t.union([t.string, t.array(t.string)])),
user: tNullable(t.union([t.string, t.array(t.string)])),
role: tNullable(t.union([t.string, t.array(t.string)])),
all: tNullable(t.array(PluginOverrideCriteriaType)),
any: tNullable(t.array(PluginOverrideCriteriaType)),
not: tNullable(PluginOverrideCriteriaType),
extra: t.unknown,
}),
);
export function strictValidationErrorToConfigValidationError(err: StrictValidationError) {
return new ConfigValidationError(
err
.getErrors()
.map((e) => e.toString())
.join("\n"),
);
}
export function makeIoTsConfigParser<Schema extends t.Type<any>>(schema: Schema): (input: unknown) => t.TypeOf<Schema> {
return (input: unknown) => {
try {
return parseIoTsSchema(schema, input);
} catch (err) {
if (err instanceof StrictValidationError) {
throw strictValidationErrorToConfigValidationError(err);
}
throw err;
}
};
}
export async function sendSuccessMessage( export async function sendSuccessMessage(
pluginData: AnyPluginData<any>, pluginData: AnyPluginData<any>,
channel: TextBasedChannel, channel: TextBasedChannel,
@ -174,3 +124,16 @@ export function mapToPublicFn<T extends AnyFn>(inputFn: T) {
}; };
}; };
} }
type FnWithPluginData<TPluginData> = (pluginData: TPluginData, ...args: any[]) => any;
export function makePublicFn<TPluginData extends BasePluginData<any>, T extends FnWithPluginData<TPluginData>>(
pluginData: TPluginData,
fn: T,
) {
return (...args: Tail<Parameters<T>>): ReturnType<T> => {
return fn(pluginData, ...args);
};
}
// ???

View file

@ -1,11 +1,9 @@
import { PluginOptions } from "knub"; import { PluginOptions, guildPlugin } from "knub";
import { GuildLogs } from "../../data/GuildLogs"; import { GuildLogs } from "../../data/GuildLogs";
import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { makeIoTsConfigParser } from "../../pluginUtils";
import { LogsPlugin } from "../Logs/LogsPlugin"; import { LogsPlugin } from "../Logs/LogsPlugin";
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { AutoDeletePluginType, zAutoDeleteConfig } from "./types";
import { AutoDeletePluginType, ConfigSchema } from "./types";
import { onMessageCreate } from "./util/onMessageCreate"; import { onMessageCreate } from "./util/onMessageCreate";
import { onMessageDelete } from "./util/onMessageDelete"; import { onMessageDelete } from "./util/onMessageDelete";
import { onMessageDeleteBulk } from "./util/onMessageDeleteBulk"; import { onMessageDeleteBulk } from "./util/onMessageDeleteBulk";
@ -17,18 +15,11 @@ const defaultOptions: PluginOptions<AutoDeletePluginType> = {
}, },
}; };
export const AutoDeletePlugin = zeppelinGuildPlugin<AutoDeletePluginType>()({ export const AutoDeletePlugin = guildPlugin<AutoDeletePluginType>()({
name: "auto_delete", name: "auto_delete",
showInDocs: true,
info: {
prettyName: "Auto-delete",
description: "Allows Zeppelin to auto-delete messages from a channel after a delay",
configurationGuide: "Maximum deletion delay is currently 5 minutes",
configSchema: ConfigSchema,
},
dependencies: () => [TimeAndDatePlugin, LogsPlugin], dependencies: () => [TimeAndDatePlugin, LogsPlugin],
configParser: makeIoTsConfigParser(ConfigSchema), configParser: (input) => zAutoDeleteConfig.parse(input),
defaultOptions, defaultOptions,
beforeLoad(pluginData) { beforeLoad(pluginData) {

View file

@ -0,0 +1,8 @@
import { ZeppelinPluginInfo } from "../../types";
export const autoDeletePluginInfo: ZeppelinPluginInfo = {
showInDocs: true,
prettyName: "Auto-delete",
description: "Allows Zeppelin to auto-delete messages from a channel after a delay",
configurationGuide: "Maximum deletion delay is currently 5 minutes",
};

View file

@ -1,9 +1,9 @@
import * as t from "io-ts";
import { BasePluginType } from "knub"; import { BasePluginType } from "knub";
import z from "zod";
import { GuildLogs } from "../../data/GuildLogs"; import { GuildLogs } from "../../data/GuildLogs";
import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { SavedMessage } from "../../data/entities/SavedMessage"; import { SavedMessage } from "../../data/entities/SavedMessage";
import { MINUTES, tDelayString } from "../../utils"; import { MINUTES, zDelayString } from "../../utils";
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
export const MAX_DELAY = 5 * MINUTES; export const MAX_DELAY = 5 * MINUTES;
@ -13,14 +13,13 @@ export interface IDeletionQueueItem {
message: SavedMessage; message: SavedMessage;
} }
export const ConfigSchema = t.type({ export const zAutoDeleteConfig = z.strictObject({
enabled: t.boolean, enabled: z.boolean(),
delay: tDelayString, delay: zDelayString,
}); });
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
export interface AutoDeletePluginType extends BasePluginType { export interface AutoDeletePluginType extends BasePluginType {
config: TConfigSchema; config: z.output<typeof zAutoDeleteConfig>;
state: { state: {
guildSavedMessages: GuildSavedMessages; guildSavedMessages: GuildSavedMessages;
guildLogs: GuildLogs; guildLogs: GuildLogs;

View file

@ -1,14 +1,11 @@
import { PluginOptions } from "knub"; import { PluginOptions, guildPlugin } from "knub";
import { GuildAutoReactions } from "../../data/GuildAutoReactions"; import { GuildAutoReactions } from "../../data/GuildAutoReactions";
import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { makeIoTsConfigParser } from "../../pluginUtils";
import { trimPluginDescription } from "../../utils";
import { LogsPlugin } from "../Logs/LogsPlugin"; import { LogsPlugin } from "../Logs/LogsPlugin";
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
import { DisableAutoReactionsCmd } from "./commands/DisableAutoReactionsCmd"; import { DisableAutoReactionsCmd } from "./commands/DisableAutoReactionsCmd";
import { NewAutoReactionsCmd } from "./commands/NewAutoReactionsCmd"; import { NewAutoReactionsCmd } from "./commands/NewAutoReactionsCmd";
import { AddReactionsEvt } from "./events/AddReactionsEvt"; import { AddReactionsEvt } from "./events/AddReactionsEvt";
import { AutoReactionsPluginType, ConfigSchema } from "./types"; import { AutoReactionsPluginType, zAutoReactionsConfig } from "./types";
const defaultOptions: PluginOptions<AutoReactionsPluginType> = { const defaultOptions: PluginOptions<AutoReactionsPluginType> = {
config: { config: {
@ -24,23 +21,15 @@ const defaultOptions: PluginOptions<AutoReactionsPluginType> = {
], ],
}; };
export const AutoReactionsPlugin = zeppelinGuildPlugin<AutoReactionsPluginType>()({ export const AutoReactionsPlugin = guildPlugin<AutoReactionsPluginType>()({
name: "auto_reactions", name: "auto_reactions",
showInDocs: true,
info: {
prettyName: "Auto-reactions",
description: trimPluginDescription(`
Allows setting up automatic reactions to all new messages on a channel
`),
configSchema: ConfigSchema,
},
// prettier-ignore // prettier-ignore
dependencies: () => [ dependencies: () => [
LogsPlugin, LogsPlugin,
], ],
configParser: makeIoTsConfigParser(ConfigSchema), configParser: (input) => zAutoReactionsConfig.parse(input),
defaultOptions, defaultOptions,
// prettier-ignore // prettier-ignore

View file

@ -0,0 +1,10 @@
import { ZeppelinPluginInfo } from "../../types";
import { trimPluginDescription } from "../../utils";
export const autoReactionsInfo: ZeppelinPluginInfo = {
showInDocs: true,
prettyName: "Auto-reactions",
description: trimPluginDescription(`
Allows setting up automatic reactions to all new messages on a channel
`),
};

View file

@ -1,17 +1,16 @@
import * as t from "io-ts";
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub"; import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub";
import z from "zod";
import { GuildAutoReactions } from "../../data/GuildAutoReactions"; import { GuildAutoReactions } from "../../data/GuildAutoReactions";
import { GuildLogs } from "../../data/GuildLogs"; import { GuildLogs } from "../../data/GuildLogs";
import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { AutoReaction } from "../../data/entities/AutoReaction"; import { AutoReaction } from "../../data/entities/AutoReaction";
export const ConfigSchema = t.type({ export const zAutoReactionsConfig = z.strictObject({
can_manage: t.boolean, can_manage: z.boolean(),
}); });
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
export interface AutoReactionsPluginType extends BasePluginType { export interface AutoReactionsPluginType extends BasePluginType {
config: TConfigSchema; config: z.output<typeof zAutoReactionsConfig>;
state: { state: {
logs: GuildLogs; logs: GuildLogs;
savedMessages: GuildSavedMessages; savedMessages: GuildSavedMessages;

View file

@ -1,14 +1,13 @@
import { configUtils, CooldownManager } from "knub"; import { CooldownManager, guildPlugin } from "knub";
import { Queue } from "../../Queue";
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels"; import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
import { GuildArchives } from "../../data/GuildArchives"; import { GuildArchives } from "../../data/GuildArchives";
import { GuildLogs } from "../../data/GuildLogs"; import { GuildLogs } from "../../data/GuildLogs";
import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { Queue } from "../../Queue";
import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners";
import { MINUTES, SECONDS } from "../../utils"; import { MINUTES, SECONDS } from "../../utils";
import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap"; import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap";
import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap"; import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap";
import { parseIoTsSchema, StrictValidationError } from "../../validatorUtils";
import { CountersPlugin } from "../Counters/CountersPlugin"; import { CountersPlugin } from "../Counters/CountersPlugin";
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin"; import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin";
import { LogsPlugin } from "../Logs/LogsPlugin"; import { LogsPlugin } from "../Logs/LogsPlugin";
@ -16,14 +15,12 @@ import { ModActionsPlugin } from "../ModActions/ModActionsPlugin";
import { MutesPlugin } from "../Mutes/MutesPlugin"; import { MutesPlugin } from "../Mutes/MutesPlugin";
import { PhishermanPlugin } from "../Phisherman/PhishermanPlugin"; import { PhishermanPlugin } from "../Phisherman/PhishermanPlugin";
import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin"; import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin";
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
import { availableActions } from "./actions/availableActions";
import { AntiraidClearCmd } from "./commands/AntiraidClearCmd"; import { AntiraidClearCmd } from "./commands/AntiraidClearCmd";
import { SetAntiraidCmd } from "./commands/SetAntiraidCmd"; import { SetAntiraidCmd } from "./commands/SetAntiraidCmd";
import { ViewAntiraidCmd } from "./commands/ViewAntiraidCmd"; import { ViewAntiraidCmd } from "./commands/ViewAntiraidCmd";
import { runAutomodOnCounterTrigger } from "./events/runAutomodOnCounterTrigger";
import { RunAutomodOnJoinEvt, RunAutomodOnLeaveEvt } from "./events/RunAutomodOnJoinLeaveEvt"; import { RunAutomodOnJoinEvt, RunAutomodOnLeaveEvt } from "./events/RunAutomodOnJoinLeaveEvt";
import { RunAutomodOnMemberUpdate } from "./events/RunAutomodOnMemberUpdate"; import { RunAutomodOnMemberUpdate } from "./events/RunAutomodOnMemberUpdate";
import { runAutomodOnCounterTrigger } from "./events/runAutomodOnCounterTrigger";
import { runAutomodOnMessage } from "./events/runAutomodOnMessage"; import { runAutomodOnMessage } from "./events/runAutomodOnMessage";
import { runAutomodOnModAction } from "./events/runAutomodOnModAction"; import { runAutomodOnModAction } from "./events/runAutomodOnModAction";
import { import {
@ -34,9 +31,7 @@ import {
import { clearOldRecentNicknameChanges } from "./functions/clearOldNicknameChanges"; import { clearOldRecentNicknameChanges } from "./functions/clearOldNicknameChanges";
import { clearOldRecentActions } from "./functions/clearOldRecentActions"; import { clearOldRecentActions } from "./functions/clearOldRecentActions";
import { clearOldRecentSpam } from "./functions/clearOldRecentSpam"; import { clearOldRecentSpam } from "./functions/clearOldRecentSpam";
import { pluginInfo } from "./info"; import { AutomodPluginType, zAutomodConfig } from "./types";
import { availableTriggers } from "./triggers/availableTriggers";
import { AutomodPluginType, ConfigSchema } from "./types";
const defaultOptions = { const defaultOptions = {
config: { config: {
@ -61,133 +56,8 @@ const defaultOptions = {
], ],
}; };
/** export const AutomodPlugin = guildPlugin<AutomodPluginType>()({
* Config preprocessor to set default values for triggers and perform extra validation
* TODO: Separate input and output types
*/
const configParser = (input: unknown) => {
const rules = (input as any).rules;
if (rules) {
// Loop through each rule
for (const [name, rule] of Object.entries(rules)) {
if (rule == null) {
delete rules[name];
continue;
}
rule["name"] = name;
// If the rule doesn't have an explicitly set "enabled" property, set it to true
if (rule["enabled"] == null) {
rule["enabled"] = true;
}
if (rule["allow_further_rules"] == null) {
rule["allow_further_rules"] = false;
}
if (rule["affects_bots"] == null) {
rule["affects_bots"] = false;
}
if (rule["affects_self"] == null) {
rule["affects_self"] = false;
}
// Loop through the rule's triggers
if (rule["triggers"]) {
for (const triggerObj of rule["triggers"]) {
for (const triggerName in triggerObj) {
if (!availableTriggers[triggerName]) {
throw new StrictValidationError([`Unknown trigger '${triggerName}' in rule '${rule["name"]}'`]);
}
const triggerBlueprint = availableTriggers[triggerName];
if (typeof triggerBlueprint.defaultConfig === "object" && triggerBlueprint.defaultConfig != null) {
triggerObj[triggerName] = configUtils.mergeConfig(
triggerBlueprint.defaultConfig,
triggerObj[triggerName] || {},
);
} else {
triggerObj[triggerName] = triggerObj[triggerName] || triggerBlueprint.defaultConfig;
}
if (triggerObj[triggerName].match_attachment_type) {
const white = triggerObj[triggerName].match_attachment_type.whitelist_enabled;
const black = triggerObj[triggerName].match_attachment_type.blacklist_enabled;
if (white && black) {
throw new StrictValidationError([
`Cannot have both blacklist and whitelist enabled at rule <${rule["name"]}/match_attachment_type>`,
]);
} else if (!white && !black) {
throw new StrictValidationError([
`Must have either blacklist or whitelist enabled at rule <${rule["name"]}/match_attachment_type>`,
]);
}
}
if (triggerObj[triggerName].match_mime_type) {
const white = triggerObj[triggerName].match_mime_type.whitelist_enabled;
const black = triggerObj[triggerName].match_mime_type.blacklist_enabled;
if (white && black) {
throw new StrictValidationError([
`Cannot have both blacklist and whitelist enabled at rule <${rule["name"]}/match_mime_type>`,
]);
} else if (!white && !black) {
throw new StrictValidationError([
`Must have either blacklist or whitelist enabled at rule <${rule["name"]}/match_mime_type>`,
]);
}
}
}
}
}
if (rule["actions"]) {
for (const actionName in rule["actions"]) {
if (!availableActions[actionName]) {
throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule["name"]}'`]);
}
const actionBlueprint = availableActions[actionName];
const actionConfig = rule["actions"][actionName];
if (typeof actionConfig !== "object" || Array.isArray(actionConfig) || actionConfig == null) {
rule["actions"][actionName] = actionConfig;
} else {
rule["actions"][actionName] = configUtils.mergeConfig(actionBlueprint.defaultConfig, actionConfig);
}
}
}
// Enable logging of automod actions by default
if (rule["actions"]) {
for (const actionName in rule["actions"]) {
if (!availableActions[actionName]) {
throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule["name"]}'`]);
}
}
if (rule["actions"]["log"] == null) {
rule["actions"]["log"] = true;
}
if (rule["actions"]["clean"] && rule["actions"]["start_thread"]) {
throw new StrictValidationError([`Cannot have both clean and start_thread at rule '${rule["name"]}'`]);
}
}
}
}
return parseIoTsSchema(ConfigSchema, input);
};
export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
name: "automod", name: "automod",
showInDocs: true,
info: pluginInfo,
// prettier-ignore // prettier-ignore
dependencies: () => [ dependencies: () => [
@ -201,7 +71,7 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
], ],
defaultOptions, defaultOptions,
configParser, configParser: (input) => zAutomodConfig.parse(input),
customOverrideCriteriaFunctions: { customOverrideCriteriaFunctions: {
antiraid_level: (pluginData, matchParams, value) => { antiraid_level: (pluginData, matchParams, value) => {

View file

@ -1,6 +1,6 @@
import { PermissionFlagsBits, Snowflake } from "discord.js"; import { PermissionFlagsBits, Snowflake } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { nonNullish, unique } from "../../../utils"; import { nonNullish, unique, zSnowflake } from "../../../utils";
import { canAssignRole } from "../../../utils/canAssignRole"; import { canAssignRole } from "../../../utils/canAssignRole";
import { getMissingPermissions } from "../../../utils/getMissingPermissions"; import { getMissingPermissions } from "../../../utils/getMissingPermissions";
import { missingPermissionError } from "../../../utils/missingPermissionError"; import { missingPermissionError } from "../../../utils/missingPermissionError";
@ -11,9 +11,10 @@ import { automodAction } from "../helpers";
const p = PermissionFlagsBits; const p = PermissionFlagsBits;
const configSchema = z.array(zSnowflake);
export const AddRolesAction = automodAction({ export const AddRolesAction = automodAction({
configType: t.array(t.string), configSchema,
defaultConfig: [],
async apply({ pluginData, contexts, actionConfig, ruleName }) { async apply({ pluginData, contexts, actionConfig, ruleName }) {
const members = unique(contexts.map((c) => c.member).filter(nonNullish)); const members = unique(contexts.map((c) => c.member).filter(nonNullish));

View file

@ -1,15 +1,16 @@
import * as t from "io-ts"; import z from "zod";
import { zBoundedCharacters } from "../../../utils";
import { CountersPlugin } from "../../Counters/CountersPlugin"; import { CountersPlugin } from "../../Counters/CountersPlugin";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const AddToCounterAction = automodAction({ const configSchema = z.object({
configType: t.type({ counter: zBoundedCharacters(0, 100),
counter: t.string, amount: z.number(),
amount: t.number, });
}),
defaultConfig: {}, export const AddToCounterAction = automodAction({
configSchema,
async apply({ pluginData, contexts, actionConfig, ruleName }) { async apply({ pluginData, contexts, actionConfig, ruleName }) {
const countersPlugin = pluginData.getPlugin(CountersPlugin); const countersPlugin = pluginData.getPlugin(CountersPlugin);

View file

@ -1,6 +1,5 @@
import { Snowflake } from "discord.js"; import { Snowflake } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { erisAllowedMentionsToDjsMentionOptions } from "src/utils/erisAllowedMentionsToDjsMentionOptions";
import { LogType } from "../../../data/LogType"; import { LogType } from "../../../data/LogType";
import { import {
createTypedTemplateSafeValueContainer, createTypedTemplateSafeValueContainer,
@ -12,25 +11,28 @@ import {
chunkMessageLines, chunkMessageLines,
isTruthy, isTruthy,
messageLink, messageLink,
tAllowedMentions,
tNormalizedNullOptional,
validateAndParseMessageContent, validateAndParseMessageContent,
verboseChannelMention, verboseChannelMention,
zAllowedMentions,
zBoundedCharacters,
zNullishToUndefined,
zSnowflake,
} from "../../../utils"; } from "../../../utils";
import { erisAllowedMentionsToDjsMentionOptions } from "../../../utils/erisAllowedMentionsToDjsMentionOptions";
import { messageIsEmpty } from "../../../utils/messageIsEmpty"; import { messageIsEmpty } from "../../../utils/messageIsEmpty";
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { InternalPosterPlugin } from "../../InternalPoster/InternalPosterPlugin"; import { InternalPosterPlugin } from "../../InternalPoster/InternalPosterPlugin";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const AlertAction = automodAction({ const configSchema = z.object({
configType: t.type({ channel: zSnowflake,
channel: t.string, text: zBoundedCharacters(0, 4000),
text: t.string, allowed_mentions: zNullishToUndefined(zAllowedMentions.nullable().default(null)),
allowed_mentions: tNormalizedNullOptional(tAllowedMentions), });
}),
defaultConfig: {}, export const AlertAction = automodAction({
configSchema,
async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) { async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) {
const channel = pluginData.guild.channels.cache.get(actionConfig.channel as Snowflake); const channel = pluginData.guild.channels.cache.get(actionConfig.channel as Snowflake);

View file

@ -1,11 +1,12 @@
import { AnyThreadChannel } from "discord.js"; import { AnyThreadChannel } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { noop } from "../../../utils"; import { noop } from "../../../utils";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
const configSchema = z.strictObject({});
export const ArchiveThreadAction = automodAction({ export const ArchiveThreadAction = automodAction({
configType: t.type({}), configSchema,
defaultConfig: {},
async apply({ pluginData, contexts }) { async apply({ pluginData, contexts }) {
const threads = contexts const threads = contexts

View file

@ -1,4 +1,3 @@
import * as t from "io-ts";
import { AutomodActionBlueprint } from "../helpers"; import { AutomodActionBlueprint } from "../helpers";
import { AddRolesAction } from "./addRoles"; import { AddRolesAction } from "./addRoles";
import { AddToCounterAction } from "./addToCounter"; import { AddToCounterAction } from "./addToCounter";
@ -11,6 +10,7 @@ import { CleanAction } from "./clean";
import { KickAction } from "./kick"; import { KickAction } from "./kick";
import { LogAction } from "./log"; import { LogAction } from "./log";
import { MuteAction } from "./mute"; import { MuteAction } from "./mute";
import { PauseInvitesAction } from "./pauseInvites";
import { RemoveRolesAction } from "./removeRoles"; import { RemoveRolesAction } from "./removeRoles";
import { ReplyAction } from "./reply"; import { ReplyAction } from "./reply";
import { SetAntiraidLevelAction } from "./setAntiraidLevel"; import { SetAntiraidLevelAction } from "./setAntiraidLevel";
@ -19,7 +19,7 @@ import { SetSlowmodeAction } from "./setSlowmode";
import { StartThreadAction } from "./startThread"; import { StartThreadAction } from "./startThread";
import { WarnAction } from "./warn"; import { WarnAction } from "./warn";
export const availableActions: Record<string, AutomodActionBlueprint<any>> = { export const availableActions = {
clean: CleanAction, clean: CleanAction,
warn: WarnAction, warn: WarnAction,
mute: MuteAction, mute: MuteAction,
@ -38,25 +38,5 @@ export const availableActions: Record<string, AutomodActionBlueprint<any>> = {
start_thread: StartThreadAction, start_thread: StartThreadAction,
archive_thread: ArchiveThreadAction, archive_thread: ArchiveThreadAction,
change_perms: ChangePermsAction, change_perms: ChangePermsAction,
}; pause_invites: PauseInvitesAction,
} satisfies Record<string, AutomodActionBlueprint<any>>;
export const AvailableActions = t.type({
clean: CleanAction.configType,
warn: WarnAction.configType,
mute: MuteAction.configType,
kick: KickAction.configType,
ban: BanAction.configType,
alert: AlertAction.configType,
change_nickname: ChangeNicknameAction.configType,
log: LogAction.configType,
add_roles: AddRolesAction.configType,
remove_roles: RemoveRolesAction.configType,
set_antiraid_level: SetAntiraidLevelAction.configType,
reply: ReplyAction.configType,
add_to_counter: AddToCounterAction.configType,
set_counter: SetCounterAction.configType,
set_slowmode: SetSlowmodeAction.configType,
start_thread: StartThreadAction.configType,
archive_thread: ArchiveThreadAction.configType,
change_perms: ChangePermsAction.configType,
});

View file

@ -1,25 +1,30 @@
import * as t from "io-ts"; import z from "zod";
import { convertDelayStringToMS, nonNullish, tDelayString, tNullable, unique } from "../../../utils"; import {
convertDelayStringToMS,
nonNullish,
unique,
zBoundedCharacters,
zDelayString,
zSnowflake,
} from "../../../utils";
import { CaseArgs } from "../../Cases/types"; import { CaseArgs } from "../../Cases/types";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
import { zNotify } from "../constants";
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const BanAction = automodAction({ const configSchema = z.strictObject({
configType: t.type({ reason: zBoundedCharacters(0, 4000).nullable().default(null),
reason: tNullable(t.string), duration: zDelayString.nullable().default(null),
duration: tNullable(tDelayString), notify: zNotify.nullable().default(null),
notify: tNullable(t.string), notifyChannel: zSnowflake.nullable().default(null),
notifyChannel: tNullable(t.string), deleteMessageDays: z.number().nullable().default(null),
deleteMessageDays: tNullable(t.number), postInCaseLog: z.boolean().nullable().default(null),
postInCaseLog: tNullable(t.boolean), hide_case: z.boolean().nullable().default(false),
hide_case: tNullable(t.boolean), });
}),
defaultConfig: { export const BanAction = automodAction({
notify: null, // Use defaults from ModActions configSchema,
hide_case: false,
},
async apply({ pluginData, contexts, actionConfig, matchResult }) { async apply({ pluginData, contexts, actionConfig, matchResult }) {
const reason = actionConfig.reason || "Kicked automatically"; const reason = actionConfig.reason || "Kicked automatically";

View file

@ -1,18 +1,16 @@
import * as t from "io-ts"; import z from "zod";
import { nonNullish, unique } from "../../../utils"; import { nonNullish, unique, zBoundedCharacters } from "../../../utils";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const ChangeNicknameAction = automodAction({ export const ChangeNicknameAction = automodAction({
configType: t.union([ configSchema: z.union([
t.string, zBoundedCharacters(0, 32),
t.type({ z.strictObject({
name: t.string, name: zBoundedCharacters(0, 32),
}), }),
]), ]),
defaultConfig: {},
async apply({ pluginData, contexts, actionConfig }) { async apply({ pluginData, contexts, actionConfig }) {
const members = unique(contexts.map((c) => c.member).filter(nonNullish)); const members = unique(contexts.map((c) => c.member).filter(nonNullish));

View file

@ -1,12 +1,14 @@
import { PermissionsBitField, PermissionsString } from "discord.js"; import { PermissionsBitField, PermissionsString } from "discord.js";
import * as t from "io-ts"; import { U } from "ts-toolbelt";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import z from "zod";
import { isValidSnowflake, noop, tNullable, tPartialDictionary } from "../../../utils"; import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { isValidSnowflake, keys, noop, zBoundedCharacters } from "../../../utils";
import { import {
guildToTemplateSafeGuild, guildToTemplateSafeGuild,
savedMessageToTemplateSafeSavedMessage, savedMessageToTemplateSafeSavedMessage,
userToTemplateSafeUser, userToTemplateSafeUser,
} from "../../../utils/templateSafeObjects"; } from "../../../utils/templateSafeObjects";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
type LegacyPermMap = Record<string, keyof (typeof PermissionsBitField)["Flags"]>; type LegacyPermMap = Record<string, keyof (typeof PermissionsBitField)["Flags"]>;
@ -59,41 +61,63 @@ const realToLegacyMap = Object.entries(legacyPermMap).reduce((map, pair) => {
return map; return map;
}, {}) as Record<keyof typeof PermissionsBitField.Flags, keyof typeof legacyPermMap>; }, {}) as Record<keyof typeof PermissionsBitField.Flags, keyof typeof legacyPermMap>;
export const ChangePermsAction = automodAction({ const permissionNames = keys(PermissionsBitField.Flags) as U.ListOf<keyof typeof PermissionsBitField.Flags>;
configType: t.type({ const legacyPermissionNames = keys(legacyPermMap) as U.ListOf<keyof typeof legacyPermMap>;
target: t.string, const allPermissionNames = [...permissionNames, ...legacyPermissionNames] as const;
channel: tNullable(t.string),
perms: tPartialDictionary(
t.union([t.keyof(PermissionsBitField.Flags), t.keyof(legacyPermMap)]),
tNullable(t.boolean),
),
}),
defaultConfig: {},
async apply({ pluginData, contexts, actionConfig }) { export const ChangePermsAction = automodAction({
configSchema: z.strictObject({
target: zBoundedCharacters(1, 2000),
channel: zBoundedCharacters(1, 2000).nullable().default(null),
perms: z.record(z.enum(allPermissionNames), z.boolean().nullable()),
}),
async apply({ pluginData, contexts, actionConfig, ruleName }) {
const user = contexts.find((c) => c.user)?.user; const user = contexts.find((c) => c.user)?.user;
const message = contexts.find((c) => c.message)?.message; const message = contexts.find((c) => c.message)?.message;
const renderTarget = async (str: string) => let target: string;
renderTemplate( try {
str, target = await renderTemplate(
actionConfig.target,
new TemplateSafeValueContainer({ new TemplateSafeValueContainer({
user: user ? userToTemplateSafeUser(user) : null, user: user ? userToTemplateSafeUser(user) : null,
guild: guildToTemplateSafeGuild(pluginData.guild), guild: guildToTemplateSafeGuild(pluginData.guild),
message: message ? savedMessageToTemplateSafeSavedMessage(message) : null, message: message ? savedMessageToTemplateSafeSavedMessage(message) : null,
}), }),
); );
const renderChannel = async (str: string) => } catch (err) {
renderTemplate( if (err instanceof TemplateParseError) {
str, pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Error in target format of automod rule ${ruleName}: ${err.message}`,
});
return;
}
throw err;
}
let channelId: string | null = null;
if (actionConfig.channel) {
try {
channelId = await renderTemplate(
actionConfig.channel,
new TemplateSafeValueContainer({ new TemplateSafeValueContainer({
user: user ? userToTemplateSafeUser(user) : null, user: user ? userToTemplateSafeUser(user) : null,
guild: guildToTemplateSafeGuild(pluginData.guild), guild: guildToTemplateSafeGuild(pluginData.guild),
message: message ? savedMessageToTemplateSafeSavedMessage(message) : null, message: message ? savedMessageToTemplateSafeSavedMessage(message) : null,
}), }),
); );
const target = await renderTarget(actionConfig.target); } catch (err) {
const channelId = actionConfig.channel ? await renderChannel(actionConfig.channel) : null; if (err instanceof TemplateParseError) {
pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Error in channel format of automod rule ${ruleName}: ${err.message}`,
});
return;
}
throw err;
}
}
const role = pluginData.guild.roles.resolve(target); const role = pluginData.guild.roles.resolve(target);
if (!role) { if (!role) {
const member = await pluginData.guild.members.fetch(target).catch(noop); const member = await pluginData.guild.members.fetch(target).catch(noop);

View file

@ -1,12 +1,11 @@
import { GuildTextBasedChannel, Snowflake } from "discord.js"; import { GuildTextBasedChannel, Snowflake } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { LogType } from "../../../data/LogType"; import { LogType } from "../../../data/LogType";
import { noop } from "../../../utils"; import { noop } from "../../../utils";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const CleanAction = automodAction({ export const CleanAction = automodAction({
configType: t.boolean, configSchema: z.boolean().default(false),
defaultConfig: false,
async apply({ pluginData, contexts, ruleName }) { async apply({ pluginData, contexts, ruleName }) {
const messageIdsToDeleteByChannelId: Map<string, string[]> = new Map(); const messageIdsToDeleteByChannelId: Map<string, string[]> = new Map();

View file

@ -1,13 +1,12 @@
import * as t from "io-ts"; import z from "zod";
import { zBoundedCharacters } from "../../../utils";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const ExampleAction = automodAction({ export const ExampleAction = automodAction({
configType: t.type({ configSchema: z.strictObject({
someValue: t.string, someValue: zBoundedCharacters(0, 1000),
}), }),
defaultConfig: {},
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
async apply({ pluginData, contexts, actionConfig }) { async apply({ pluginData, contexts, actionConfig }) {
// TODO: Everything // TODO: Everything

View file

@ -1,24 +1,20 @@
import * as t from "io-ts"; import z from "zod";
import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils"; import { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils";
import { CaseArgs } from "../../Cases/types"; import { CaseArgs } from "../../Cases/types";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
import { zNotify } from "../constants";
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const KickAction = automodAction({ export const KickAction = automodAction({
configType: t.type({ configSchema: z.strictObject({
reason: tNullable(t.string), reason: zBoundedCharacters(0, 4000).nullable().default(null),
notify: tNullable(t.string), notify: zNotify.nullable().default(null),
notifyChannel: tNullable(t.string), notifyChannel: zSnowflake.nullable().default(null),
postInCaseLog: tNullable(t.boolean), postInCaseLog: z.boolean().nullable().default(null),
hide_case: tNullable(t.boolean), hide_case: z.boolean().nullable().default(false),
}), }),
defaultConfig: {
notify: null, // Use defaults from ModActions
hide_case: false,
},
async apply({ pluginData, contexts, actionConfig, matchResult }) { async apply({ pluginData, contexts, actionConfig, matchResult }) {
const reason = actionConfig.reason || "Kicked automatically"; const reason = actionConfig.reason || "Kicked automatically";
const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined; const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined;

View file

@ -1,11 +1,10 @@
import * as t from "io-ts"; import z from "zod";
import { isTruthy, unique } from "../../../utils"; import { isTruthy, unique } from "../../../utils";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const LogAction = automodAction({ export const LogAction = automodAction({
configType: t.boolean, configSchema: z.boolean().default(true),
defaultConfig: true,
async apply({ pluginData, contexts, ruleName, matchResult }) { async apply({ pluginData, contexts, ruleName, matchResult }) {
const users = unique(contexts.map((c) => c.user)).filter(isTruthy); const users = unique(contexts.map((c) => c.user)).filter(isTruthy);

View file

@ -1,29 +1,38 @@
import * as t from "io-ts"; import z from "zod";
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError";
import { convertDelayStringToMS, nonNullish, tDelayString, tNullable, unique } from "../../../utils"; import {
convertDelayStringToMS,
nonNullish,
unique,
zBoundedCharacters,
zDelayString,
zSnowflake,
} from "../../../utils";
import { CaseArgs } from "../../Cases/types"; import { CaseArgs } from "../../Cases/types";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { MutesPlugin } from "../../Mutes/MutesPlugin"; import { MutesPlugin } from "../../Mutes/MutesPlugin";
import { zNotify } from "../constants";
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const MuteAction = automodAction({ export const MuteAction = automodAction({
configType: t.type({ configSchema: z.strictObject({
reason: tNullable(t.string), reason: zBoundedCharacters(0, 4000).nullable().default(null),
duration: tNullable(tDelayString), duration: zDelayString.nullable().default(null),
notify: tNullable(t.string), notify: zNotify.nullable().default(null),
notifyChannel: tNullable(t.string), notifyChannel: zSnowflake.nullable().default(null),
remove_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])), remove_roles_on_mute: z
restore_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])), .union([z.boolean(), z.array(zSnowflake)])
postInCaseLog: tNullable(t.boolean), .nullable()
hide_case: tNullable(t.boolean), .default(null),
restore_roles_on_mute: z
.union([z.boolean(), z.array(zSnowflake)])
.nullable()
.default(null),
postInCaseLog: z.boolean().nullable().default(null),
hide_case: z.boolean().nullable().default(false),
}), }),
defaultConfig: {
notify: null, // Use defaults from ModActions
hide_case: false,
},
async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) { async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) {
const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined; const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined;
const reason = actionConfig.reason || "Muted automatically"; const reason = actionConfig.reason || "Muted automatically";

View file

@ -0,0 +1,17 @@
import { GuildFeature } from "discord.js";
import z from "zod";
import { automodAction } from "../helpers";
export const PauseInvitesAction = automodAction({
configSchema: z.strictObject({
paused: z.boolean(),
}),
async apply({ pluginData, actionConfig }) {
const hasInvitesDisabled = pluginData.guild.features.includes(GuildFeature.InvitesDisabled);
if (actionConfig.paused !== hasInvitesDisabled) {
await pluginData.guild.disableInvites(actionConfig.paused);
}
},
});

View file

@ -1,6 +1,6 @@
import { PermissionFlagsBits, Snowflake } from "discord.js"; import { PermissionFlagsBits, Snowflake } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { nonNullish, unique } from "../../../utils"; import { nonNullish, unique, zSnowflake } from "../../../utils";
import { canAssignRole } from "../../../utils/canAssignRole"; import { canAssignRole } from "../../../utils/canAssignRole";
import { getMissingPermissions } from "../../../utils/getMissingPermissions"; import { getMissingPermissions } from "../../../utils/getMissingPermissions";
import { memberRolesLock } from "../../../utils/lockNameHelpers"; import { memberRolesLock } from "../../../utils/lockNameHelpers";
@ -12,9 +12,7 @@ import { automodAction } from "../helpers";
const p = PermissionFlagsBits; const p = PermissionFlagsBits;
export const RemoveRolesAction = automodAction({ export const RemoveRolesAction = automodAction({
configType: t.array(t.string), configSchema: z.array(zSnowflake).default([]),
defaultConfig: [],
async apply({ pluginData, contexts, actionConfig, ruleName }) { async apply({ pluginData, contexts, actionConfig, ruleName }) {
const members = unique(contexts.map((c) => c.member).filter(nonNullish)); const members = unique(contexts.map((c) => c.member).filter(nonNullish));

View file

@ -1,16 +1,16 @@
import { GuildTextBasedChannel, MessageCreateOptions, PermissionsBitField, Snowflake, User } from "discord.js"; import { GuildTextBasedChannel, MessageCreateOptions, PermissionsBitField, Snowflake, User } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter"; import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { import {
convertDelayStringToMS, convertDelayStringToMS,
noop, noop,
renderRecursively, renderRecursively,
tDelayString,
tMessageContent,
tNullable,
unique, unique,
validateAndParseMessageContent, validateAndParseMessageContent,
verboseChannelMention, verboseChannelMention,
zBoundedCharacters,
zDelayString,
zMessageContent,
} from "../../../utils"; } from "../../../utils";
import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions"; import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions";
import { messageIsEmpty } from "../../../utils/messageIsEmpty"; import { messageIsEmpty } from "../../../utils/messageIsEmpty";
@ -20,17 +20,15 @@ import { automodAction } from "../helpers";
import { AutomodContext } from "../types"; import { AutomodContext } from "../types";
export const ReplyAction = automodAction({ export const ReplyAction = automodAction({
configType: t.union([ configSchema: z.union([
t.string, zBoundedCharacters(0, 4000),
t.type({ z.strictObject({
text: tMessageContent, text: zMessageContent,
auto_delete: tNullable(t.union([tDelayString, t.number])), auto_delete: z.union([zDelayString, z.number()]).nullable().default(null),
inline: tNullable(t.boolean), inline: z.boolean().default(false),
}), }),
]), ]),
defaultConfig: {},
async apply({ pluginData, contexts, actionConfig, ruleName }) { async apply({ pluginData, contexts, actionConfig, ruleName }) {
const contextsWithTextChannels = contexts const contextsWithTextChannels = contexts
.filter((c) => c.message?.channel_id) .filter((c) => c.message?.channel_id)
@ -60,10 +58,21 @@ export const ReplyAction = automodAction({
}), }),
); );
const formatted = let formatted: string | MessageCreateOptions;
try {
formatted =
typeof actionConfig === "string" typeof actionConfig === "string"
? await renderReplyText(actionConfig) ? await renderReplyText(actionConfig)
: ((await renderRecursively(actionConfig.text, renderReplyText)) as MessageCreateOptions); : ((await renderRecursively(actionConfig.text, renderReplyText)) as MessageCreateOptions);
} catch (err) {
if (err instanceof TemplateParseError) {
pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Error in reply format of automod rule \`${ruleName}\`: ${err.message}`,
});
return;
}
throw err;
}
if (formatted) { if (formatted) {
const channel = pluginData.guild.channels.cache.get(channelId as Snowflake) as GuildTextBasedChannel; const channel = pluginData.guild.channels.cache.get(channelId as Snowflake) as GuildTextBasedChannel;

View file

@ -1,11 +1,9 @@
import * as t from "io-ts"; import { zBoundedCharacters } from "../../../utils";
import { tNullable } from "../../../utils";
import { setAntiraidLevel } from "../functions/setAntiraidLevel"; import { setAntiraidLevel } from "../functions/setAntiraidLevel";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const SetAntiraidLevelAction = automodAction({ export const SetAntiraidLevelAction = automodAction({
configType: tNullable(t.string), configSchema: zBoundedCharacters(0, 100).nullable(),
defaultConfig: "",
async apply({ pluginData, actionConfig }) { async apply({ pluginData, actionConfig }) {
setAntiraidLevel(pluginData, actionConfig ?? null); setAntiraidLevel(pluginData, actionConfig ?? null);

View file

@ -1,16 +1,16 @@
import * as t from "io-ts"; import z from "zod";
import { MAX_COUNTER_VALUE, MIN_COUNTER_VALUE } from "../../../data/GuildCounters";
import { zBoundedCharacters } from "../../../utils";
import { CountersPlugin } from "../../Counters/CountersPlugin"; import { CountersPlugin } from "../../Counters/CountersPlugin";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const SetCounterAction = automodAction({ export const SetCounterAction = automodAction({
configType: t.type({ configSchema: z.strictObject({
counter: t.string, counter: zBoundedCharacters(0, 100),
value: t.number, value: z.number().min(MIN_COUNTER_VALUE).max(MAX_COUNTER_VALUE),
}), }),
defaultConfig: {},
async apply({ pluginData, contexts, actionConfig, ruleName }) { async apply({ pluginData, contexts, actionConfig, ruleName }) {
const countersPlugin = pluginData.getPlugin(CountersPlugin); const countersPlugin = pluginData.getPlugin(CountersPlugin);
if (!countersPlugin.counterExists(actionConfig.counter)) { if (!countersPlugin.counterExists(actionConfig.counter)) {

View file

@ -1,19 +1,15 @@
import { ChannelType, GuildTextBasedChannel, Snowflake } from "discord.js"; import { ChannelType, GuildTextBasedChannel, Snowflake } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { convertDelayStringToMS, isDiscordAPIError, tDelayString, tNullable } from "../../../utils"; import { convertDelayStringToMS, isDiscordAPIError, zDelayString, zSnowflake } from "../../../utils";
import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const SetSlowmodeAction = automodAction({ export const SetSlowmodeAction = automodAction({
configType: t.type({ configSchema: z.strictObject({
channels: tNullable(t.array(t.string)), channels: z.array(zSnowflake),
duration: tNullable(tDelayString), duration: zDelayString.nullable().default("10s"),
}), }),
defaultConfig: {
duration: "10s",
},
async apply({ pluginData, actionConfig, contexts }) { async apply({ pluginData, actionConfig, contexts }) {
const slowmodeMs = Math.max(actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : 0, 0); const slowmodeMs = Math.max(actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : 0, 0);
const channels: Snowflake[] = actionConfig.channels ?? []; const channels: Snowflake[] = actionConfig.channels ?? [];

View file

@ -1,14 +1,9 @@
import { import { ChannelType, GuildTextThreadCreateOptions, ThreadAutoArchiveDuration, ThreadChannel } from "discord.js";
ChannelType, import z from "zod";
GuildFeature, import { TemplateParseError, TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
GuildTextThreadCreateOptions, import { MINUTES, convertDelayStringToMS, noop, zBoundedCharacters, zDelayString } from "../../../utils";
ThreadAutoArchiveDuration,
ThreadChannel,
} from "discord.js";
import * as t from "io-ts";
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
import { MINUTES, convertDelayStringToMS, noop, tDelayString, tNullable } from "../../../utils";
import { savedMessageToTemplateSafeSavedMessage, userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; import { savedMessageToTemplateSafeSavedMessage, userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
import { LogsPlugin } from "../../Logs/LogsPlugin";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
const validThreadAutoArchiveDurations: ThreadAutoArchiveDuration[] = [ const validThreadAutoArchiveDurations: ThreadAutoArchiveDuration[] = [
@ -19,19 +14,15 @@ const validThreadAutoArchiveDurations: ThreadAutoArchiveDuration[] = [
]; ];
export const StartThreadAction = automodAction({ export const StartThreadAction = automodAction({
configType: t.type({ configSchema: z.strictObject({
name: tNullable(t.string), name: zBoundedCharacters(1, 100).nullable(),
auto_archive: tDelayString, auto_archive: zDelayString,
private: tNullable(t.boolean), private: z.boolean().default(false),
slowmode: tNullable(tDelayString), slowmode: zDelayString.nullable().default(null),
limit_per_channel: tNullable(t.number), limit_per_channel: z.number().nullable().default(5),
}), }),
defaultConfig: { async apply({ pluginData, contexts, actionConfig, ruleName }) {
limit_per_channel: 5,
},
async apply({ pluginData, contexts, actionConfig }) {
// check if the message still exists, we don't want to create threads for deleted messages // check if the message still exists, we don't want to create threads for deleted messages
const threads = contexts.filter((c) => { const threads = contexts.filter((c) => {
if (!c.message || !c.user) return false; if (!c.message || !c.user) return false;
@ -47,7 +38,6 @@ export const StartThreadAction = automodAction({
return true; return true;
}); });
const guild = pluginData.guild;
const archiveSet = actionConfig.auto_archive const archiveSet = actionConfig.auto_archive
? Math.ceil(Math.max(convertDelayStringToMS(actionConfig.auto_archive) ?? 0, 0) / MINUTES) ? Math.ceil(Math.max(convertDelayStringToMS(actionConfig.auto_archive) ?? 0, 0) / MINUTES)
: ThreadAutoArchiveDuration.OneDay; : ThreadAutoArchiveDuration.OneDay;
@ -57,24 +47,31 @@ export const StartThreadAction = automodAction({
for (const threadContext of threads) { for (const threadContext of threads) {
const channel = pluginData.guild.channels.cache.get(threadContext.message!.channel_id); const channel = pluginData.guild.channels.cache.get(threadContext.message!.channel_id);
if (!channel || !("threads" in channel) || channel.type === ChannelType.GuildForum) continue; if (!channel || !("threads" in channel) || channel.isThreadOnly()) continue;
const renderThreadName = async (str: string) => let threadName: string;
renderTemplate( try {
str, threadName = await renderTemplate(
actionConfig.name ?? "{user.renderedUsername}'s thread",
new TemplateSafeValueContainer({ new TemplateSafeValueContainer({
user: userToTemplateSafeUser(threadContext.user!), user: userToTemplateSafeUser(threadContext.user!),
msg: savedMessageToTemplateSafeSavedMessage(threadContext.message!), msg: savedMessageToTemplateSafeSavedMessage(threadContext.message!),
}), }),
); );
const threadName = await renderThreadName(actionConfig.name ?? "{user.renderedUsername}'s thread"); } catch (err) {
if (err instanceof TemplateParseError) {
pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Error in thread name format of automod rule ${ruleName}: ${err.message}`,
});
return;
}
throw err;
}
const threadOptions: GuildTextThreadCreateOptions<unknown> = { const threadOptions: GuildTextThreadCreateOptions<unknown> = {
name: threadName, name: threadName,
autoArchiveDuration: autoArchive, autoArchiveDuration: autoArchive,
startMessage: startMessage: !actionConfig.private ? threadContext.message!.id : undefined,
!actionConfig.private && guild.features.includes(GuildFeature.PrivateThreads)
? threadContext.message!.id
: undefined,
}; };
let thread: ThreadChannel | undefined; let thread: ThreadChannel | undefined;
@ -90,10 +87,7 @@ export const StartThreadAction = automodAction({
.create({ .create({
...threadOptions, ...threadOptions,
type: actionConfig.private ? ChannelType.PrivateThread : ChannelType.PublicThread, type: actionConfig.private ? ChannelType.PrivateThread : ChannelType.PublicThread,
startMessage: startMessage: !actionConfig.private ? threadContext.message!.id : undefined,
!actionConfig.private && guild.features.includes(GuildFeature.PrivateThreads)
? threadContext.message!.id
: undefined,
}) })
.catch(() => undefined); .catch(() => undefined);
} }

View file

@ -1,24 +1,20 @@
import * as t from "io-ts"; import z from "zod";
import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils"; import { asyncMap, nonNullish, resolveMember, unique, zBoundedCharacters, zSnowflake } from "../../../utils";
import { CaseArgs } from "../../Cases/types"; import { CaseArgs } from "../../Cases/types";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
import { zNotify } from "../constants";
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
import { automodAction } from "../helpers"; import { automodAction } from "../helpers";
export const WarnAction = automodAction({ export const WarnAction = automodAction({
configType: t.type({ configSchema: z.strictObject({
reason: tNullable(t.string), reason: zBoundedCharacters(0, 4000).nullable().default(null),
notify: tNullable(t.string), notify: zNotify.nullable().default(null),
notifyChannel: tNullable(t.string), notifyChannel: zSnowflake.nullable().default(null),
postInCaseLog: tNullable(t.boolean), postInCaseLog: z.boolean().nullable().default(null),
hide_case: tNullable(t.boolean), hide_case: z.boolean().nullable().default(false),
}), }),
defaultConfig: {
notify: null, // Use defaults from ModActions
hide_case: false,
},
async apply({ pluginData, contexts, actionConfig, matchResult }) { async apply({ pluginData, contexts, actionConfig, matchResult }) {
const reason = actionConfig.reason || "Warned automatically"; const reason = actionConfig.reason || "Warned automatically";
const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined; const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined;

View file

@ -1,3 +1,4 @@
import z from "zod";
import { MINUTES, SECONDS } from "../../utils"; import { MINUTES, SECONDS } from "../../utils";
export const RECENT_SPAM_EXPIRY_TIME = 10 * SECONDS; export const RECENT_SPAM_EXPIRY_TIME = 10 * SECONDS;
@ -18,3 +19,5 @@ export enum RecentActionType {
MemberLeave, MemberLeave,
ThreadCreate, ThreadCreate,
} }
export const zNotify = z.union([z.literal("dm"), z.literal("channel")]);

View file

@ -0,0 +1,10 @@
import { GuildPluginData } from "knub";
import { convertDelayStringToMS } from "../../../utils";
import { AutomodContext, AutomodPluginType, TRule } from "../types";
export function applyCooldown(pluginData: GuildPluginData<AutomodPluginType>, rule: TRule, context: AutomodContext) {
const cooldownKey = `${rule.name}-${context.user?.id}`;
const cooldownTime = convertDelayStringToMS(rule.cooldown, "s");
if (cooldownTime) pluginData.state.cooldownManager.setCooldown(cooldownKey, cooldownTime);
}

View file

@ -1,24 +0,0 @@
import { GuildPluginData } from "knub";
import { convertDelayStringToMS } from "../../../utils";
import { AutomodContext, AutomodPluginType, TRule } from "../types";
export function checkAndUpdateCooldown(
pluginData: GuildPluginData<AutomodPluginType>,
rule: TRule,
context: AutomodContext,
) {
const cooldownKey = `${rule.name}-${context.user?.id}`;
if (cooldownKey) {
if (pluginData.state.cooldownManager.isOnCooldown(cooldownKey)) {
return true;
}
const cooldownTime = convertDelayStringToMS(rule.cooldown, "s");
if (cooldownTime) {
pluginData.state.cooldownManager.setCooldown(cooldownKey, cooldownTime);
}
}
return false;
}

View file

@ -0,0 +1,8 @@
import { GuildPluginData } from "knub";
import { AutomodContext, AutomodPluginType, TRule } from "../types";
export function checkCooldown(pluginData: GuildPluginData<AutomodPluginType>, rule: TRule, context: AutomodContext) {
const cooldownKey = `${rule.name}-${context.user?.id}`;
return pluginData.state.cooldownManager.isOnCooldown(cooldownKey);
}

View file

@ -1,28 +1,27 @@
import * as t from "io-ts"; import z from "zod";
import { SavedMessage } from "../../../data/entities/SavedMessage"; import { SavedMessage } from "../../../data/entities/SavedMessage";
import { humanizeDurationShort } from "../../../humanizeDurationShort"; import { humanizeDurationShort } from "../../../humanizeDurationShort";
import { getBaseUrl } from "../../../pluginUtils"; import { getBaseUrl } from "../../../pluginUtils";
import { convertDelayStringToMS, sorter, tDelayString, tNullable } from "../../../utils"; import { convertDelayStringToMS, sorter, zDelayString } from "../../../utils";
import { RecentActionType } from "../constants"; import { RecentActionType } from "../constants";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
import { findRecentSpam } from "./findRecentSpam"; import { findRecentSpam } from "./findRecentSpam";
import { getMatchingMessageRecentActions } from "./getMatchingMessageRecentActions"; import { getMatchingMessageRecentActions } from "./getMatchingMessageRecentActions";
import { getMessageSpamIdentifier } from "./getSpamIdentifier"; import { getMessageSpamIdentifier } from "./getSpamIdentifier";
const MessageSpamTriggerConfig = t.type({
amount: t.number,
within: tDelayString,
per_channel: tNullable(t.boolean),
});
interface TMessageSpamMatchResultType { interface TMessageSpamMatchResultType {
archiveId: string; archiveId: string;
} }
const configSchema = z.strictObject({
amount: z.number().int(),
within: zDelayString,
per_channel: z.boolean().nullable().default(false),
});
export function createMessageSpamTrigger(spamType: RecentActionType, prettyName: string) { export function createMessageSpamTrigger(spamType: RecentActionType, prettyName: string) {
return automodTrigger<TMessageSpamMatchResultType>()({ return automodTrigger<TMessageSpamMatchResultType>()({
configType: MessageSpamTriggerConfig, configSchema,
defaultConfig: {},
async match({ pluginData, context, triggerConfig }) { async match({ pluginData, context, triggerConfig }) {
if (!context.message) { if (!context.message) {

View file

@ -42,7 +42,7 @@ export async function* matchMultipleTextTypesOnMessage(
} }
if (trigger.match_visible_names) { if (trigger.match_visible_names) {
yield ["visiblename", member.nickname || msg.data.author.username]; yield ["visiblename", member.displayName || msg.data.author.username];
} }
if (trigger.match_usernames) { if (trigger.match_usernames) {

View file

@ -7,7 +7,8 @@ import { CleanAction } from "../actions/clean";
import { AutomodTriggerMatchResult } from "../helpers"; import { AutomodTriggerMatchResult } from "../helpers";
import { availableTriggers } from "../triggers/availableTriggers"; import { availableTriggers } from "../triggers/availableTriggers";
import { AutomodContext, AutomodPluginType } from "../types"; import { AutomodContext, AutomodPluginType } from "../types";
import { checkAndUpdateCooldown } from "./checkAndUpdateCooldown"; import { applyCooldown } from "./applyCooldown";
import { checkCooldown } from "./checkCooldown";
export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>, context: AutomodContext) { export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>, context: AutomodContext) {
const userId = context.user?.id || context.member?.id || context.message?.user_id; const userId = context.user?.id || context.member?.id || context.message?.user_id;
@ -46,7 +47,7 @@ export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>,
} }
if (!rule.affects_self && userId && userId === pluginData.client.user?.id) continue; if (!rule.affects_self && userId && userId === pluginData.client.user?.id) continue;
if (rule.cooldown && checkAndUpdateCooldown(pluginData, rule, context)) { if (rule.cooldown && checkCooldown(pluginData, rule, context)) {
continue; continue;
} }
@ -84,6 +85,8 @@ export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>,
} }
if (matchResult) { if (matchResult) {
if (rule.cooldown) applyCooldown(pluginData, rule, context);
contexts = [context, ...(matchResult.extraContexts || [])]; contexts = [context, ...(matchResult.extraContexts || [])];
for (const _context of contexts) { for (const _context of contexts) {

View file

@ -1,5 +1,5 @@
import * as t from "io-ts";
import { GuildPluginData } from "knub"; import { GuildPluginData } from "knub";
import z, { ZodTypeAny } from "zod";
import { Awaitable } from "../../utils/typeUtils"; import { Awaitable } from "../../utils/typeUtils";
import { AutomodContext, AutomodPluginType } from "./types"; import { AutomodContext, AutomodPluginType } from "./types";
@ -31,21 +31,19 @@ type AutomodTriggerRenderMatchInformationFn<TConfigType, TMatchResultExtra> = (m
matchResult: AutomodTriggerMatchResult<TMatchResultExtra>; matchResult: AutomodTriggerMatchResult<TMatchResultExtra>;
}) => Awaitable<string>; }) => Awaitable<string>;
export interface AutomodTriggerBlueprint<TConfigType extends t.Any, TMatchResultExtra> { export interface AutomodTriggerBlueprint<TConfigSchema extends ZodTypeAny, TMatchResultExtra> {
configType: TConfigType; configSchema: TConfigSchema;
defaultConfig: Partial<t.TypeOf<TConfigType>>; match: AutomodTriggerMatchFn<z.output<TConfigSchema>, TMatchResultExtra>;
renderMatchInformation: AutomodTriggerRenderMatchInformationFn<z.output<TConfigSchema>, TMatchResultExtra>;
match: AutomodTriggerMatchFn<t.TypeOf<TConfigType>, TMatchResultExtra>;
renderMatchInformation: AutomodTriggerRenderMatchInformationFn<t.TypeOf<TConfigType>, TMatchResultExtra>;
} }
export function automodTrigger<TMatchResultExtra>(): <TConfigType extends t.Any>( export function automodTrigger<TMatchResultExtra>(): <TConfigSchema extends ZodTypeAny>(
blueprint: AutomodTriggerBlueprint<TConfigType, TMatchResultExtra>, blueprint: AutomodTriggerBlueprint<TConfigSchema, TMatchResultExtra>,
) => AutomodTriggerBlueprint<TConfigType, TMatchResultExtra>; ) => AutomodTriggerBlueprint<TConfigSchema, TMatchResultExtra>;
export function automodTrigger<TConfigType extends t.Any>( export function automodTrigger<TConfigSchema extends ZodTypeAny>(
blueprint: AutomodTriggerBlueprint<TConfigType, unknown>, blueprint: AutomodTriggerBlueprint<TConfigSchema, unknown>,
): AutomodTriggerBlueprint<TConfigType, unknown>; ): AutomodTriggerBlueprint<TConfigSchema, unknown>;
export function automodTrigger(...args) { export function automodTrigger(...args) {
if (args.length) { if (args.length) {
@ -63,15 +61,13 @@ type AutomodActionApplyFn<TConfigType> = (meta: {
matchResult: AutomodTriggerMatchResult; matchResult: AutomodTriggerMatchResult;
}) => Awaitable<void>; }) => Awaitable<void>;
export interface AutomodActionBlueprint<TConfigType extends t.Any> { export interface AutomodActionBlueprint<TConfigSchema extends ZodTypeAny> {
configType: TConfigType; configSchema: TConfigSchema;
defaultConfig: Partial<t.TypeOf<TConfigType>>; apply: AutomodActionApplyFn<z.output<TConfigSchema>>;
apply: AutomodActionApplyFn<t.TypeOf<TConfigType>>;
} }
export function automodAction<TConfigType extends t.Any>( export function automodAction<TConfigSchema extends ZodTypeAny>(
blueprint: AutomodActionBlueprint<TConfigType>, blueprint: AutomodActionBlueprint<TConfigSchema>,
): AutomodActionBlueprint<TConfigType> { ): AutomodActionBlueprint<TConfigSchema> {
return blueprint; return blueprint;
} }

View file

@ -1,8 +1,8 @@
import { ZeppelinPluginInfo } from "../../types";
import { trimPluginDescription } from "../../utils"; import { trimPluginDescription } from "../../utils";
import { ZeppelinGuildPluginBlueprint } from "../ZeppelinPluginBlueprint";
import { ConfigSchema } from "./types";
export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = { export const automodPluginInfo: ZeppelinPluginInfo = {
showInDocs: true,
prettyName: "Automod", prettyName: "Automod",
description: trimPluginDescription(` description: trimPluginDescription(`
Allows specifying automated actions in response to triggers. Example use cases include word filtering and spam prevention. Allows specifying automated actions in response to triggers. Example use cases include word filtering and spam prevention.
@ -100,5 +100,4 @@ export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = {
{matchSummary} {matchSummary}
~~~ ~~~
`), `),
configSchema: ConfigSchema,
}; };

View file

@ -1,16 +1,15 @@
import * as t from "io-ts"; import z from "zod";
import { tNullable } from "../../../utils";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
interface AntiraidLevelTriggerResult {} interface AntiraidLevelTriggerResult {}
export const AntiraidLevelTrigger = automodTrigger<AntiraidLevelTriggerResult>()({ const configSchema = z.strictObject({
configType: t.type({ level: z.nullable(z.string().max(100)),
level: tNullable(t.string), only_on_change: z.nullable(z.boolean()),
only_on_change: tNullable(t.boolean), });
}),
defaultConfig: {}, export const AntiraidLevelTrigger = automodTrigger<AntiraidLevelTriggerResult>()({
configSchema,
async match({ triggerConfig, context }) { async match({ triggerConfig, context }) {
if (!context.antiraid) { if (!context.antiraid) {

View file

@ -1,14 +1,14 @@
import { Snowflake } from "discord.js"; import { Snowflake } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { verboseChannelMention } from "../../../utils"; import { verboseChannelMention } from "../../../utils";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
interface AnyMessageResultType {} interface AnyMessageResultType {}
export const AnyMessageTrigger = automodTrigger<AnyMessageResultType>()({ const configSchema = z.strictObject({});
configType: t.type({}),
defaultConfig: {}, export const AnyMessageTrigger = automodTrigger<AnyMessageResultType>()({
configSchema,
async match({ context }) { async match({ context }) {
if (!context.message) { if (!context.message) {

View file

@ -1,4 +1,3 @@
import * as t from "io-ts";
import { AutomodTriggerBlueprint } from "../helpers"; import { AutomodTriggerBlueprint } from "../helpers";
import { AntiraidLevelTrigger } from "./antiraidLevel"; import { AntiraidLevelTrigger } from "./antiraidLevel";
import { AnyMessageTrigger } from "./anyMessage"; import { AnyMessageTrigger } from "./anyMessage";
@ -45,6 +44,7 @@ export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>
match_attachment_type: MatchAttachmentTypeTrigger, match_attachment_type: MatchAttachmentTypeTrigger,
match_mime_type: MatchMimeTypeTrigger, match_mime_type: MatchMimeTypeTrigger,
member_join: MemberJoinTrigger, member_join: MemberJoinTrigger,
member_leave: MemberLeaveTrigger,
role_added: RoleAddedTrigger, role_added: RoleAddedTrigger,
role_removed: RoleRemovedTrigger, role_removed: RoleRemovedTrigger,
@ -76,46 +76,3 @@ export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>
thread_archive: ThreadArchiveTrigger, thread_archive: ThreadArchiveTrigger,
thread_unarchive: ThreadUnarchiveTrigger, thread_unarchive: ThreadUnarchiveTrigger,
}; };
export const AvailableTriggers = t.type({
any_message: AnyMessageTrigger.configType,
match_words: MatchWordsTrigger.configType,
match_regex: MatchRegexTrigger.configType,
match_invites: MatchInvitesTrigger.configType,
match_links: MatchLinksTrigger.configType,
match_attachment_type: MatchAttachmentTypeTrigger.configType,
match_mime_type: MatchMimeTypeTrigger.configType,
member_join: MemberJoinTrigger.configType,
member_leave: MemberLeaveTrigger.configType,
role_added: RoleAddedTrigger.configType,
role_removed: RoleRemovedTrigger.configType,
message_spam: MessageSpamTrigger.configType,
mention_spam: MentionSpamTrigger.configType,
link_spam: LinkSpamTrigger.configType,
attachment_spam: AttachmentSpamTrigger.configType,
emoji_spam: EmojiSpamTrigger.configType,
line_spam: LineSpamTrigger.configType,
character_spam: CharacterSpamTrigger.configType,
member_join_spam: MemberJoinSpamTrigger.configType,
sticker_spam: StickerSpamTrigger.configType,
thread_create_spam: ThreadCreateSpamTrigger.configType,
counter_trigger: CounterTrigger.configType,
note: NoteTrigger.configType,
warn: WarnTrigger.configType,
mute: MuteTrigger.configType,
unmute: UnmuteTrigger.configType,
kick: KickTrigger.configType,
ban: BanTrigger.configType,
unban: UnbanTrigger.configType,
antiraid_level: AntiraidLevelTrigger.configType,
thread_create: ThreadCreateTrigger.configType,
thread_delete: ThreadDeleteTrigger.configType,
thread_archive: ThreadArchiveTrigger.configType,
thread_unarchive: ThreadUnarchiveTrigger.configType,
});

View file

@ -1,19 +1,16 @@
import * as t from "io-ts"; import z from "zod";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface // tslint:disable-next-line:no-empty-interface
interface BanTriggerResultType {} interface BanTriggerResultType {}
export const BanTrigger = automodTrigger<BanTriggerResultType>()({ const configSchema = z.strictObject({
configType: t.type({ manual: z.boolean().default(true),
manual: t.boolean, automatic: z.boolean().default(true),
automatic: t.boolean, });
}),
defaultConfig: { export const BanTrigger = automodTrigger<BanTriggerResultType>()({
manual: true, configSchema,
automatic: true,
},
async match({ context, triggerConfig }) { async match({ context, triggerConfig }) {
if (context.modAction?.type !== "ban") { if (context.modAction?.type !== "ban") {

View file

@ -1,18 +1,17 @@
import * as t from "io-ts"; import z from "zod";
import { tNullable } from "../../../utils";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
// tslint:disable-next-line // tslint:disable-next-line
interface CounterTriggerResult {} interface CounterTriggerResult {}
export const CounterTrigger = automodTrigger<CounterTriggerResult>()({ const configSchema = z.strictObject({
configType: t.type({ counter: z.string().max(100),
counter: t.string, trigger: z.string().max(100),
trigger: t.string, reverse: z.boolean().optional(),
reverse: tNullable(t.boolean), });
}),
defaultConfig: {}, export const CounterTrigger = automodTrigger<CounterTriggerResult>()({
configSchema,
async match({ triggerConfig, context }) { async match({ triggerConfig, context }) {
if (!context.counterTrigger) { if (!context.counterTrigger) {

View file

@ -1,18 +1,16 @@
import * as t from "io-ts"; import z from "zod";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
interface ExampleMatchResultType { interface ExampleMatchResultType {
isBanana: boolean; isBanana: boolean;
} }
export const ExampleTrigger = automodTrigger<ExampleMatchResultType>()({ const configSchema = z.strictObject({
configType: t.type({ allowedFruits: z.array(z.string().max(100)).max(50).default(["peach", "banana"]),
allowedFruits: t.array(t.string), });
}),
defaultConfig: { export const ExampleTrigger = automodTrigger<ExampleMatchResultType>()({
allowedFruits: ["peach", "banana"], configSchema,
},
async match({ triggerConfig, context }) { async match({ triggerConfig, context }) {
const foundFruit = triggerConfig.allowedFruits.find((fruit) => context.message?.data.content === fruit); const foundFruit = triggerConfig.allowedFruits.find((fruit) => context.message?.data.content === fruit);

View file

@ -1,19 +1,16 @@
import * as t from "io-ts"; import z from "zod";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface // tslint:disable-next-line:no-empty-interface
interface KickTriggerResultType {} interface KickTriggerResultType {}
export const KickTrigger = automodTrigger<KickTriggerResultType>()({ const configSchema = z.strictObject({
configType: t.type({ manual: z.boolean().default(true),
manual: t.boolean, automatic: z.boolean().default(true),
automatic: t.boolean, });
}),
defaultConfig: { export const KickTrigger = automodTrigger<KickTriggerResultType>()({
manual: true, configSchema,
automatic: true,
},
async match({ context, triggerConfig }) { async match({ context, triggerConfig }) {
if (context.modAction?.type !== "kick") { if (context.modAction?.type !== "kick") {

View file

@ -1,6 +1,6 @@
import { escapeInlineCode, Snowflake } from "discord.js"; import { escapeInlineCode, Snowflake } from "discord.js";
import * as t from "io-ts";
import { extname } from "path"; import { extname } from "path";
import z from "zod";
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils"; import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
@ -9,20 +9,33 @@ interface MatchResultType {
mode: "blacklist" | "whitelist"; mode: "blacklist" | "whitelist";
} }
export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({ const configSchema = z
configType: t.type({ .strictObject({
filetype_blacklist: t.array(t.string), filetype_blacklist: z.array(z.string().max(32)).max(255).default([]),
blacklist_enabled: t.boolean, blacklist_enabled: z.boolean().default(false),
filetype_whitelist: t.array(t.string), filetype_whitelist: z.array(z.string().max(32)).max(255).default([]),
whitelist_enabled: t.boolean, whitelist_enabled: z.boolean().default(false),
}), })
.transform((parsed, ctx) => {
if (parsed.blacklist_enabled && parsed.whitelist_enabled) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Cannot have both blacklist and whitelist enabled",
});
return z.NEVER;
}
if (!parsed.blacklist_enabled && !parsed.whitelist_enabled) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Must have either blacklist or whitelist enabled",
});
return z.NEVER;
}
return parsed;
});
defaultConfig: { export const MatchAttachmentTypeTrigger = automodTrigger<MatchResultType>()({
filetype_blacklist: [], configSchema,
blacklist_enabled: false,
filetype_whitelist: [],
whitelist_enabled: false,
},
async match({ context, triggerConfig: trigger }) { async match({ context, triggerConfig: trigger }) {
if (!context.message) { if (!context.message) {

View file

@ -1,5 +1,5 @@
import * as t from "io-ts"; import z from "zod";
import { getInviteCodesInString, GuildInvite, isGuildInvite, resolveInvite, tNullable } from "../../../utils"; import { getInviteCodesInString, GuildInvite, isGuildInvite, resolveInvite, zSnowflake } from "../../../utils";
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary"; import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary";
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage"; import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
@ -10,30 +10,22 @@ interface MatchResultType {
invite?: GuildInvite; invite?: GuildInvite;
} }
export const MatchInvitesTrigger = automodTrigger<MatchResultType>()({ const configSchema = z.strictObject({
configType: t.type({ include_guilds: z.array(zSnowflake).max(255).optional(),
include_guilds: tNullable(t.array(t.string)), exclude_guilds: z.array(zSnowflake).max(255).optional(),
exclude_guilds: tNullable(t.array(t.string)), include_invite_codes: z.array(z.string().max(32)).max(255).optional(),
include_invite_codes: tNullable(t.array(t.string)), exclude_invite_codes: z.array(z.string().max(32)).max(255).optional(),
exclude_invite_codes: tNullable(t.array(t.string)), allow_group_dm_invites: z.boolean().default(false),
allow_group_dm_invites: t.boolean, match_messages: z.boolean().default(true),
match_messages: t.boolean, match_embeds: z.boolean().default(false),
match_embeds: t.boolean, match_visible_names: z.boolean().default(false),
match_visible_names: t.boolean, match_usernames: z.boolean().default(false),
match_usernames: t.boolean, match_nicknames: z.boolean().default(false),
match_nicknames: t.boolean, match_custom_status: z.boolean().default(false),
match_custom_status: t.boolean, });
}),
defaultConfig: { export const MatchInvitesTrigger = automodTrigger<MatchResultType>()({
allow_group_dm_invites: false, configSchema,
match_messages: true,
match_embeds: false,
match_visible_names: false,
match_usernames: false,
match_nicknames: false,
match_custom_status: false,
},
async match({ pluginData, context, triggerConfig: trigger }) { async match({ pluginData, context, triggerConfig: trigger }) {
if (!context.message) { if (!context.message) {

View file

@ -1,11 +1,10 @@
import { escapeInlineCode } from "discord.js"; import { escapeInlineCode } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { allowTimeout } from "../../../RegExpRunner"; import { allowTimeout } from "../../../RegExpRunner";
import { phishermanDomainIsSafe } from "../../../data/Phisherman"; import { phishermanDomainIsSafe } from "../../../data/Phisherman";
import { getUrlsInString, tNullable } from "../../../utils"; import { getUrlsInString, zRegex } from "../../../utils";
import { mergeRegexes } from "../../../utils/mergeRegexes"; import { mergeRegexes } from "../../../utils/mergeRegexes";
import { mergeWordsIntoRegex } from "../../../utils/mergeWordsIntoRegex"; import { mergeWordsIntoRegex } from "../../../utils/mergeWordsIntoRegex";
import { TRegex } from "../../../validatorUtils";
import { PhishermanPlugin } from "../../Phisherman/PhishermanPlugin"; import { PhishermanPlugin } from "../../Phisherman/PhishermanPlugin";
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary"; import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary";
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage"; import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage";
@ -21,40 +20,37 @@ const regexCache = new WeakMap<any, RegExp[]>();
const quickLinkCheck = /^https?:\/\//i; const quickLinkCheck = /^https?:\/\//i;
export const MatchLinksTrigger = automodTrigger<MatchResultType>()({ const configSchema = z.strictObject({
configType: t.type({ include_domains: z.array(z.string().max(255)).max(700).optional(),
include_domains: tNullable(t.array(t.string)), exclude_domains: z.array(z.string().max(255)).max(700).optional(),
exclude_domains: tNullable(t.array(t.string)), include_subdomains: z.boolean().default(true),
include_subdomains: t.boolean, include_words: z.array(z.string().max(2000)).max(700).optional(),
include_words: tNullable(t.array(t.string)), exclude_words: z.array(z.string().max(2000)).max(700).optional(),
exclude_words: tNullable(t.array(t.string)), include_regex: z
include_regex: tNullable(t.array(TRegex)), .array(zRegex(z.string().max(2000)))
exclude_regex: tNullable(t.array(TRegex)), .max(512)
phisherman: tNullable( .optional(),
t.type({ exclude_regex: z
include_suspected: tNullable(t.boolean), .array(zRegex(z.string().max(2000)))
include_verified: tNullable(t.boolean), .max(512)
}), .optional(),
), phisherman: z
only_real_links: t.boolean, .strictObject({
match_messages: t.boolean, include_suspected: z.boolean().optional(),
match_embeds: t.boolean, include_verified: z.boolean().optional(),
match_visible_names: t.boolean, })
match_usernames: t.boolean, .optional(),
match_nicknames: t.boolean, only_real_links: z.boolean().default(true),
match_custom_status: t.boolean, match_messages: z.boolean().default(true),
}), match_embeds: z.boolean().default(true),
match_visible_names: z.boolean().default(false),
match_usernames: z.boolean().default(false),
match_nicknames: z.boolean().default(false),
match_custom_status: z.boolean().default(false),
});
defaultConfig: { export const MatchLinksTrigger = automodTrigger<MatchResultType>()({
include_subdomains: true, configSchema,
match_messages: true,
match_embeds: false,
match_visible_names: false,
match_usernames: false,
match_nicknames: false,
match_custom_status: false,
only_real_links: true,
},
async match({ pluginData, context, triggerConfig: trigger }) { async match({ pluginData, context, triggerConfig: trigger }) {
if (!context.message) { if (!context.message) {

View file

@ -1,5 +1,5 @@
import { escapeInlineCode } from "discord.js"; import { escapeInlineCode } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils"; import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
@ -8,20 +8,33 @@ interface MatchResultType {
mode: "blacklist" | "whitelist"; mode: "blacklist" | "whitelist";
} }
export const MatchMimeTypeTrigger = automodTrigger<MatchResultType>()({ const configSchema = z
configType: t.type({ .strictObject({
mime_type_blacklist: t.array(t.string), mime_type_blacklist: z.array(z.string().max(255)).max(255).default([]),
blacklist_enabled: t.boolean, blacklist_enabled: z.boolean().default(false),
mime_type_whitelist: t.array(t.string), mime_type_whitelist: z.array(z.string().max(255)).max(255).default([]),
whitelist_enabled: t.boolean, whitelist_enabled: z.boolean().default(false),
}), })
.transform((parsed, ctx) => {
if (parsed.blacklist_enabled && parsed.whitelist_enabled) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Cannot have both blacklist and whitelist enabled",
});
return z.NEVER;
}
if (!parsed.blacklist_enabled && !parsed.whitelist_enabled) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Must have either blacklist or whitelist enabled",
});
return z.NEVER;
}
return parsed;
});
defaultConfig: { export const MatchMimeTypeTrigger = automodTrigger<MatchResultType>()({
mime_type_blacklist: [], configSchema,
blacklist_enabled: false,
mime_type_whitelist: [],
whitelist_enabled: false,
},
async match({ context, triggerConfig: trigger }) { async match({ context, triggerConfig: trigger }) {
if (!context.message) return; if (!context.message) return;

View file

@ -1,9 +1,9 @@
import * as t from "io-ts"; import z from "zod";
import { allowTimeout } from "../../../RegExpRunner"; import { allowTimeout } from "../../../RegExpRunner";
import { zRegex } from "../../../utils";
import { mergeRegexes } from "../../../utils/mergeRegexes"; import { mergeRegexes } from "../../../utils/mergeRegexes";
import { normalizeText } from "../../../utils/normalizeText"; import { normalizeText } from "../../../utils/normalizeText";
import { stripMarkdown } from "../../../utils/stripMarkdown"; import { stripMarkdown } from "../../../utils/stripMarkdown";
import { TRegex } from "../../../validatorUtils";
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary"; import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary";
import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage"; import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
@ -13,33 +13,23 @@ interface MatchResultType {
type: MatchableTextType; type: MatchableTextType;
} }
const configSchema = z.strictObject({
patterns: z.array(zRegex(z.string().max(2000))).max(512),
case_sensitive: z.boolean().default(false),
normalize: z.boolean().default(false),
strip_markdown: z.boolean().default(false),
match_messages: z.boolean().default(true),
match_embeds: z.boolean().default(false),
match_visible_names: z.boolean().default(false),
match_usernames: z.boolean().default(false),
match_nicknames: z.boolean().default(false),
match_custom_status: z.boolean().default(false),
});
const regexCache = new WeakMap<any, RegExp[]>(); const regexCache = new WeakMap<any, RegExp[]>();
export const MatchRegexTrigger = automodTrigger<MatchResultType>()({ export const MatchRegexTrigger = automodTrigger<MatchResultType>()({
configType: t.type({ configSchema,
patterns: t.array(TRegex),
case_sensitive: t.boolean,
normalize: t.boolean,
strip_markdown: t.boolean,
match_messages: t.boolean,
match_embeds: t.boolean,
match_visible_names: t.boolean,
match_usernames: t.boolean,
match_nicknames: t.boolean,
match_custom_status: t.boolean,
}),
defaultConfig: {
case_sensitive: false,
normalize: false,
strip_markdown: false,
match_messages: true,
match_embeds: false,
match_visible_names: false,
match_usernames: false,
match_nicknames: false,
match_custom_status: false,
},
async match({ pluginData, context, triggerConfig: trigger }) { async match({ pluginData, context, triggerConfig: trigger }) {
if (!context.message) { if (!context.message) {

View file

@ -1,5 +1,5 @@
import escapeStringRegexp from "escape-string-regexp"; import escapeStringRegexp from "escape-string-regexp";
import * as t from "io-ts"; import z from "zod";
import { normalizeText } from "../../../utils/normalizeText"; import { normalizeText } from "../../../utils/normalizeText";
import { stripMarkdown } from "../../../utils/stripMarkdown"; import { stripMarkdown } from "../../../utils/stripMarkdown";
import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary"; import { getTextMatchPartialSummary } from "../functions/getTextMatchPartialSummary";
@ -13,37 +13,24 @@ interface MatchResultType {
const regexCache = new WeakMap<any, RegExp[]>(); const regexCache = new WeakMap<any, RegExp[]>();
export const MatchWordsTrigger = automodTrigger<MatchResultType>()({ const configSchema = z.strictObject({
configType: t.type({ words: z.array(z.string().max(2000)).max(1024),
words: t.array(t.string), case_sensitive: z.boolean().default(false),
case_sensitive: t.boolean, only_full_words: z.boolean().default(true),
only_full_words: t.boolean, normalize: z.boolean().default(false),
normalize: t.boolean, loose_matching: z.boolean().default(false),
loose_matching: t.boolean, loose_matching_threshold: z.number().int().default(4),
loose_matching_threshold: t.number, strip_markdown: z.boolean().default(false),
strip_markdown: t.boolean, match_messages: z.boolean().default(true),
match_messages: t.boolean, match_embeds: z.boolean().default(false),
match_embeds: t.boolean, match_visible_names: z.boolean().default(false),
match_visible_names: t.boolean, match_usernames: z.boolean().default(false),
match_usernames: t.boolean, match_nicknames: z.boolean().default(false),
match_nicknames: t.boolean, match_custom_status: z.boolean().default(false),
match_custom_status: t.boolean, });
}),
defaultConfig: { export const MatchWordsTrigger = automodTrigger<MatchResultType>()({
case_sensitive: false, configSchema,
only_full_words: true,
normalize: false,
loose_matching: false,
loose_matching_threshold: 4,
strip_markdown: false,
match_messages: true,
match_embeds: false,
match_visible_names: false,
match_usernames: false,
match_nicknames: false,
match_custom_status: false,
},
async match({ pluginData, context, triggerConfig: trigger }) { async match({ pluginData, context, triggerConfig: trigger }) {
if (!context.message) { if (!context.message) {

View file

@ -1,17 +1,14 @@
import * as t from "io-ts"; import z from "zod";
import { convertDelayStringToMS, tDelayString } from "../../../utils"; import { convertDelayStringToMS, zDelayString } from "../../../utils";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
export const MemberJoinTrigger = automodTrigger<unknown>()({ const configSchema = z.strictObject({
configType: t.type({ only_new: z.boolean().default(false),
only_new: t.boolean, new_threshold: zDelayString.default("1h"),
new_threshold: tDelayString, });
}),
defaultConfig: { export const MemberJoinTrigger = automodTrigger<unknown>()({
only_new: false, configSchema,
new_threshold: "1h",
},
async match({ context, triggerConfig }) { async match({ context, triggerConfig }) {
if (!context.joined || !context.member) { if (!context.joined || !context.member) {

View file

@ -1,18 +1,18 @@
import * as t from "io-ts"; import z from "zod";
import { convertDelayStringToMS, tDelayString } from "../../../utils"; import { convertDelayStringToMS, zDelayString } from "../../../utils";
import { RecentActionType } from "../constants"; import { RecentActionType } from "../constants";
import { findRecentSpam } from "../functions/findRecentSpam"; import { findRecentSpam } from "../functions/findRecentSpam";
import { getMatchingRecentActions } from "../functions/getMatchingRecentActions"; import { getMatchingRecentActions } from "../functions/getMatchingRecentActions";
import { sumRecentActionCounts } from "../functions/sumRecentActionCounts"; import { sumRecentActionCounts } from "../functions/sumRecentActionCounts";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
export const MemberJoinSpamTrigger = automodTrigger<unknown>()({ const configSchema = z.strictObject({
configType: t.type({ amount: z.number().int(),
amount: t.number, within: zDelayString,
within: tDelayString, });
}),
defaultConfig: {}, export const MemberJoinSpamTrigger = automodTrigger<unknown>()({
configSchema,
async match({ pluginData, context, triggerConfig }) { async match({ pluginData, context, triggerConfig }) {
if (!context.joined || !context.member) { if (!context.joined || !context.member) {

View file

@ -1,10 +1,10 @@
import * as t from "io-ts"; import z from "zod";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
export const MemberLeaveTrigger = automodTrigger<unknown>()({ const configSchema = z.strictObject({});
configType: t.type({}),
defaultConfig: {}, export const MemberLeaveTrigger = automodTrigger<unknown>()({
configSchema,
async match({ context }) { async match({ context }) {
if (!context.joined || !context.member) { if (!context.joined || !context.member) {

View file

@ -1,19 +1,16 @@
import * as t from "io-ts"; import z from "zod";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface // tslint:disable-next-line:no-empty-interface
interface MuteTriggerResultType {} interface MuteTriggerResultType {}
export const MuteTrigger = automodTrigger<MuteTriggerResultType>()({ const configSchema = z.strictObject({
configType: t.type({ manual: z.boolean().default(true),
manual: t.boolean, automatic: z.boolean().default(true),
automatic: t.boolean, });
}),
defaultConfig: { export const MuteTrigger = automodTrigger<MuteTriggerResultType>()({
manual: true, configSchema,
automatic: true,
},
async match({ context, triggerConfig }) { async match({ context, triggerConfig }) {
if (context.modAction?.type !== "mute") { if (context.modAction?.type !== "mute") {

View file

@ -1,12 +1,13 @@
import * as t from "io-ts"; import z from "zod";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
// tslint:disable-next-line:no-empty-interface // tslint:disable-next-line:no-empty-interface
interface NoteTriggerResultType {} interface NoteTriggerResultType {}
const configSchema = z.strictObject({});
export const NoteTrigger = automodTrigger<NoteTriggerResultType>()({ export const NoteTrigger = automodTrigger<NoteTriggerResultType>()({
configType: t.type({}), configSchema,
defaultConfig: {},
async match({ context }) { async match({ context }) {
if (context.modAction?.type !== "note") { if (context.modAction?.type !== "note") {

View file

@ -1,6 +1,6 @@
import { Snowflake } from "discord.js"; import { Snowflake } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { renderUserUsername } from "../../../utils"; import { renderUsername, zSnowflake } from "../../../utils";
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges"; import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
@ -8,10 +8,10 @@ interface RoleAddedMatchResult {
matchedRoleId: string; matchedRoleId: string;
} }
export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({ const configSchema = z.union([zSnowflake, z.array(zSnowflake).max(255)]).default([]);
configType: t.union([t.string, t.array(t.string)]),
defaultConfig: "", export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({
configSchema,
async match({ triggerConfig, context, pluginData }) { async match({ triggerConfig, context, pluginData }) {
if (!context.member || !context.rolesChanged || context.rolesChanged.added!.length === 0) { if (!context.member || !context.rolesChanged || context.rolesChanged.added!.length === 0) {
@ -38,7 +38,7 @@ export const RoleAddedTrigger = automodTrigger<RoleAddedMatchResult>()({
const role = pluginData.guild.roles.cache.get(matchResult.extra.matchedRoleId as Snowflake); const role = pluginData.guild.roles.cache.get(matchResult.extra.matchedRoleId as Snowflake);
const roleName = role?.name || "Unknown"; const roleName = role?.name || "Unknown";
const member = contexts[0].member!; const member = contexts[0].member!;
const memberName = `**${renderUserUsername(member.user)}** (\`${member.id}\`)`; const memberName = `**${renderUsername(member)}** (\`${member.id}\`)`;
return `Role ${roleName} (\`${matchResult.extra.matchedRoleId}\`) was added to ${memberName}`; return `Role ${roleName} (\`${matchResult.extra.matchedRoleId}\`) was added to ${memberName}`;
}, },
}); });

View file

@ -1,6 +1,6 @@
import { Snowflake } from "discord.js"; import { Snowflake } from "discord.js";
import * as t from "io-ts"; import z from "zod";
import { renderUserUsername } from "../../../utils"; import { renderUsername, zSnowflake } from "../../../utils";
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges"; import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges";
import { automodTrigger } from "../helpers"; import { automodTrigger } from "../helpers";
@ -8,10 +8,10 @@ interface RoleAddedMatchResult {
matchedRoleId: string; matchedRoleId: string;
} }
export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({ const configSchema = z.union([zSnowflake, z.array(zSnowflake).max(255)]).default([]);
configType: t.union([t.string, t.array(t.string)]),
defaultConfig: "", export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({
configSchema,
async match({ triggerConfig, context, pluginData }) { async match({ triggerConfig, context, pluginData }) {
if (!context.member || !context.rolesChanged || context.rolesChanged.removed!.length === 0) { if (!context.member || !context.rolesChanged || context.rolesChanged.removed!.length === 0) {
@ -38,7 +38,7 @@ export const RoleRemovedTrigger = automodTrigger<RoleAddedMatchResult>()({
const role = pluginData.guild.roles.cache.get(matchResult.extra.matchedRoleId as Snowflake); const role = pluginData.guild.roles.cache.get(matchResult.extra.matchedRoleId as Snowflake);
const roleName = role?.name || "Unknown"; const roleName = role?.name || "Unknown";
const member = contexts[0].member!; const member = contexts[0].member!;
const memberName = `**${renderUserUsername(member.user)}** (\`${member.id}\`)`; const memberName = `**${renderUsername(member)}** (\`${member.id}\`)`;
return `Role ${roleName} (\`${matchResult.extra.matchedRoleId}\`) was removed from ${memberName}`; return `Role ${roleName} (\`${matchResult.extra.matchedRoleId}\`) was removed from ${memberName}`;
}, },
}); });

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