mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-18 06:51:51 +00:00
Merge remote-tracking branch 'upstream/master' into defaultreason
This commit is contained in:
commit
7be9274610
352 changed files with 24828 additions and 29302 deletions
5
.clabot
5
.clabot
|
@ -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!"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
|
|
||||||
"service": "devenv",
|
"service": "devenv",
|
||||||
"remoteUser": "ubuntu",
|
"remoteUser": "ubuntu",
|
||||||
"workspaceFolder": "/home/ubuntu/zeppelin"
|
"workspaceFolder": "/workspace/zeppelin"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
76
.env.example
76
.env.example
|
@ -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
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package-lock.json binary
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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
34
Dockerfile
Normal 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
|
|
@ -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...>
|
|
||||||
```
|
|
||||||
|
|
|
@ -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`
|
|
||||||
|
|
|
@ -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
10212
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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") {
|
|
||||||
return schema.types.map((t) => formatConfigSchema(t)).join(" & ");
|
|
||||||
} else {
|
|
||||||
return schema.name;
|
|
||||||
}
|
}
|
||||||
|
if (isZodRecord(schema)) {
|
||||||
|
return "{\n" + indentLines(`[string]: ${formatZodConfigSchema(schema._def.valueType)}`, 2) + "\n}";
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -15,3 +15,9 @@ export function connect() {
|
||||||
|
|
||||||
return connectionPromise;
|
return connectionPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function disconnect() {
|
||||||
|
if (connectionPromise) {
|
||||||
|
connectionPromise.then(() => dataSource.destroy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
23
backend/src/exportSchemas.ts
Normal file
23
backend/src/exportSchemas.ts
Normal 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);
|
|
@ -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);
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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"]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,36 +2,40 @@ 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(`
|
||||||
UPDATE starboard_messages AS sm
|
UPDATE starboard_messages AS sm
|
||||||
JOIN starboards AS sb
|
JOIN starboards AS sb
|
||||||
ON sm.starboard_id = sb.id
|
ON sm.starboard_id = sb.id
|
||||||
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({
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ???
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
8
backend/src/plugins/AutoDelete/info.ts
Normal file
8
backend/src/plugins/AutoDelete/info.ts
Normal 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",
|
||||||
|
};
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
10
backend/src/plugins/AutoReactions/info.ts
Normal file
10
backend/src/plugins/AutoReactions/info.ts
Normal 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
|
||||||
|
`),
|
||||||
|
};
|
|
@ -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;
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
|
||||||
});
|
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
|
@ -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({
|
||||||
new TemplateSafeValueContainer({
|
body: `Error in target format of automod rule ${ruleName}: ${err.message}`,
|
||||||
user: user ? userToTemplateSafeUser(user) : null,
|
});
|
||||||
guild: guildToTemplateSafeGuild(pluginData.guild),
|
return;
|
||||||
message: message ? savedMessageToTemplateSafeSavedMessage(message) : null,
|
}
|
||||||
}),
|
throw err;
|
||||||
);
|
}
|
||||||
const target = await renderTarget(actionConfig.target);
|
|
||||||
const channelId = actionConfig.channel ? await renderChannel(actionConfig.channel) : null;
|
let channelId: string | null = null;
|
||||||
|
if (actionConfig.channel) {
|
||||||
|
try {
|
||||||
|
channelId = await renderTemplate(
|
||||||
|
actionConfig.channel,
|
||||||
|
new TemplateSafeValueContainer({
|
||||||
|
user: user ? userToTemplateSafeUser(user) : null,
|
||||||
|
guild: guildToTemplateSafeGuild(pluginData.guild),
|
||||||
|
message: message ? savedMessageToTemplateSafeSavedMessage(message) : null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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";
|
||||||
|
|
17
backend/src/plugins/Automod/actions/pauseInvites.ts
Normal file
17
backend/src/plugins/Automod/actions/pauseInvites.ts
Normal 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
typeof actionConfig === "string"
|
try {
|
||||||
? await renderReplyText(actionConfig)
|
formatted =
|
||||||
: ((await renderRecursively(actionConfig.text, renderReplyText)) as MessageCreateOptions);
|
typeof actionConfig === "string"
|
||||||
|
? await renderReplyText(actionConfig)
|
||||||
|
: ((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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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 ?? [];
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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")]);
|
||||||
|
|
10
backend/src/plugins/Automod/functions/applyCooldown.ts
Normal file
10
backend/src/plugins/Automod/functions/applyCooldown.ts
Normal 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);
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
8
backend/src/plugins/Automod/functions/checkCooldown.ts
Normal file
8
backend/src/plugins/Automod/functions/checkCooldown.ts
Normal 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);
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
|
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.
|
||||||
`),
|
`),
|
||||||
configurationGuide: trimPluginDescription(`
|
configurationGuide: trimPluginDescription(`
|
||||||
The automod plugin is very customizable. For a full list of available triggers, actions, and their options, see Config schema at the bottom of this page.
|
The automod plugin is very customizable. For a full list of available triggers, actions, and their options, see Config schema at the bottom of this page.
|
||||||
|
|
||||||
|
@ -99,6 +99,5 @@ export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = {
|
||||||
Bad custom status on user <@!{user.id}>:
|
Bad custom status on user <@!{user.id}>:
|
||||||
{matchSummary}
|
{matchSummary}
|
||||||
~~~
|
~~~
|
||||||
`),
|
`),
|
||||||
configSchema: ConfigSchema,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
|
||||||
});
|
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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}`;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue