mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-07-07 11:07:19 +00:00
Merge branch 'master' into mod_actions-reason-aliases
This commit is contained in:
commit
f6b3df4dd1
681 changed files with 24542 additions and 35605 deletions
29
.clabot
Normal file
29
.clabot
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"contributors": [
|
||||||
|
"BanTheNons",
|
||||||
|
"CleverSource",
|
||||||
|
"DarkView",
|
||||||
|
"DenverCoder1",
|
||||||
|
"Jernik",
|
||||||
|
"Rstar284",
|
||||||
|
"almeidx",
|
||||||
|
"axisiscool",
|
||||||
|
"dexbiobot",
|
||||||
|
"greenbigfrog",
|
||||||
|
"k200-1",
|
||||||
|
"metal0",
|
||||||
|
"paolojpa",
|
||||||
|
"roflmaoqwerty",
|
||||||
|
"thewilloftheshadow",
|
||||||
|
"usoka",
|
||||||
|
"vcokltfre",
|
||||||
|
"Dragory",
|
||||||
|
"rubyowo",
|
||||||
|
"Dalkskkskk",
|
||||||
|
"iamshoXy",
|
||||||
|
"Scraayp",
|
||||||
|
"app/dependabot",
|
||||||
|
"zayKenyon"
|
||||||
|
],
|
||||||
|
"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!"
|
||||||
|
}
|
9
.devcontainer/devcontainer.json
Normal file
9
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "Zeppelin Development",
|
||||||
|
|
||||||
|
"dockerComposeFile": "../docker-compose.development.yml",
|
||||||
|
|
||||||
|
"service": "devenv",
|
||||||
|
"remoteUser": "ubuntu",
|
||||||
|
"workspaceFolder": "/home/ubuntu/zeppelin"
|
||||||
|
}
|
11
.dockerignore
Normal file
11
.dockerignore
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.idea
|
||||||
|
.devcontainer
|
||||||
|
|
||||||
|
/docker/development/data
|
||||||
|
/docker/production/data
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
/backend/dist
|
||||||
|
/dashboard/dist
|
75
.env.example
75
.env.example
|
@ -1 +1,74 @@
|
||||||
KEY=32_character_encryption_key
|
# 32 character encryption key
|
||||||
|
KEY=
|
||||||
|
|
||||||
|
# Values from the Discord developer portal
|
||||||
|
CLIENT_ID=
|
||||||
|
CLIENT_SECRET=
|
||||||
|
BOT_TOKEN=
|
||||||
|
|
||||||
|
# The defaults here automatically work for the development environment.
|
||||||
|
# For production, change localhost:3300 to your domain.
|
||||||
|
DASHBOARD_URL=https://localhost:3300
|
||||||
|
API_URL=https://localhost:3300/api
|
||||||
|
|
||||||
|
# Comma-separated list of user IDs who should have access to the bot's global commands
|
||||||
|
STAFF=
|
||||||
|
|
||||||
|
# A comma-separated list of server IDs that should be allowed by default
|
||||||
|
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
|
||||||
|
#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)
|
||||||
|
# NOTE: You only need to fill in these values for running the development environment. See production config further below.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCKER_DEV_WEB_PORT=3300
|
||||||
|
|
||||||
|
# The MySQL database running in the container is exposed to the host on this port,
|
||||||
|
# allowing access with database tools such as DBeaver
|
||||||
|
DOCKER_DEV_MYSQL_PORT=3001
|
||||||
|
# Password for the Zeppelin database user
|
||||||
|
DOCKER_DEV_MYSQL_PASSWORD=
|
||||||
|
# Password for the MySQL root user
|
||||||
|
DOCKER_DEV_MYSQL_ROOT_PASSWORD=
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
DOCKER_DEV_SSH_PORT=3002
|
||||||
|
DOCKER_DEV_SSH_PASSWORD=password
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# The MySQL database running in the container is exposed to the host on this port,
|
||||||
|
# allowing access with database tools such as DBeaver
|
||||||
|
DOCKER_PROD_MYSQL_PORT=3001
|
||||||
|
# Password for the Zeppelin database user
|
||||||
|
DOCKER_PROD_MYSQL_PASSWORD=
|
||||||
|
# Password for the MySQL root user
|
||||||
|
DOCKER_PROD_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=
|
||||||
|
#DB_USER=
|
||||||
|
#DB_PASSWORD=
|
||||||
|
#DB_DATABASE=
|
||||||
|
|
28
.eslintrc.js
Normal file
28
.eslintrc.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
|
"@typescript-eslint/ban-ts-comment": 0,
|
||||||
|
"@typescript-eslint/no-non-null-assertion": 0,
|
||||||
|
"no-async-promise-executor": 0,
|
||||||
|
"@typescript-eslint/no-empty-interface": 0,
|
||||||
|
"no-constant-condition": ["error", {
|
||||||
|
checkLoops: false,
|
||||||
|
}],
|
||||||
|
"prefer-const": ["error", {
|
||||||
|
destructuring: "all",
|
||||||
|
ignoreReadBeforeAssign: true,
|
||||||
|
}],
|
||||||
|
"@typescript-eslint/no-namespace": ["error", {
|
||||||
|
allowDeclarations: true,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
2
.github/workflows/codequality.yml
vendored
2
.github/workflows/codequality.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [16.6]
|
node-version: [18.16]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -6,6 +6,9 @@
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
.clinic
|
||||||
|
.clinic-bot
|
||||||
|
.clinic-api
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
pids
|
pids
|
||||||
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
||||||
16.6
|
18
|
||||||
|
|
71
DEVELOPMENT.md
Normal file
71
DEVELOPMENT.md
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# Zeppelin development environment
|
||||||
|
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
|
55
LICENSE.md
Normal file
55
LICENSE.md
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# Elastic License 2.0 (ELv2)
|
||||||
|
|
||||||
|
## Elastic License
|
||||||
|
|
||||||
|
### Acceptance
|
||||||
|
|
||||||
|
By using the software, you agree to all of the terms and conditions below.
|
||||||
|
|
||||||
|
### Copyright License
|
||||||
|
|
||||||
|
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below.
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software.
|
||||||
|
|
||||||
|
You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key.
|
||||||
|
|
||||||
|
You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||||
|
|
||||||
|
### Patents
|
||||||
|
|
||||||
|
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
||||||
|
|
||||||
|
### Notices
|
||||||
|
|
||||||
|
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms.
|
||||||
|
|
||||||
|
If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software.
|
||||||
|
|
||||||
|
### No Other Rights
|
||||||
|
|
||||||
|
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||||
|
|
||||||
|
### Termination
|
||||||
|
|
||||||
|
If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently.
|
||||||
|
|
||||||
|
### No Liability
|
||||||
|
|
||||||
|
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
||||||
|
|
||||||
|
### Definitions
|
||||||
|
|
||||||
|
The **licensor** is the entity offering these terms, and the **software** is the software the licensor makes available under these terms, including any portion of it.
|
||||||
|
|
||||||
|
**you** refers to the individual or entity agreeing to these terms.
|
||||||
|
|
||||||
|
**your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||||
|
|
||||||
|
**your licenses** are all the licenses granted to you for the software under these terms.
|
||||||
|
|
||||||
|
**use** means anything you do with the software requiring one of your licenses.
|
||||||
|
|
||||||
|
**trademark** means trademarks, service marks, and similar rights.
|
34
MANAGEMENT.md
Normal file
34
MANAGEMENT.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Management
|
||||||
|
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...>
|
||||||
|
```
|
34
PRODUCTION.md
Normal file
34
PRODUCTION.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Zeppelin production environment
|
||||||
|
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`
|
90
README.md
90
README.md
|
@ -19,89 +19,15 @@ Zeppelin is a moderation bot for Discord, designed with large servers and reliab
|
||||||
|
|
||||||
See https://zeppelin.gg/ for more details.
|
See https://zeppelin.gg/ for more details.
|
||||||
|
|
||||||
|
## Usage documentation
|
||||||
|
For information on how to use the bot, see https://zeppelin.gg/docs
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
These instructions are intended for bot development only.
|
See [DEVELOPMENT.md](./DEVELOPMENT.md) for instructions on running the development environment.
|
||||||
|
|
||||||
👉 **No support is offered for self-hosting the bot!** 👈
|
Once you have the environment up and running, see [MANAGEMENT.md](./MANAGEMENT.md) for how to manage your bot.
|
||||||
|
|
||||||
### Running the bot
|
## Production
|
||||||
1. `cd backend`
|
See [PRODUCTION.md](./PRODUCTION.md) for instructions on how to run the bot in production.
|
||||||
2. `npm ci`
|
|
||||||
3. Make a copy of `bot.env.example` called `bot.env`, fill in the values
|
|
||||||
4. Run the desired start script:
|
|
||||||
* `npm run build` followed by `npm run start-bot-dev` to run the bot in a **development** environment
|
|
||||||
* `npm run build` followed by `npm run start-bot-prod` to run the bot in a **production** environment
|
|
||||||
* `npm run watch` to watch files and run the **bot and api both** in a **development** environment
|
|
||||||
with automatic restart on file changes
|
|
||||||
5. When testing, make sure you have your test server in the `allowed_guilds` table or the guild's config won't be loaded at all
|
|
||||||
|
|
||||||
### Running the API server
|
Once you have the environment up and running, see [MANAGEMENT.md](./MANAGEMENT.md) for how to manage your bot.
|
||||||
1. `cd backend`
|
|
||||||
2. `npm ci`
|
|
||||||
3. Make a copy of `api.env.example` called `api.env`, fill in the values
|
|
||||||
4. Run the desired start script:
|
|
||||||
* `npm run build` followed by `npm run start-api-dev` to run the api in a **development** environment
|
|
||||||
* `npm run build` followed by `npm run start-api-prod` to run the api in a **production** environment
|
|
||||||
* `npm run watch` to watch files and run the **bot and api both** in a **development** environment
|
|
||||||
with automatic restart on file changes
|
|
||||||
|
|
||||||
### Running the dashboard
|
|
||||||
1. `cd dashboard`
|
|
||||||
2. `npm ci`
|
|
||||||
3. Make a copy of `.env.example` called `.env`, fill in the values
|
|
||||||
4. Run the desired start script:
|
|
||||||
* `npm run build` compiles the dashboard's static files to `dist/` which can then be served with any web server
|
|
||||||
* `npm run watch` runs webpack's dev server that automatically reloads on changes
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
* Since we now use shared paths in `tsconfig.json`, the compiled files in `backend/dist/` have longer paths, e.g.
|
|
||||||
`backend/dist/backend/src/index.js` instead of `backend/dist/index.js`. This is because the compiled shared files
|
|
||||||
are placed in `backend/dist/shared`.
|
|
||||||
* The `backend/register-tsconfig-paths.js` module takes care of registering shared paths from `tsconfig.json` for
|
|
||||||
`ava` and compiled `.js` files
|
|
||||||
* To run the tests for the files in the `shared/` directory, you also need to run `npm ci` there
|
|
||||||
|
|
||||||
### Config format example
|
|
||||||
Configuration is stored in the database in the `configs` table
|
|
||||||
|
|
||||||
```yml
|
|
||||||
prefix: '!'
|
|
||||||
|
|
||||||
# role id: level
|
|
||||||
levels:
|
|
||||||
"12345678": 100 # Example admin
|
|
||||||
"98765432": 50 # Example mod
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
mod_plugin:
|
|
||||||
config:
|
|
||||||
kick_message: 'You have been kicked'
|
|
||||||
can_kick: false
|
|
||||||
overrides:
|
|
||||||
- level: '>=50'
|
|
||||||
config:
|
|
||||||
can_kick: true
|
|
||||||
- level: '>=100'
|
|
||||||
config:
|
|
||||||
kick_message: 'You have been kicked by an admin'
|
|
||||||
|
|
||||||
other_plugin:
|
|
||||||
config:
|
|
||||||
categories:
|
|
||||||
mycategory:
|
|
||||||
opt: "something"
|
|
||||||
othercategory:
|
|
||||||
enabled: false
|
|
||||||
opt: "hello"
|
|
||||||
overrides:
|
|
||||||
- level: '>=50'
|
|
||||||
config:
|
|
||||||
categories:
|
|
||||||
mycategory:
|
|
||||||
enabled: false
|
|
||||||
- channel: '1234'
|
|
||||||
config:
|
|
||||||
categories:
|
|
||||||
othercategory:
|
|
||||||
enabled: true
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
PORT=
|
|
||||||
CLIENT_ID=
|
|
||||||
CLIENT_SECRET=
|
|
||||||
OAUTH_CALLBACK_URL=
|
|
||||||
DASHBOARD_URL=
|
|
||||||
DB_HOST=
|
|
||||||
DB_USER=
|
|
||||||
DB_PASSWORD=
|
|
||||||
DB_DATABASE=
|
|
||||||
STAFF=
|
|
|
@ -1,7 +0,0 @@
|
||||||
TOKEN=
|
|
||||||
DB_HOST=
|
|
||||||
DB_USER=
|
|
||||||
DB_PASSWORD=
|
|
||||||
DB_DATABASE=
|
|
||||||
PROFILING=false
|
|
||||||
PHISHERMAN_API_KEY=
|
|
|
@ -1,65 +0,0 @@
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const pkgUp = require("pkg-up");
|
|
||||||
|
|
||||||
const closestPackageJson = pkgUp.sync();
|
|
||||||
if (!closestPackageJson) {
|
|
||||||
throw new Error("Could not find project root from ormconfig.js");
|
|
||||||
}
|
|
||||||
const backendRoot = path.dirname(closestPackageJson);
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.accessSync(path.resolve(backendRoot, "bot.env"));
|
|
||||||
require("dotenv").config({ path: path.resolve(backendRoot, "bot.env") });
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
fs.accessSync(path.resolve(backendRoot, "api.env"));
|
|
||||||
require("dotenv").config({ path: path.resolve(backendRoot, "api.env") });
|
|
||||||
} catch {
|
|
||||||
throw new Error("bot.env or api.env required");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const moment = require("moment-timezone");
|
|
||||||
moment.tz.setDefault("UTC");
|
|
||||||
|
|
||||||
const entities = path.relative(process.cwd(), path.resolve(backendRoot, "dist/backend/src/data/entities/*.js"));
|
|
||||||
const migrations = path.relative(process.cwd(), path.resolve(backendRoot, "dist/backend/src/migrations/*.js"));
|
|
||||||
const migrationsDir = path.relative(process.cwd(), path.resolve(backendRoot, "src/migrations"));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
type: "mysql",
|
|
||||||
host: process.env.DB_HOST,
|
|
||||||
username: process.env.DB_USER,
|
|
||||||
password: process.env.DB_PASSWORD,
|
|
||||||
database: process.env.DB_DATABASE,
|
|
||||||
charset: "utf8mb4",
|
|
||||||
supportBigNumbers: true,
|
|
||||||
bigNumberStrings: true,
|
|
||||||
dateStrings: true,
|
|
||||||
synchronize: false,
|
|
||||||
connectTimeout: 2000,
|
|
||||||
|
|
||||||
logging: ["error", "warn"],
|
|
||||||
|
|
||||||
// Entities
|
|
||||||
entities: [entities],
|
|
||||||
|
|
||||||
// Pool options
|
|
||||||
extra: {
|
|
||||||
typeCast(field, next) {
|
|
||||||
if (field.type === "DATETIME") {
|
|
||||||
const val = field.string();
|
|
||||||
return val != null ? moment.utc(val).format("YYYY-MM-DD HH:mm:ss") : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Migrations
|
|
||||||
migrations: [migrations],
|
|
||||||
cli: {
|
|
||||||
migrationsDir,
|
|
||||||
},
|
|
||||||
};
|
|
14797
backend/package-lock.json
generated
14797
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -8,16 +8,22 @@
|
||||||
"watch-yaml-parse-test": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node dist/backend/src/yamlParseTest.js\"",
|
"watch-yaml-parse-test": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node dist/backend/src/yamlParseTest.js\"",
|
||||||
"build": "rimraf dist && tsc",
|
"build": "rimraf dist && tsc",
|
||||||
"start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js",
|
"start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js",
|
||||||
|
"start-bot-dev-debug": "NODE_ENV=development DEBUG=true clinic heapprofiler --collect-only --dest .clinic-bot -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js",
|
||||||
"start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js",
|
"start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js",
|
||||||
|
"start-bot-prod-debug": "NODE_ENV=production DEBUG=true clinic heapprofiler --collect-only --dest .clinic-bot -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js",
|
||||||
"watch-bot": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-bot-dev\"",
|
"watch-bot": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-bot-dev\"",
|
||||||
"start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
|
"start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
|
||||||
|
"start-api-dev-debug": "NODE_ENV=development DEBUG=true clinic heapprofiler --collect-only --dest .clinic-api -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
|
||||||
"start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js",
|
"start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js",
|
||||||
|
"start-api-prod-debug": "NODE_ENV=production DEBUG=true clinic heapprofiler --collect-only --dest .clinic-api -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js",
|
||||||
"watch-api": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-api-dev\"",
|
"watch-api": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-api-dev\"",
|
||||||
"typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js",
|
"typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js",
|
||||||
"migrate-prod": "npm run typeorm -- migration:run",
|
"migrate": "npm run typeorm -- migration:run -d dist/backend/src/data/dataSource.js",
|
||||||
"migrate-dev": "npm run build && npm run typeorm -- migration:run",
|
"migrate-prod": "cross-env NODE_ENV=production npm run migrate",
|
||||||
"migrate-rollback-prod": "npm run typeorm -- migration:revert",
|
"migrate-dev": "cross-env NODE_ENV=development npm run build && npm run migrate",
|
||||||
"migrate-rollback-dev": "npm run build && npm run typeorm -- migration:revert",
|
"migrate-rollback": "npm run typeorm -- migration:revert -d dist/backend/src/data/dataSource.js",
|
||||||
|
"migrate-rollback-prod": "cross-env NODE_ENV=production npm run migrate",
|
||||||
|
"migrate-rollback-dev": "cross-env NODE_ENV=development npm run build && npm run migrate",
|
||||||
"test": "npm run build && npm run run-tests",
|
"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 --onSuccess \"npx ava\""
|
||||||
|
@ -25,21 +31,21 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@silvia-odwyer/photon-node": "^0.3.1",
|
"@silvia-odwyer/photon-node": "^0.3.1",
|
||||||
"bufferutil": "^4.0.3",
|
"bufferutil": "^4.0.3",
|
||||||
|
"clinic": "^13.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^7.0.3",
|
||||||
"deep-diff": "^1.0.2",
|
"deep-diff": "^1.0.2",
|
||||||
"discord-api-types": "^0.22.0",
|
"discord.js": "^14.11.0",
|
||||||
"discord.js": "^13.3.1",
|
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
"erlpack": "github:almeidx/erlpack#f0c535f73817fd914806d6ca26a7730c14e0fb7c",
|
"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",
|
"io-ts": "^2.0.0",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"knub": "^30.0.0-beta.46",
|
"knub": "^32.0.0-next.16",
|
||||||
"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",
|
||||||
|
@ -49,13 +55,12 @@
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.pick": "^4.4.0",
|
"lodash.pick": "^4.4.0",
|
||||||
"moment-timezone": "^0.5.21",
|
"moment-timezone": "^0.5.21",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql": "^2.16.0",
|
"mysql": "^2.16.0",
|
||||||
"node-fetch": "^2.6.5",
|
|
||||||
"parse-color": "^1.0.0",
|
"parse-color": "^1.0.0",
|
||||||
"passport": "^0.4.0",
|
"passport": "^0.6.0",
|
||||||
"passport-custom": "^1.0.5",
|
"passport-custom": "^1.0.5",
|
||||||
"passport-oauth2": "^1.5.0",
|
"passport-oauth2": "^1.6.1",
|
||||||
"pkg-up": "^3.1.0",
|
"pkg-up": "^3.1.0",
|
||||||
"reflect-metadata": "^0.1.12",
|
"reflect-metadata": "^0.1.12",
|
||||||
"regexp-worker": "^1.1.0",
|
"regexp-worker": "^1.1.0",
|
||||||
|
@ -67,9 +72,9 @@
|
||||||
"tmp": "0.0.33",
|
"tmp": "0.0.33",
|
||||||
"tsconfig-paths": "^3.9.0",
|
"tsconfig-paths": "^3.9.0",
|
||||||
"twemoji": "^12.1.4",
|
"twemoji": "^12.1.4",
|
||||||
"typeorm": "^0.2.31",
|
"typeorm": "^0.3.17",
|
||||||
"utf-8-validate": "^5.0.5",
|
"utf-8-validate": "^5.0.5",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^9.0.0",
|
||||||
"yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build",
|
"yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build",
|
||||||
"zlib-sync": "^0.1.7",
|
"zlib-sync": "^0.1.7",
|
||||||
"zod": "^3.7.2"
|
"zod": "^3.7.2"
|
||||||
|
@ -82,18 +87,17 @@
|
||||||
"@types/lodash.at": "^4.6.3",
|
"@types/lodash.at": "^4.6.3",
|
||||||
"@types/moment-timezone": "^0.5.6",
|
"@types/moment-timezone": "^0.5.6",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^14.0.14",
|
"@types/node": "^18.16.3",
|
||||||
"@types/node-fetch": "^2.5.12",
|
|
||||||
"@types/passport": "^1.0.0",
|
"@types/passport": "^1.0.0",
|
||||||
"@types/passport-oauth2": "^1.4.8",
|
"@types/passport-oauth2": "^1.4.8",
|
||||||
"@types/passport-strategy": "^0.2.35",
|
"@types/passport-strategy": "^0.2.35",
|
||||||
"@types/safe-regex": "^1.1.2",
|
"@types/safe-regex": "^1.1.2",
|
||||||
"@types/tmp": "0.0.33",
|
"@types/tmp": "0.0.33",
|
||||||
"@types/twemoji": "^12.1.0",
|
"@types/twemoji": "^12.1.0",
|
||||||
"ava": "^3.10.0",
|
"@types/uuid": "^9.0.2",
|
||||||
|
"ava": "^5.3.1",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"source-map-support": "^0.5.16",
|
"source-map-support": "^0.5.16"
|
||||||
"tsc-watch": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"ava": {
|
"ava": {
|
||||||
"files": [
|
"files": [
|
||||||
|
|
40
backend/src/Blocker.ts
Normal file
40
backend/src/Blocker.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
export type Block = {
|
||||||
|
count: number;
|
||||||
|
unblock: () => void;
|
||||||
|
getPromise: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Blocker {
|
||||||
|
#blocks: Map<string, Block> = new Map();
|
||||||
|
|
||||||
|
block(key: string): void {
|
||||||
|
if (!this.#blocks.has(key)) {
|
||||||
|
const promise = new Promise<void>((resolve) => {
|
||||||
|
this.#blocks.set(key, {
|
||||||
|
count: 0, // Incremented to 1 further below
|
||||||
|
unblock() {
|
||||||
|
this.count--;
|
||||||
|
if (this.count === 0) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPromise: () => promise, // :d
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.#blocks.get(key)!.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
unblock(key: string): void {
|
||||||
|
if (this.#blocks.has(key)) {
|
||||||
|
this.#blocks.get(key)!.unblock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitToBeUnblocked(key: string): Promise<void> {
|
||||||
|
if (!this.#blocks.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.#blocks.get(key)!.getPromise();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ export class SimpleError extends Error {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[util.inspect.custom](depth, options) {
|
[util.inspect.custom]() {
|
||||||
return `Error: ${this.message}`;
|
return `Error: ${this.message}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { ApiLogins } from "../data/ApiLogins";
|
||||||
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
||||||
import { ApiUserInfo } from "../data/ApiUserInfo";
|
import { ApiUserInfo } from "../data/ApiUserInfo";
|
||||||
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
||||||
|
import { env } from "../env";
|
||||||
import { ok } from "./responses";
|
import { ok } from "./responses";
|
||||||
|
|
||||||
interface IPassportApiUser {
|
interface IPassportApiUser {
|
||||||
|
@ -17,7 +18,6 @@ interface IPassportApiUser {
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
// tslint:disable-next-line:no-empty-interface
|
|
||||||
interface User extends IPassportApiUser {}
|
interface User extends IPassportApiUser {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,24 +54,8 @@ function simpleDiscordAPIRequest(bearerToken, path): Promise<any> {
|
||||||
export function initAuth(app: express.Express) {
|
export function initAuth(app: express.Express) {
|
||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
|
|
||||||
if (!process.env.CLIENT_ID) {
|
|
||||||
throw new Error("Auth: CLIENT ID missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.CLIENT_SECRET) {
|
|
||||||
throw new Error("Auth: CLIENT SECRET missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.OAUTH_CALLBACK_URL) {
|
|
||||||
throw new Error("Auth: OAUTH CALLBACK URL missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.DASHBOARD_URL) {
|
|
||||||
throw new Error("DASHBOARD_URL missing!");
|
|
||||||
}
|
|
||||||
|
|
||||||
passport.serializeUser((user, done) => done(null, user));
|
passport.serializeUser((user, done) => done(null, user));
|
||||||
passport.deserializeUser((user, done) => done(null, user));
|
passport.deserializeUser((user, done) => done(null, user as IPassportApiUser));
|
||||||
|
|
||||||
const apiLogins = new ApiLogins();
|
const apiLogins = new ApiLogins();
|
||||||
const apiUserInfo = new ApiUserInfo();
|
const apiUserInfo = new ApiUserInfo();
|
||||||
|
@ -101,9 +85,9 @@ export function initAuth(app: express.Express) {
|
||||||
{
|
{
|
||||||
authorizationURL: "https://discord.com/api/oauth2/authorize",
|
authorizationURL: "https://discord.com/api/oauth2/authorize",
|
||||||
tokenURL: "https://discord.com/api/oauth2/token",
|
tokenURL: "https://discord.com/api/oauth2/token",
|
||||||
clientID: process.env.CLIENT_ID,
|
clientID: env.CLIENT_ID,
|
||||||
clientSecret: process.env.CLIENT_SECRET,
|
clientSecret: env.CLIENT_SECRET,
|
||||||
callbackURL: process.env.OAUTH_CALLBACK_URL,
|
callbackURL: `${env.API_URL}/auth/oauth-callback`,
|
||||||
scope: ["identify"],
|
scope: ["identify"],
|
||||||
},
|
},
|
||||||
async (accessToken, refreshToken, profile, cb) => {
|
async (accessToken, refreshToken, profile, cb) => {
|
||||||
|
@ -132,9 +116,9 @@ export function initAuth(app: express.Express) {
|
||||||
passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
|
passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
|
||||||
(req: Request, res: Response) => {
|
(req: Request, res: Response) => {
|
||||||
if (req.user && req.user.apiKey) {
|
if (req.user && req.user.apiKey) {
|
||||||
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
|
res.redirect(`${env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
|
||||||
} else {
|
} else {
|
||||||
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?error=noAccess`);
|
res.redirect(`${env.DASHBOARD_URL}/login-callback/?error=noAccess`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -165,7 +149,8 @@ export function initAuth(app: express.Express) {
|
||||||
|
|
||||||
export function apiTokenAuthHandlers() {
|
export function apiTokenAuthHandlers() {
|
||||||
return [
|
return [
|
||||||
passport.authenticate("api-token", { failWithError: true }),
|
passport.authenticate("api-token", { failWithError: true, session: false }),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
(err, req: Request, res: Response, next) => {
|
(err, req: Request, res: Response, next) => {
|
||||||
return res.status(401).json({ error: err.message });
|
return res.status(401).json({ error: err.message });
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,9 +54,10 @@ export function initDocs(app: express.Express) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = plugin.name;
|
const name = plugin.name;
|
||||||
const info = plugin.info || {};
|
const info = { ...(plugin.info || {}) };
|
||||||
|
delete info.configSchema;
|
||||||
|
|
||||||
const commands = (plugin.commands || []).map((cmd) => ({
|
const messageCommands = (plugin.messageCommands || []).map((cmd) => ({
|
||||||
trigger: cmd.trigger,
|
trigger: cmd.trigger,
|
||||||
permission: cmd.permission,
|
permission: cmd.permission,
|
||||||
signature: cmd.signature,
|
signature: cmd.signature,
|
||||||
|
@ -66,14 +67,14 @@ export function initDocs(app: express.Express) {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const defaultOptions = plugin.defaultOptions || {};
|
const defaultOptions = plugin.defaultOptions || {};
|
||||||
const configSchema = plugin.configSchema && formatConfigSchema(plugin.configSchema);
|
const configSchema = plugin.info?.configSchema && formatConfigSchema(plugin.info.configSchema);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
name,
|
name,
|
||||||
info,
|
info,
|
||||||
configSchema,
|
configSchema,
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
commands,
|
messageCommands,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import { ApiPermissions } from "@shared/apiPermissions";
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import { YAMLException } from "js-yaml";
|
import { YAMLException } from "js-yaml";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { Queue } from "../Queue";
|
||||||
import { validateGuildConfig } from "../configValidator";
|
import { validateGuildConfig } from "../configValidator";
|
||||||
import { AllowedGuilds } from "../data/AllowedGuilds";
|
import { AllowedGuilds } from "../data/AllowedGuilds";
|
||||||
|
import { ApiAuditLog } from "../data/ApiAuditLog";
|
||||||
import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments";
|
import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments";
|
||||||
import { Configs } from "../data/Configs";
|
import { Configs } from "../data/Configs";
|
||||||
|
import { AuditLogEventTypes } from "../data/apiAuditLogTypes";
|
||||||
|
import { isSnowflake } from "../utils";
|
||||||
|
import { loadYamlSafely } from "../utils/loadYamlSafely";
|
||||||
|
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
|
||||||
import { apiTokenAuthHandlers } from "./auth";
|
import { apiTokenAuthHandlers } from "./auth";
|
||||||
import { hasGuildPermission, requireGuildPermission } from "./permissions";
|
import { hasGuildPermission, requireGuildPermission } from "./permissions";
|
||||||
import { clientError, ok, serverError, unauthorized } from "./responses";
|
import { clientError, ok, serverError, unauthorized } from "./responses";
|
||||||
import { loadYamlSafely } from "../utils/loadYamlSafely";
|
|
||||||
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
|
|
||||||
import { isSnowflake } from "../utils";
|
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { ApiAuditLog } from "../data/ApiAuditLog";
|
|
||||||
import { AuditLogEventTypes } from "../data/apiAuditLogTypes";
|
|
||||||
import { Queue } from "../Queue";
|
|
||||||
|
|
||||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||||
const auditLog = new ApiAuditLog();
|
const auditLog = new ApiAuditLog();
|
||||||
|
@ -126,7 +126,7 @@ export function initGuildsAPI(app: express.Express) {
|
||||||
if (type !== ApiPermissionTypes.User) {
|
if (type !== ApiPermissionTypes.User) {
|
||||||
return clientError(res, "Invalid type");
|
return clientError(res, "Invalid type");
|
||||||
}
|
}
|
||||||
if (!isSnowflake(targetId)) {
|
if (!isSnowflake(targetId) || targetId === req.user!.userId) {
|
||||||
return clientError(res, "Invalid targetId");
|
return clientError(res, "Invalid targetId");
|
||||||
}
|
}
|
||||||
const validPermissions = new Set(Object.values(ApiPermissions));
|
const validPermissions = new Set(Object.values(ApiPermissions));
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { ApiPermissions } from "@shared/apiPermissions";
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import { requireGuildPermission } from "../permissions";
|
|
||||||
import { clientError, ok } from "../responses";
|
|
||||||
import { GuildCases } from "../../data/GuildCases";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { Case } from "../../data/entities/Case";
|
|
||||||
import { rateLimit } from "../rateLimits";
|
|
||||||
import { MINUTES } from "../../utils";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { GuildCases } from "../../data/GuildCases";
|
||||||
|
import { Case } from "../../data/entities/Case";
|
||||||
|
import { MINUTES } from "../../utils";
|
||||||
|
import { requireGuildPermission } from "../permissions";
|
||||||
|
import { rateLimit } from "../rateLimits";
|
||||||
|
import { clientError, ok } from "../responses";
|
||||||
|
|
||||||
const caseHandlingModeSchema = z.union([
|
const caseHandlingModeSchema = z.union([
|
||||||
z.literal("replace"),
|
z.literal("replace"),
|
||||||
|
@ -50,7 +50,7 @@ export function initGuildsImportExportAPI(guildRouter: express.Router) {
|
||||||
importExportRouter.get(
|
importExportRouter.get(
|
||||||
"/:guildId/pre-import",
|
"/:guildId/pre-import",
|
||||||
requireGuildPermission(ApiPermissions.ManageAccess),
|
requireGuildPermission(ApiPermissions.ManageAccess),
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request) => {
|
||||||
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
||||||
const minNum = await guildCases.getMinCaseNumber();
|
const minNum = await guildCases.getMinCaseNumber();
|
||||||
const maxNum = await guildCases.getMaxCaseNumber();
|
const maxNum = await guildCases.getMaxCaseNumber();
|
||||||
|
@ -75,7 +75,10 @@ export function initGuildsImportExportAPI(guildRouter: express.Router) {
|
||||||
try {
|
try {
|
||||||
data = importExportData.parse(req.body.data);
|
data = importExportData.parse(req.body.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return clientError(res, "Invalid import data format");
|
const prettyMessage = `${err.issues[0].code}: expected ${err.issues[0].expected}, received ${
|
||||||
|
err.issues[0].received
|
||||||
|
} at /${err.issues[0].path.join("/")}`;
|
||||||
|
return clientError(res, `Invalid import data format: ${prettyMessage}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +90,14 @@ export function initGuildsImportExportAPI(guildRouter: express.Router) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seenCaseNumbers = new Set();
|
||||||
|
for (const theCase of data.cases) {
|
||||||
|
if (seenCaseNumbers.has(theCase.case_number)) {
|
||||||
|
return clientError(res, `Duplicate case number: ${theCase.case_number}`);
|
||||||
|
}
|
||||||
|
seenCaseNumbers.add(theCase.case_number);
|
||||||
|
}
|
||||||
|
|
||||||
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
||||||
|
|
||||||
// Prepare cases
|
// Prepare cases
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { apiTokenAuthHandlers } from "../auth";
|
import { apiTokenAuthHandlers } from "../auth";
|
||||||
import { initGuildsMiscAPI } from "./misc";
|
|
||||||
import { initGuildsImportExportAPI } from "./importExport";
|
import { initGuildsImportExportAPI } from "./importExport";
|
||||||
|
import { initGuildsMiscAPI } from "./misc";
|
||||||
|
|
||||||
export function initGuildsAPI(app: express.Express) {
|
export function initGuildsAPI(app: express.Express) {
|
||||||
const guildRouter = express.Router();
|
const guildRouter = express.Router();
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
import { ApiPermissions } from "@shared/apiPermissions";
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import { YAMLException } from "js-yaml";
|
import { YAMLException } from "js-yaml";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { Queue } from "../../Queue";
|
||||||
import { validateGuildConfig } from "../../configValidator";
|
import { validateGuildConfig } from "../../configValidator";
|
||||||
import { AllowedGuilds } from "../../data/AllowedGuilds";
|
import { AllowedGuilds } from "../../data/AllowedGuilds";
|
||||||
|
import { ApiAuditLog } from "../../data/ApiAuditLog";
|
||||||
import { ApiPermissionAssignments, ApiPermissionTypes } from "../../data/ApiPermissionAssignments";
|
import { ApiPermissionAssignments, ApiPermissionTypes } from "../../data/ApiPermissionAssignments";
|
||||||
import { Configs } from "../../data/Configs";
|
import { Configs } from "../../data/Configs";
|
||||||
import { apiTokenAuthHandlers } from "../auth";
|
import { AuditLogEventTypes } from "../../data/apiAuditLogTypes";
|
||||||
import { hasGuildPermission, requireGuildPermission } from "../permissions";
|
import { isSnowflake } from "../../utils";
|
||||||
import { clientError, ok, serverError, unauthorized } from "../responses";
|
|
||||||
import { loadYamlSafely } from "../../utils/loadYamlSafely";
|
import { loadYamlSafely } from "../../utils/loadYamlSafely";
|
||||||
import { ObjectAliasError } from "../../utils/validateNoObjectAliases";
|
import { ObjectAliasError } from "../../utils/validateNoObjectAliases";
|
||||||
import { isSnowflake } from "../../utils";
|
import { hasGuildPermission, requireGuildPermission } from "../permissions";
|
||||||
import moment from "moment-timezone";
|
import { clientError, ok, serverError, unauthorized } from "../responses";
|
||||||
import { ApiAuditLog } from "../../data/ApiAuditLog";
|
|
||||||
import { AuditLogEventTypes } from "../../data/apiAuditLogTypes";
|
|
||||||
import { Queue } from "../../Queue";
|
|
||||||
import { GuildCases } from "../../data/GuildCases";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||||
const auditLog = new ApiAuditLog();
|
const auditLog = new ApiAuditLog();
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import { connect } from "../data/db";
|
// KEEP THIS AS FIRST IMPORT
|
||||||
import { setIsAPI } from "../globals";
|
// See comment in module for details
|
||||||
import "./loadEnv";
|
import "../threadsSignalFix";
|
||||||
|
|
||||||
if (!process.env.KEY) {
|
import { connect } from "../data/db";
|
||||||
|
import { env } from "../env";
|
||||||
|
import { setIsAPI } from "../globals";
|
||||||
|
|
||||||
|
if (!env.KEY) {
|
||||||
// tslint:disable-next-line:no-console
|
// tslint:disable-next-line:no-console
|
||||||
console.error("Project root .env with KEY is required!");
|
console.error("Project root .env with KEY is required!");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@ -20,5 +24,5 @@ setIsAPI(true);
|
||||||
// Connect to the database before loading the rest of the code (that depend on the database connection)
|
// Connect to the database before loading the rest of the code (that depend on the database connection)
|
||||||
console.log("Connecting to database..."); // tslint:disable-line
|
console.log("Connecting to database..."); // tslint:disable-line
|
||||||
connect().then(() => {
|
connect().then(() => {
|
||||||
import("./start");
|
import("./start.js");
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
require("dotenv").config({ path: path.resolve(process.cwd(), "../.env") });
|
|
||||||
require("dotenv").config({ path: path.resolve(process.cwd(), "api.env") });
|
|
|
@ -4,7 +4,7 @@ export function unauthorized(res: Response) {
|
||||||
res.status(403).json({ error: "Unauthorized" });
|
res.status(403).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function error(res: Response, message: string, statusCode: number = 500) {
|
export function error(res: Response, message: string, statusCode = 500) {
|
||||||
res.status(statusCode).json({ error: message });
|
res.status(statusCode).json({ error: message });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import multer from "multer";
|
||||||
import { TokenError } from "passport-oauth2";
|
import { TokenError } from "passport-oauth2";
|
||||||
|
import { env } from "../env";
|
||||||
import { initArchives } from "./archives";
|
import { initArchives } from "./archives";
|
||||||
import { initAuth } from "./auth";
|
import { initAuth } from "./auth";
|
||||||
import { initDocs } from "./docs";
|
import { initDocs } from "./docs";
|
||||||
import { initGuildsAPI } from "./guilds/index";
|
import { initGuildsAPI } from "./guilds/index";
|
||||||
import { clientError, error, notFound } from "./responses";
|
import { clientError, error, notFound } from "./responses";
|
||||||
import { startBackgroundTasks } from "./tasks";
|
import { startBackgroundTasks } from "./tasks";
|
||||||
import multer from "multer";
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: process.env.DASHBOARD_URL,
|
origin: env.DASHBOARD_URL,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
|
@ -34,6 +35,7 @@ app.get("/", (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error response
|
// Error response
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
if (err instanceof TokenError) {
|
if (err instanceof TokenError) {
|
||||||
clientError(res, "Invalid code");
|
clientError(res, "Invalid code");
|
||||||
|
@ -44,11 +46,12 @@ app.use((err, req, res, next) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 404 response
|
// 404 response
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
return notFound(res);
|
return notFound(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
const port = (process.env.PORT && parseInt(process.env.PORT, 10)) || 3000;
|
const port = env.API_PORT;
|
||||||
app.listen(port, "0.0.0.0", () => console.log(`API server listening on port ${port}`)); // tslint:disable-line
|
app.listen(port, "0.0.0.0", () => console.log(`API server listening on port ${port}`)); // tslint:disable-line
|
||||||
|
|
||||||
startBackgroundTasks();
|
startBackgroundTasks();
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
import { GuildChannel, GuildMember, Snowflake, Util, User } from "discord.js";
|
import {
|
||||||
import { baseCommandParameterTypeHelpers, baseTypeConverters, CommandContext, TypeConversionError } from "knub";
|
escapeCodeBlock,
|
||||||
|
escapeInlineCode,
|
||||||
|
GuildChannel,
|
||||||
|
GuildMember,
|
||||||
|
GuildTextBasedChannel,
|
||||||
|
Snowflake,
|
||||||
|
User,
|
||||||
|
} from "discord.js";
|
||||||
|
import {
|
||||||
|
baseCommandParameterTypeHelpers,
|
||||||
|
CommandContext,
|
||||||
|
messageCommandBaseTypeConverters,
|
||||||
|
TypeConversionError,
|
||||||
|
} from "knub";
|
||||||
import { createTypeHelper } from "knub-command-manager";
|
import { createTypeHelper } from "knub-command-manager";
|
||||||
import {
|
import {
|
||||||
channelMentionRegex,
|
channelMentionRegex,
|
||||||
|
@ -16,7 +29,7 @@ import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarge
|
||||||
import { inputPatternToRegExp } from "./validatorUtils";
|
import { inputPatternToRegExp } from "./validatorUtils";
|
||||||
|
|
||||||
export const commandTypes = {
|
export const commandTypes = {
|
||||||
...baseTypeConverters,
|
...messageCommandBaseTypeConverters,
|
||||||
|
|
||||||
delay(value) {
|
delay(value) {
|
||||||
const result = convertDelayStringToMS(value);
|
const result = convertDelayStringToMS(value);
|
||||||
|
@ -30,7 +43,7 @@ export const commandTypes = {
|
||||||
async resolvedUser(value, context: CommandContext<any>) {
|
async resolvedUser(value, context: CommandContext<any>) {
|
||||||
const result = await resolveUser(context.pluginData.client, value);
|
const result = await resolveUser(context.pluginData.client, value);
|
||||||
if (result == null || result instanceof UnknownUser) {
|
if (result == null || result instanceof UnknownUser) {
|
||||||
throw new TypeConversionError(`User \`${Util.escapeCodeBlock(value)}\` was not found`);
|
throw new TypeConversionError(`User \`${escapeCodeBlock(value)}\` was not found`);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -38,7 +51,7 @@ export const commandTypes = {
|
||||||
async resolvedUserLoose(value, context: CommandContext<any>) {
|
async resolvedUserLoose(value, context: CommandContext<any>) {
|
||||||
const result = await resolveUser(context.pluginData.client, value);
|
const result = await resolveUser(context.pluginData.client, value);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
throw new TypeConversionError(`Invalid user: \`${Util.escapeCodeBlock(value)}\``);
|
throw new TypeConversionError(`Invalid user: \`${escapeCodeBlock(value)}\``);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -50,9 +63,7 @@ export const commandTypes = {
|
||||||
|
|
||||||
const result = await resolveMember(context.pluginData.client, context.message.channel.guild, value);
|
const result = await resolveMember(context.pluginData.client, context.message.channel.guild, value);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
throw new TypeConversionError(
|
throw new TypeConversionError(`Member \`${escapeCodeBlock(value)}\` was not found or they have left the server`);
|
||||||
`Member \`${Util.escapeCodeBlock(value)}\` was not found or they have left the server`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -62,7 +73,7 @@ export const commandTypes = {
|
||||||
|
|
||||||
const result = await resolveMessageTarget(context.pluginData, value);
|
const result = await resolveMessageTarget(context.pluginData, value);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new TypeConversionError(`Unknown message \`${Util.escapeInlineCode(value)}\``);
|
throw new TypeConversionError(`Unknown message \`${escapeInlineCode(value)}\``);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -82,24 +93,28 @@ export const commandTypes = {
|
||||||
return value as Snowflake;
|
return value as Snowflake;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new TypeConversionError(`Could not parse ID: \`${Util.escapeInlineCode(value)}\``);
|
throw new TypeConversionError(`Could not parse ID: \`${escapeInlineCode(value)}\``);
|
||||||
},
|
},
|
||||||
|
|
||||||
regex(value: string, context: CommandContext<any>): RegExp {
|
regex(value: string): RegExp {
|
||||||
try {
|
try {
|
||||||
return inputPatternToRegExp(value);
|
return inputPatternToRegExp(value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new TypeConversionError(`Could not parse RegExp: \`${Util.escapeInlineCode(e.message)}\``);
|
throw new TypeConversionError(`Could not parse RegExp: \`${escapeInlineCode(e.message)}\``);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
timezone(value: string) {
|
timezone(value: string) {
|
||||||
if (!isValidTimezone(value)) {
|
if (!isValidTimezone(value)) {
|
||||||
throw new TypeConversionError(`Invalid timezone: ${Util.escapeInlineCode(value)}`);
|
throw new TypeConversionError(`Invalid timezone: ${escapeInlineCode(value)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
guildTextBasedChannel(value: string, context: CommandContext<any>) {
|
||||||
|
return messageCommandBaseTypeConverters.textChannel(value, context);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const commandTypeHelpers = {
|
export const commandTypeHelpers = {
|
||||||
|
@ -113,4 +128,5 @@ export const commandTypeHelpers = {
|
||||||
anyId: createTypeHelper<Promise<Snowflake>>(commandTypes.anyId),
|
anyId: createTypeHelper<Promise<Snowflake>>(commandTypes.anyId),
|
||||||
regex: createTypeHelper<RegExp>(commandTypes.regex),
|
regex: createTypeHelper<RegExp>(commandTypes.regex),
|
||||||
timezone: createTypeHelper<string>(commandTypes.timezone),
|
timezone: createTypeHelper<string>(commandTypes.timezone),
|
||||||
|
guildTextBasedChannel: createTypeHelper<GuildTextBasedChannel>(commandTypes.guildTextBasedChannel),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { configUtils, ConfigValidationError, PluginOptions } from "knub";
|
import { ConfigValidationError, PluginConfigManager } from "knub";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { guildPlugins } from "./plugins/availablePlugins";
|
|
||||||
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
||||||
|
import { guildPlugins } from "./plugins/availablePlugins";
|
||||||
import { PartialZeppelinGuildConfigSchema, ZeppelinGuildConfig } from "./types";
|
import { PartialZeppelinGuildConfigSchema, ZeppelinGuildConfig } from "./types";
|
||||||
import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils";
|
import { StrictValidationError, decodeAndValidateStrict } from "./validatorUtils";
|
||||||
|
|
||||||
const pluginNameToPlugin = new Map<string, ZeppelinPlugin>();
|
const pluginNameToPlugin = new Map<string, ZeppelinPlugin>();
|
||||||
for (const plugin of guildPlugins) {
|
for (const plugin of guildPlugins) {
|
||||||
|
@ -34,9 +34,12 @@ export async function validateGuildConfig(config: any): Promise<string | null> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = pluginNameToPlugin.get(pluginName)!;
|
const plugin = pluginNameToPlugin.get(pluginName)!;
|
||||||
|
const configManager = new PluginConfigManager(plugin.defaultOptions || { config: {} }, pluginOptions, {
|
||||||
|
levels: {},
|
||||||
|
parser: plugin.configParser,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const mergedOptions = configUtils.mergeConfig(plugin.defaultOptions || {}, pluginOptions);
|
await configManager.init();
|
||||||
await plugin.configPreprocessor?.(mergedOptions as unknown as PluginOptions<any>, true);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ConfigValidationError || err instanceof StrictValidationError) {
|
if (err instanceof ConfigValidationError || err instanceof StrictValidationError) {
|
||||||
return `${pluginName}: ${err.message}`;
|
return `${pluginName}: ${err.message}`;
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import moment from "moment-timezone";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
import { DBDateFormat } from "../utils";
|
||||||
import { ApiPermissionTypes } from "./ApiPermissionAssignments";
|
import { ApiPermissionTypes } from "./ApiPermissionAssignments";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { AllowedGuild } from "./entities/AllowedGuild";
|
import { AllowedGuild } from "./entities/AllowedGuild";
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { DBDateFormat } from "../utils";
|
|
||||||
|
|
||||||
export class AllowedGuilds extends BaseRepository {
|
export class AllowedGuilds extends BaseRepository {
|
||||||
private allowedGuilds: Repository<AllowedGuild>;
|
private allowedGuilds: Repository<AllowedGuild>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.allowedGuilds = getRepository(AllowedGuild);
|
this.allowedGuilds = dataSource.getRepository(AllowedGuild);
|
||||||
}
|
}
|
||||||
|
|
||||||
async isAllowed(guildId) {
|
async isAllowed(guildId: string) {
|
||||||
const count = await this.allowedGuilds.count({
|
const count = await this.allowedGuilds.count({
|
||||||
where: {
|
where: {
|
||||||
id: guildId,
|
id: guildId,
|
||||||
|
@ -22,11 +23,15 @@ export class AllowedGuilds extends BaseRepository {
|
||||||
return count !== 0;
|
return count !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
find(guildId) {
|
find(guildId: string) {
|
||||||
return this.allowedGuilds.findOne(guildId);
|
return this.allowedGuilds.findOne({
|
||||||
|
where: {
|
||||||
|
id: guildId,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getForApiUser(userId) {
|
getForApiUser(userId: string) {
|
||||||
return this.allowedGuilds
|
return this.allowedGuilds
|
||||||
.createQueryBuilder("allowed_guilds")
|
.createQueryBuilder("allowed_guilds")
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
|
import { Repository } from "typeorm/index";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
import { getRepository, Repository } from "typeorm/index";
|
|
||||||
import { ApiAuditLogEntry } from "./entities/ApiAuditLogEntry";
|
|
||||||
import { ApiLogin } from "./entities/ApiLogin";
|
|
||||||
import { AuditLogEventData, AuditLogEventType } from "./apiAuditLogTypes";
|
import { AuditLogEventData, AuditLogEventType } from "./apiAuditLogTypes";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { ApiAuditLogEntry } from "./entities/ApiAuditLogEntry";
|
||||||
|
|
||||||
export class ApiAuditLog extends BaseRepository {
|
export class ApiAuditLog extends BaseRepository {
|
||||||
private auditLog: Repository<ApiAuditLogEntry<any>>;
|
private auditLog: Repository<ApiAuditLogEntry<any>>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.auditLog = getRepository(ApiAuditLogEntry);
|
this.auditLog = dataSource.getRepository(ApiAuditLogEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
addEntry<TEventType extends AuditLogEventType>(
|
addEntry<TEventType extends AuditLogEventType>(
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
// tslint:disable-next-line:no-submodule-imports
|
// tslint:disable-next-line:no-submodule-imports
|
||||||
import uuidv4 from "uuid/v4";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { DAYS, DBDateFormat } from "../utils";
|
import { DAYS, DBDateFormat } from "../utils";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { ApiLogin } from "./entities/ApiLogin";
|
import { ApiLogin } from "./entities/ApiLogin";
|
||||||
|
|
||||||
const LOGIN_EXPIRY_TIME = 1 * DAYS;
|
const LOGIN_EXPIRY_TIME = 1 * DAYS;
|
||||||
|
@ -14,7 +15,7 @@ export class ApiLogins extends BaseRepository {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.apiLogins = getRepository(ApiLogin);
|
this.apiLogins = dataSource.getRepository(ApiLogin);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserIdByApiKey(apiKey: string): Promise<string | null> {
|
async getUserIdByApiKey(apiKey: string): Promise<string | null> {
|
||||||
|
@ -90,10 +91,15 @@ export class ApiLogins extends BaseRepository {
|
||||||
const [loginId, token] = apiKey.split(".");
|
const [loginId, token] = apiKey.split(".");
|
||||||
if (!loginId || !token) return;
|
if (!loginId || !token) return;
|
||||||
|
|
||||||
|
const updatedTime = moment().utc().add(LOGIN_EXPIRY_TIME, "ms");
|
||||||
|
|
||||||
|
const login = await this.apiLogins.createQueryBuilder().where("id = :id", { id: loginId }).getOne();
|
||||||
|
if (!login || moment.utc(login.expires_at).isSameOrAfter(updatedTime)) return;
|
||||||
|
|
||||||
await this.apiLogins.update(
|
await this.apiLogins.update(
|
||||||
{ id: loginId },
|
{ id: loginId },
|
||||||
{
|
{
|
||||||
expires_at: moment().utc().add(LOGIN_EXPIRY_TIME, "ms").format(DBDateFormat),
|
expires_at: updatedTime.format(DBDateFormat),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { ApiPermissions } from "@shared/apiPermissions";
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
|
|
||||||
import { Permissions } from "discord.js";
|
|
||||||
import { ApiAuditLog } from "./ApiAuditLog";
|
import { ApiAuditLog } from "./ApiAuditLog";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
import { AuditLogEventTypes } from "./apiAuditLogTypes";
|
import { AuditLogEventTypes } from "./apiAuditLogTypes";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
|
||||||
|
|
||||||
export enum ApiPermissionTypes {
|
export enum ApiPermissionTypes {
|
||||||
User = "USER",
|
User = "USER",
|
||||||
|
@ -17,7 +17,7 @@ export class ApiPermissionAssignments extends BaseRepository {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.apiPermissions = getRepository(ApiPermissionAssignment);
|
this.apiPermissions = dataSource.getRepository(ApiPermissionAssignment);
|
||||||
this.auditLogs = new ApiAuditLog();
|
this.auditLogs = new ApiAuditLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +80,8 @@ export class ApiPermissionAssignments extends BaseRepository {
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.where("expires_at IS NOT NULL")
|
.where("expires_at IS NOT NULL")
|
||||||
.andWhere("expires_at <= NOW()")
|
.andWhere("expires_at <= NOW()")
|
||||||
.delete();
|
.delete()
|
||||||
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyOwnerChange(guildId: string, newOwnerId: string) {
|
async applyOwnerChange(guildId: string, newOwnerId: string) {
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { DBDateFormat } from "../utils";
|
import { DBDateFormat } from "../utils";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
import { connection } from "./db";
|
import { dataSource } from "./dataSource";
|
||||||
import { ApiUserInfo as ApiUserInfoEntity, ApiUserInfoData } from "./entities/ApiUserInfo";
|
import { ApiUserInfoData, ApiUserInfo as ApiUserInfoEntity } from "./entities/ApiUserInfo";
|
||||||
|
|
||||||
export class ApiUserInfo extends BaseRepository {
|
export class ApiUserInfo extends BaseRepository {
|
||||||
private apiUserInfo: Repository<ApiUserInfoEntity>;
|
private apiUserInfo: Repository<ApiUserInfoEntity>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.apiUserInfo = getRepository(ApiUserInfoEntity);
|
this.apiUserInfo = dataSource.getRepository(ApiUserInfoEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(id) {
|
get(id) {
|
||||||
|
@ -22,7 +22,7 @@ export class ApiUserInfo extends BaseRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id, data: ApiUserInfoData) {
|
update(id, data: ApiUserInfoData) {
|
||||||
return connection.transaction(async (entityManager) => {
|
return dataSource.transaction(async (entityManager) => {
|
||||||
const repo = entityManager.getRepository(ApiUserInfoEntity);
|
const repo = entityManager.getRepository(ApiUserInfoEntity);
|
||||||
|
|
||||||
const existingInfo = await repo.findOne({ where: { id } });
|
const existingInfo = await repo.findOne({ where: { id } });
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
||||||
|
|
||||||
export class Archives extends BaseRepository {
|
export class Archives extends BaseRepository {
|
||||||
protected archives: Repository<ArchiveEntry>;
|
protected archives: Repository<ArchiveEntry>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.archives = getRepository(ArchiveEntry);
|
this.archives = dataSource.getRepository(ArchiveEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteExpiredArchives() {
|
public deleteExpiredArchives() {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
|
||||||
export class BaseGuildRepository<TEntity extends unknown = unknown> extends BaseRepository<TEntity> {
|
export class BaseGuildRepository<TEntity = unknown> extends BaseRepository<TEntity> {
|
||||||
private static guildInstances: Map<string, any>;
|
private static guildInstances: Map<string, any>;
|
||||||
|
|
||||||
protected guildId: string;
|
protected guildId: string;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { asyncMap } from "../utils/async";
|
import { asyncMap } from "../utils/async";
|
||||||
|
|
||||||
export class BaseRepository<TEntity extends unknown = unknown> {
|
export class BaseRepository<TEntity = unknown> {
|
||||||
private nextRelations: string[];
|
private nextRelations: string[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -40,7 +40,7 @@ export class BaseRepository<TEntity extends unknown = unknown> {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async processEntityFromDB<T extends TEntity | undefined>(entity: T): Promise<T> {
|
protected async processEntityFromDB<T extends TEntity | null>(entity: T): Promise<T> {
|
||||||
return this._processEntityFromDB(entity);
|
return this._processEntityFromDB(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { isAPI } from "../globals";
|
import { isAPI } from "../globals";
|
||||||
import { HOURS, SECONDS } from "../utils";
|
import { HOURS, SECONDS } from "../utils";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
import { cleanupConfigs } from "./cleanup/configs";
|
import { cleanupConfigs } from "./cleanup/configs";
|
||||||
import { connection } from "./db";
|
import { dataSource } from "./dataSource";
|
||||||
import { Config } from "./entities/Config";
|
import { Config } from "./entities/Config";
|
||||||
|
|
||||||
if (isAPI()) {
|
const CLEANUP_INTERVAL = 1 * HOURS;
|
||||||
const CLEANUP_INTERVAL = 1 * HOURS;
|
|
||||||
|
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
await cleanupConfigs();
|
await cleanupConfigs();
|
||||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAPI()) {
|
||||||
// Start first cleanup 30 seconds after startup
|
// Start first cleanup 30 seconds after startup
|
||||||
|
// TODO: Move to bot startup code
|
||||||
setTimeout(cleanup, 30 * SECONDS);
|
setTimeout(cleanup, 30 * SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ export class Configs extends BaseRepository {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.configs = getRepository(Config);
|
this.configs = dataSource.getRepository(Config);
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveByKey(key) {
|
getActiveByKey(key) {
|
||||||
|
@ -36,7 +37,7 @@ export class Configs extends BaseRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHighestId(): Promise<number> {
|
async getHighestId(): Promise<number> {
|
||||||
const rows = await connection.query("SELECT MAX(id) AS highest_id FROM configs");
|
const rows = await dataSource.query("SELECT MAX(id) AS highest_id FROM configs");
|
||||||
return (rows.length && rows[0].highest_id) || 0;
|
return (rows.length && rows[0].highest_id) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +62,7 @@ export class Configs extends BaseRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveNewRevision(key, config, editedBy) {
|
async saveNewRevision(key, config, editedBy) {
|
||||||
return connection.transaction(async (entityManager) => {
|
return dataSource.transaction(async (entityManager) => {
|
||||||
const repo = entityManager.getRepository(Config);
|
const repo = entityManager.getRepository(Config);
|
||||||
// Mark all old revisions inactive
|
// Mark all old revisions inactive
|
||||||
await repo.update({ key }, { is_active: false });
|
await repo.update({ key }, { is_active: false });
|
||||||
|
|
|
@ -11,10 +11,10 @@
|
||||||
"MEMBER_UNBAN": "{timestamp} 🔓 User (`{userId}`) was unbanned by {userMention(mod)}",
|
"MEMBER_UNBAN": "{timestamp} 🔓 User (`{userId}`) was unbanned by {userMention(mod)}",
|
||||||
"MEMBER_FORCEBAN": "{timestamp} 🔨 User (`{userId}`) was forcebanned by {userMention(mod)}",
|
"MEMBER_FORCEBAN": "{timestamp} 🔨 User (`{userId}`) was forcebanned by {userMention(mod)}",
|
||||||
"MEMBER_SOFTBAN": "{timestamp} 🔨 {userMention(member)} was softbanned by {userMention(mod)}",
|
"MEMBER_SOFTBAN": "{timestamp} 🔨 {userMention(member)} was softbanned by {userMention(mod)}",
|
||||||
"MEMBER_JOIN": "{timestamp} 📥 {new} {userMention(member)} joined (created {account_age} ago)",
|
"MEMBER_JOIN": "{timestamp} 📥 {new} {userMention(member)} joined (created <t:{account_age_ts}:R>)",
|
||||||
"MEMBER_LEAVE": "{timestamp} 📤 {userMention(member)} left the server",
|
"MEMBER_LEAVE": "{timestamp} 📤 {userMention(member)} left the server",
|
||||||
"MEMBER_ROLE_ADD": "{timestamp} 🔑 {userMention(member)} received roles: **{roles}**",
|
"MEMBER_ROLE_ADD": "{timestamp} 🔑 {userMention(mod)} added roles for {userMention(member)}: **{roles}**",
|
||||||
"MEMBER_ROLE_REMOVE": "{timestamp} 🔑 {userMention(member)} lost roles: **{roles}**",
|
"MEMBER_ROLE_REMOVE": "{timestamp} 🔑 {userMention(mod)} removed roles from {userMention(member)}: **{roles}**",
|
||||||
"MEMBER_ROLE_CHANGES": "{timestamp} 🔑 {userMention(member)} had role changes: received **{addedRoles}**, lost **{removedRoles}**",
|
"MEMBER_ROLE_CHANGES": "{timestamp} 🔑 {userMention(member)} had role changes: received **{addedRoles}**, lost **{removedRoles}**",
|
||||||
"MEMBER_NICK_CHANGE": "{timestamp} ✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**",
|
"MEMBER_NICK_CHANGE": "{timestamp} ✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**",
|
||||||
"MEMBER_USERNAME_CHANGE": "{timestamp} ✏ {userMention(user)}: username changed from **{oldName}** to **{newName}**",
|
"MEMBER_USERNAME_CHANGE": "{timestamp} ✏ {userMention(user)}: username changed from **{oldName}** to **{newName}**",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { AntiraidLevel } from "./entities/AntiraidLevel";
|
import { AntiraidLevel } from "./entities/AntiraidLevel";
|
||||||
|
|
||||||
export class GuildAntiraidLevels extends BaseGuildRepository {
|
export class GuildAntiraidLevels extends BaseGuildRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class GuildAntiraidLevels extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId: string) {
|
constructor(guildId: string) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.antiraidLevels = getRepository(AntiraidLevel);
|
this.antiraidLevels = dataSource.getRepository(AntiraidLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
async get() {
|
async get() {
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import { Guild, Snowflake, User } from "discord.js";
|
import { Guild, Snowflake } from "discord.js";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { isDefaultSticker } from "src/utils/isDefaultSticker";
|
import { isDefaultSticker } from "src/utils/isDefaultSticker";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { renderTemplate, TemplateSafeValueContainer } from "../templateFormatter";
|
import { TemplateSafeValueContainer, renderTemplate } from "../templateFormatter";
|
||||||
import { trimLines } from "../utils";
|
import { renderUsername, trimLines } from "../utils";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
|
||||||
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
|
||||||
import {
|
|
||||||
channelToTemplateSafeChannel,
|
|
||||||
guildToTemplateSafeGuild,
|
|
||||||
userToTemplateSafeUser,
|
|
||||||
} from "../utils/templateSafeObjects";
|
|
||||||
import { SavedMessage } from "./entities/SavedMessage";
|
|
||||||
import { decrypt, encrypt } from "../utils/crypt";
|
import { decrypt, encrypt } from "../utils/crypt";
|
||||||
|
import { channelToTemplateSafeChannel, guildToTemplateSafeGuild } from "../utils/templateSafeObjects";
|
||||||
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
||||||
|
import { SavedMessage } from "./entities/SavedMessage";
|
||||||
|
|
||||||
const DEFAULT_EXPIRY_DAYS = 30;
|
const DEFAULT_EXPIRY_DAYS = 30;
|
||||||
|
|
||||||
|
@ -20,14 +17,14 @@ const MESSAGE_ARCHIVE_HEADER_FORMAT = trimLines(`
|
||||||
Server: {guild.name} ({guild.id})
|
Server: {guild.name} ({guild.id})
|
||||||
`);
|
`);
|
||||||
const MESSAGE_ARCHIVE_MESSAGE_FORMAT =
|
const MESSAGE_ARCHIVE_MESSAGE_FORMAT =
|
||||||
"[#{channel.name}] [{user.id}] [{timestamp}] {user.username}#{user.discriminator}: {content}{attachments}{stickers}";
|
"[#{channel.name}] [{user.id}] [{timestamp}] {username}: {content}{attachments}{stickers}";
|
||||||
|
|
||||||
export class GuildArchives extends BaseGuildRepository<ArchiveEntry> {
|
export class GuildArchives extends BaseGuildRepository<ArchiveEntry> {
|
||||||
protected archives: Repository<ArchiveEntry>;
|
protected archives: Repository<ArchiveEntry>;
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.archives = getRepository(ArchiveEntry);
|
this.archives = dataSource.getRepository(ArchiveEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _processEntityFromDB(entity: ArchiveEntry | undefined) {
|
protected async _processEntityFromDB(entity: ArchiveEntry | undefined) {
|
||||||
|
@ -46,7 +43,7 @@ export class GuildArchives extends BaseGuildRepository<ArchiveEntry> {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
async find(id: string): Promise<ArchiveEntry | undefined> {
|
async find(id: string): Promise<ArchiveEntry | null> {
|
||||||
const result = await this.archives.findOne({
|
const result = await this.archives.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations: this.getRelations(),
|
relations: this.getRelations(),
|
||||||
|
@ -101,6 +98,7 @@ export class GuildArchives extends BaseGuildRepository<ArchiveEntry> {
|
||||||
}),
|
}),
|
||||||
user: partialUser,
|
user: partialUser,
|
||||||
channel: channel ? channelToTemplateSafeChannel(channel) : null,
|
channel: channel ? channelToTemplateSafeChannel(channel) : null,
|
||||||
|
username: renderUsername(msg.data.author.username, msg.data.author.discriminator),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { AutoReaction } from "./entities/AutoReaction";
|
import { AutoReaction } from "./entities/AutoReaction";
|
||||||
|
|
||||||
export class GuildAutoReactions extends BaseGuildRepository {
|
export class GuildAutoReactions extends BaseGuildRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class GuildAutoReactions extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.autoReactions = getRepository(AutoReaction);
|
this.autoReactions = dataSource.getRepository(AutoReaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
async all(): Promise<AutoReaction[]> {
|
async all(): Promise<AutoReaction[]> {
|
||||||
|
@ -18,7 +19,7 @@ export class GuildAutoReactions extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getForChannel(channelId: string): Promise<AutoReaction | undefined> {
|
async getForChannel(channelId: string): Promise<AutoReaction | null> {
|
||||||
return this.autoReactions.findOne({
|
return this.autoReactions.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
|
|
@ -12,15 +12,19 @@ export class GuildButtonRoles extends BaseGuildRepository {
|
||||||
|
|
||||||
async getForButtonId(buttonId: string) {
|
async getForButtonId(buttonId: string) {
|
||||||
return this.buttonRoles.findOne({
|
return this.buttonRoles.findOne({
|
||||||
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
button_id: buttonId,
|
button_id: buttonId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllForMessageId(messageId: string) {
|
async getAllForMessageId(messageId: string) {
|
||||||
return this.buttonRoles.find({
|
return this.buttonRoles.find({
|
||||||
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
message_id: messageId,
|
message_id: messageId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,8 +44,10 @@ export class GuildButtonRoles extends BaseGuildRepository {
|
||||||
|
|
||||||
async getForButtonGroup(buttonGroup: string) {
|
async getForButtonGroup(buttonGroup: string) {
|
||||||
return this.buttonRoles.find({
|
return this.buttonRoles.find({
|
||||||
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
button_group: buttonGroup,
|
button_group: buttonGroup,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import { getRepository, In, InsertResult, Repository } from "typeorm";
|
import { In, InsertResult, Repository } from "typeorm";
|
||||||
|
import { Queue } from "../Queue";
|
||||||
|
import { chunkArray } from "../utils";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { CaseTypes } from "./CaseTypes";
|
import { CaseTypes } from "./CaseTypes";
|
||||||
import { connection } from "./db";
|
import { dataSource } from "./dataSource";
|
||||||
import { Case } from "./entities/Case";
|
import { Case } from "./entities/Case";
|
||||||
import { CaseNote } from "./entities/CaseNote";
|
import { CaseNote } from "./entities/CaseNote";
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { chunkArray } from "../utils";
|
|
||||||
import { Queue } from "../Queue";
|
|
||||||
|
|
||||||
const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
|
|
||||||
|
|
||||||
export class GuildCases extends BaseGuildRepository {
|
export class GuildCases extends BaseGuildRepository {
|
||||||
private cases: Repository<Case>;
|
private cases: Repository<Case>;
|
||||||
|
@ -18,8 +15,8 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.cases = getRepository(Case);
|
this.cases = dataSource.getRepository(Case);
|
||||||
this.caseNotes = getRepository(CaseNote);
|
this.caseNotes = dataSource.getRepository(CaseNote);
|
||||||
this.createQueue = new Queue();
|
this.createQueue = new Queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +30,7 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async find(id: number): Promise<Case | undefined> {
|
async find(id: number): Promise<Case | null> {
|
||||||
return this.cases.findOne({
|
return this.cases.findOne({
|
||||||
relations: this.getRelations(),
|
relations: this.getRelations(),
|
||||||
where: {
|
where: {
|
||||||
|
@ -43,7 +40,7 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByCaseNumber(caseNumber: number): Promise<Case | undefined> {
|
async findByCaseNumber(caseNumber: number): Promise<Case | null> {
|
||||||
return this.cases.findOne({
|
return this.cases.findOne({
|
||||||
relations: this.getRelations(),
|
relations: this.getRelations(),
|
||||||
where: {
|
where: {
|
||||||
|
@ -53,7 +50,7 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findLatestByModId(modId: string): Promise<Case | undefined> {
|
async findLatestByModId(modId: string): Promise<Case | null> {
|
||||||
return this.cases.findOne({
|
return this.cases.findOne({
|
||||||
relations: this.getRelations(),
|
relations: this.getRelations(),
|
||||||
where: {
|
where: {
|
||||||
|
@ -66,7 +63,7 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByAuditLogId(auditLogId: string): Promise<Case | undefined> {
|
async findByAuditLogId(auditLogId: string): Promise<Case | null> {
|
||||||
return this.cases.findOne({
|
return this.cases.findOne({
|
||||||
relations: this.getRelations(),
|
relations: this.getRelations(),
|
||||||
where: {
|
where: {
|
||||||
|
@ -91,7 +88,7 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
mod_id: modId,
|
mod_id: modId,
|
||||||
is_hidden: 0,
|
is_hidden: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -102,7 +99,7 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
mod_id: modId,
|
mod_id: modId,
|
||||||
is_hidden: 0,
|
is_hidden: false,
|
||||||
},
|
},
|
||||||
skip,
|
skip,
|
||||||
take: count,
|
take: count,
|
||||||
|
@ -184,7 +181,7 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async softDelete(id: number, deletedById: string, deletedByName: string, deletedByText: string) {
|
async softDelete(id: number, deletedById: string, deletedByName: string, deletedByText: string) {
|
||||||
return connection.transaction(async (entityManager) => {
|
return dataSource.transaction(async (entityManager) => {
|
||||||
const cases = entityManager.getRepository(Case);
|
const cases = entityManager.getRepository(Case);
|
||||||
const caseNotes = entityManager.getRepository(CaseNote);
|
const caseNotes = entityManager.getRepository(CaseNote);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { DeleteResult, getRepository, InsertResult, Repository } from "typeorm";
|
import { DeleteResult, InsertResult, Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { ContextMenuLink } from "./entities/ContextMenuLink";
|
import { ContextMenuLink } from "./entities/ContextMenuLink";
|
||||||
|
|
||||||
export class GuildContextMenuLinks extends BaseGuildRepository {
|
export class GuildContextMenuLinks extends BaseGuildRepository {
|
||||||
|
@ -7,10 +8,10 @@ export class GuildContextMenuLinks extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.contextLinks = getRepository(ContextMenuLink);
|
this.contextLinks = dataSource.getRepository(ContextMenuLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string): Promise<ContextMenuLink | undefined> {
|
async get(id: string): Promise<ContextMenuLink | null> {
|
||||||
return this.contextLinks.findOne({
|
return this.contextLinks.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { FindConditions, getRepository, In, IsNull, Not, Repository } from "typeorm";
|
import { FindOptionsWhere, In, IsNull, Not, Repository } from "typeorm";
|
||||||
import { Queue } from "../Queue";
|
import { Queue } from "../Queue";
|
||||||
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
|
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { connection } from "./db";
|
import { dataSource } from "./dataSource";
|
||||||
import { Counter } from "./entities/Counter";
|
import { Counter } from "./entities/Counter";
|
||||||
import { CounterTrigger, isValidCounterComparisonOp, TriggerComparisonOp } from "./entities/CounterTrigger";
|
import { CounterTrigger, TriggerComparisonOp, isValidCounterComparisonOp } from "./entities/CounterTrigger";
|
||||||
import { CounterTriggerState } from "./entities/CounterTriggerState";
|
import { CounterTriggerState } from "./entities/CounterTriggerState";
|
||||||
import { CounterValue } from "./entities/CounterValue";
|
import { CounterValue } from "./entities/CounterValue";
|
||||||
|
|
||||||
|
@ -17,11 +17,11 @@ const MAX_COUNTER_VALUE = 2147483647; // 2^31-1, for MySQL INT
|
||||||
const decayQueue = new Queue();
|
const decayQueue = new Queue();
|
||||||
|
|
||||||
async function deleteCountersMarkedToBeDeleted(): Promise<void> {
|
async function deleteCountersMarkedToBeDeleted(): Promise<void> {
|
||||||
await getRepository(Counter).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
|
await dataSource.getRepository(Counter).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteTriggersMarkedToBeDeleted(): Promise<void> {
|
async function deleteTriggersMarkedToBeDeleted(): Promise<void> {
|
||||||
await getRepository(CounterTrigger).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
|
await dataSource.getRepository(CounterTrigger).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(deleteCountersMarkedToBeDeleted, 1 * HOURS);
|
setInterval(deleteCountersMarkedToBeDeleted, 1 * HOURS);
|
||||||
|
@ -38,10 +38,10 @@ export class GuildCounters extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.counters = getRepository(Counter);
|
this.counters = dataSource.getRepository(Counter);
|
||||||
this.counterValues = getRepository(CounterValue);
|
this.counterValues = dataSource.getRepository(CounterValue);
|
||||||
this.counterTriggers = getRepository(CounterTrigger);
|
this.counterTriggers = dataSource.getRepository(CounterTrigger);
|
||||||
this.counterTriggerStates = getRepository(CounterTriggerState);
|
this.counterTriggerStates = dataSource.getRepository(CounterTriggerState);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOrCreateCounter(name: string, perChannel: boolean, perUser: boolean): Promise<Counter> {
|
async findOrCreateCounter(name: string, perChannel: boolean, perUser: boolean): Promise<Counter> {
|
||||||
|
@ -80,7 +80,7 @@ export class GuildCounters extends BaseGuildRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async markUnusedCountersToBeDeleted(idsToKeep: number[]): Promise<void> {
|
async markUnusedCountersToBeDeleted(idsToKeep: number[]): Promise<void> {
|
||||||
const criteria: FindConditions<Counter> = {
|
const criteria: FindOptionsWhere<Counter> = {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
delete_at: IsNull(),
|
delete_at: IsNull(),
|
||||||
};
|
};
|
||||||
|
@ -161,7 +161,7 @@ export class GuildCounters extends BaseGuildRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
|
const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
|
||||||
if (decayAmountToApply === 0) {
|
if (decayAmountToApply === 0 || Number.isNaN(decayAmountToApply)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,10 +256,12 @@ export class GuildCounters extends BaseGuildRepository {
|
||||||
throw new Error(`Invalid comparison value: ${reverseComparisonValue}`);
|
throw new Error(`Invalid comparison value: ${reverseComparisonValue}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return connection.transaction(async (entityManager) => {
|
return dataSource.transaction(async (entityManager) => {
|
||||||
const existing = await entityManager.findOne(CounterTrigger, {
|
const existing = await entityManager.findOne(CounterTrigger, {
|
||||||
|
where: {
|
||||||
counter_id: counterId,
|
counter_id: counterId,
|
||||||
name: triggerName,
|
name: triggerName,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
@ -283,7 +285,11 @@ export class GuildCounters extends BaseGuildRepository {
|
||||||
reverse_comparison_value: reverseComparisonValue,
|
reverse_comparison_value: reverseComparisonValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (await entityManager.findOne(CounterTrigger, insertResult.identifiers[0].id))!;
|
return (await entityManager.findOne(CounterTrigger, {
|
||||||
|
where: {
|
||||||
|
id: insertResult.identifiers[0].id,
|
||||||
|
},
|
||||||
|
}))!;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,11 +314,13 @@ export class GuildCounters extends BaseGuildRepository {
|
||||||
channelId = channelId || "0";
|
channelId = channelId || "0";
|
||||||
userId = userId || "0";
|
userId = userId || "0";
|
||||||
|
|
||||||
return connection.transaction(async (entityManager) => {
|
return dataSource.transaction(async (entityManager) => {
|
||||||
const previouslyTriggered = await entityManager.findOne(CounterTriggerState, {
|
const previouslyTriggered = await entityManager.findOne(CounterTriggerState, {
|
||||||
|
where: {
|
||||||
trigger_id: counterTrigger.id,
|
trigger_id: counterTrigger.id,
|
||||||
user_id: userId!,
|
user_id: userId!,
|
||||||
channel_id: channelId!,
|
channel_id: channelId!,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (previouslyTriggered) {
|
if (previouslyTriggered) {
|
||||||
|
@ -356,7 +364,7 @@ export class GuildCounters extends BaseGuildRepository {
|
||||||
async checkAllValuesForTrigger(
|
async checkAllValuesForTrigger(
|
||||||
counterTrigger: CounterTrigger,
|
counterTrigger: CounterTrigger,
|
||||||
): Promise<Array<{ channelId: string; userId: string }>> {
|
): Promise<Array<{ channelId: string; userId: string }>> {
|
||||||
return connection.transaction(async (entityManager) => {
|
return dataSource.transaction(async (entityManager) => {
|
||||||
const matchingValues = await entityManager
|
const matchingValues = await entityManager
|
||||||
.createQueryBuilder(CounterValue, "cv")
|
.createQueryBuilder(CounterValue, "cv")
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
|
@ -407,7 +415,7 @@ export class GuildCounters extends BaseGuildRepository {
|
||||||
channelId = channelId || "0";
|
channelId = channelId || "0";
|
||||||
userId = userId || "0";
|
userId = userId || "0";
|
||||||
|
|
||||||
return connection.transaction(async (entityManager) => {
|
return dataSource.transaction(async (entityManager) => {
|
||||||
const matchingValue = await entityManager
|
const matchingValue = await entityManager
|
||||||
.createQueryBuilder(CounterValue, "cv")
|
.createQueryBuilder(CounterValue, "cv")
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
|
@ -446,7 +454,7 @@ export class GuildCounters extends BaseGuildRepository {
|
||||||
async checkAllValuesForReverseTrigger(
|
async checkAllValuesForReverseTrigger(
|
||||||
counterTrigger: CounterTrigger,
|
counterTrigger: CounterTrigger,
|
||||||
): Promise<Array<{ channelId: string; userId: string }>> {
|
): Promise<Array<{ channelId: string; userId: string }>> {
|
||||||
return connection.transaction(async (entityManager) => {
|
return dataSource.transaction(async (entityManager) => {
|
||||||
const matchingValues: Array<{
|
const matchingValues: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
triggerStateId: string;
|
triggerStateId: string;
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { Mute } from "./entities/Mute";
|
import { Mute } from "./entities/Mute";
|
||||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
|
||||||
import { Reminder } from "./entities/Reminder";
|
import { Reminder } from "./entities/Reminder";
|
||||||
|
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||||
import { Tempban } from "./entities/Tempban";
|
import { Tempban } from "./entities/Tempban";
|
||||||
import { VCAlert } from "./entities/VCAlert";
|
import { VCAlert } from "./entities/VCAlert";
|
||||||
|
|
||||||
interface GuildEventArgs extends Record<string, unknown[]> {
|
interface GuildEventArgs extends Record<string, unknown[]> {
|
||||||
expiredMute: [Mute];
|
expiredMute: [Mute];
|
||||||
|
timeoutMuteToRenew: [Mute];
|
||||||
scheduledPost: [ScheduledPost];
|
scheduledPost: [ScheduledPost];
|
||||||
reminder: [Reminder];
|
reminder: [Reminder];
|
||||||
expiredTempban: [Tempban];
|
expiredTempban: [Tempban];
|
||||||
|
|
102
backend/src/data/GuildMemberCache.ts
Normal file
102
backend/src/data/GuildMemberCache.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
import { Blocker } from "../Blocker";
|
||||||
|
import { DBDateFormat, MINUTES } from "../utils";
|
||||||
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { MemberCacheItem } from "./entities/MemberCacheItem";
|
||||||
|
|
||||||
|
const SAVE_PENDING_BLOCKER_KEY = "save-pending" as const;
|
||||||
|
|
||||||
|
const DELETION_DELAY = 5 * MINUTES;
|
||||||
|
|
||||||
|
type UpdateData = Pick<MemberCacheItem, "username" | "nickname" | "roles">;
|
||||||
|
|
||||||
|
export class GuildMemberCache extends BaseGuildRepository {
|
||||||
|
#memberCache: Repository<MemberCacheItem>;
|
||||||
|
|
||||||
|
#pendingUpdates: Map<string, Partial<MemberCacheItem>>;
|
||||||
|
|
||||||
|
#blocker: Blocker;
|
||||||
|
|
||||||
|
constructor(guildId: string) {
|
||||||
|
super(guildId);
|
||||||
|
this.#memberCache = dataSource.getRepository(MemberCacheItem);
|
||||||
|
this.#pendingUpdates = new Map();
|
||||||
|
this.#blocker = new Blocker();
|
||||||
|
}
|
||||||
|
|
||||||
|
async savePendingUpdates(): Promise<void> {
|
||||||
|
await this.#blocker.waitToBeUnblocked(SAVE_PENDING_BLOCKER_KEY);
|
||||||
|
|
||||||
|
if (this.#pendingUpdates.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#blocker.block(SAVE_PENDING_BLOCKER_KEY);
|
||||||
|
|
||||||
|
const entitiesToSave = Array.from(this.#pendingUpdates.values());
|
||||||
|
this.#pendingUpdates.clear();
|
||||||
|
|
||||||
|
await this.#memberCache.upsert(entitiesToSave, ["guild_id", "user_id"]).finally(() => {
|
||||||
|
this.#blocker.unblock(SAVE_PENDING_BLOCKER_KEY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCachedMemberData(userId: string): Promise<MemberCacheItem | null> {
|
||||||
|
await this.#blocker.waitToBeUnblocked(SAVE_PENDING_BLOCKER_KEY);
|
||||||
|
|
||||||
|
const dbItem = await this.#memberCache.findOne({
|
||||||
|
where: {
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const pendingItem = this.#pendingUpdates.get(userId);
|
||||||
|
if (!dbItem && !pendingItem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = new MemberCacheItem();
|
||||||
|
Object.assign(item, dbItem ?? {});
|
||||||
|
Object.assign(item, pendingItem ?? {});
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCachedMemberData(userId: string, data: UpdateData): Promise<void> {
|
||||||
|
await this.#blocker.waitToBeUnblocked(SAVE_PENDING_BLOCKER_KEY);
|
||||||
|
|
||||||
|
if (!this.#pendingUpdates.has(userId)) {
|
||||||
|
const newItem = new MemberCacheItem();
|
||||||
|
newItem.guild_id = this.guildId;
|
||||||
|
newItem.user_id = userId;
|
||||||
|
this.#pendingUpdates.set(userId, newItem);
|
||||||
|
}
|
||||||
|
Object.assign(this.#pendingUpdates.get(userId)!, data);
|
||||||
|
this.#pendingUpdates.get(userId)!.last_seen = moment().format("YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
async markMemberForDeletion(userId: string): Promise<void> {
|
||||||
|
await this.#memberCache.update(
|
||||||
|
{
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delete_at: moment().add(DELETION_DELAY, "ms").format(DBDateFormat),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unmarkMemberForDeletion(userId: string): Promise<void> {
|
||||||
|
await this.#memberCache.update(
|
||||||
|
{
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delete_at: null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm/index";
|
import { Repository } from "typeorm/index";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { connection } from "./db";
|
import { dataSource } from "./dataSource";
|
||||||
import { MemberTimezone } from "./entities/MemberTimezone";
|
import { MemberTimezone } from "./entities/MemberTimezone";
|
||||||
|
|
||||||
export class GuildMemberTimezones extends BaseGuildRepository {
|
export class GuildMemberTimezones extends BaseGuildRepository {
|
||||||
|
@ -8,22 +8,26 @@ export class GuildMemberTimezones extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId: string) {
|
constructor(guildId: string) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.memberTimezones = getRepository(MemberTimezone);
|
this.memberTimezones = dataSource.getRepository(MemberTimezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(memberId: string) {
|
get(memberId: string) {
|
||||||
return this.memberTimezones.findOne({
|
return this.memberTimezones.findOne({
|
||||||
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
member_id: memberId,
|
member_id: memberId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(memberId, timezone: string) {
|
async set(memberId, timezone: string) {
|
||||||
await connection.transaction(async (entityManager) => {
|
await dataSource.transaction(async (entityManager) => {
|
||||||
const repo = entityManager.getRepository(MemberTimezone);
|
const repo = entityManager.getRepository(MemberTimezone);
|
||||||
const existingRow = await repo.findOne({
|
const existingRow = await repo.findOne({
|
||||||
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
member_id: memberId,
|
member_id: memberId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingRow) {
|
if (existingRow) {
|
||||||
|
|
|
@ -1,14 +1,26 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { Brackets, getRepository, Repository } from "typeorm";
|
import { Brackets, Repository } from "typeorm";
|
||||||
|
import { DBDateFormat } from "../utils";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { MuteTypes } from "./MuteTypes";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { Mute } from "./entities/Mute";
|
import { Mute } from "./entities/Mute";
|
||||||
|
|
||||||
|
export type AddMuteParams = {
|
||||||
|
userId: Mute["user_id"];
|
||||||
|
type: MuteTypes;
|
||||||
|
expiresAt: number | null;
|
||||||
|
rolesToRestore?: Mute["roles_to_restore"];
|
||||||
|
muteRole?: string | null;
|
||||||
|
timeoutExpiresAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class GuildMutes extends BaseGuildRepository {
|
export class GuildMutes extends BaseGuildRepository {
|
||||||
private mutes: Repository<Mute>;
|
private mutes: Repository<Mute>;
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.mutes = getRepository(Mute);
|
this.mutes = dataSource.getRepository(Mute);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getExpiredMutes(): Promise<Mute[]> {
|
async getExpiredMutes(): Promise<Mute[]> {
|
||||||
|
@ -20,7 +32,7 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
async findExistingMuteForUserId(userId: string): Promise<Mute | undefined> {
|
async findExistingMuteForUserId(userId: string): Promise<Mute | null> {
|
||||||
return this.mutes.findOne({
|
return this.mutes.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
@ -34,14 +46,18 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
return mute != null;
|
return mute != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addMute(userId, expiryTime, rolesToRestore?: string[]): Promise<Mute> {
|
async addMute(params: AddMuteParams): Promise<Mute> {
|
||||||
const expiresAt = expiryTime ? moment.utc().add(expiryTime, "ms").format("YYYY-MM-DD HH:mm:ss") : null;
|
const expiresAt = params.expiresAt ? moment.utc(params.expiresAt).format(DBDateFormat) : null;
|
||||||
|
const timeoutExpiresAt = params.timeoutExpiresAt ? moment.utc(params.timeoutExpiresAt).format(DBDateFormat) : null;
|
||||||
|
|
||||||
const result = await this.mutes.insert({
|
const result = await this.mutes.insert({
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId,
|
user_id: params.userId,
|
||||||
|
type: params.type,
|
||||||
expires_at: expiresAt,
|
expires_at: expiresAt,
|
||||||
roles_to_restore: rolesToRestore ?? [],
|
roles_to_restore: params.rolesToRestore ?? [],
|
||||||
|
mute_role: params.muteRole,
|
||||||
|
timeout_expires_at: timeoutExpiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (await this.mutes.findOne({ where: result.identifiers[0] }))!;
|
return (await this.mutes.findOne({ where: result.identifiers[0] }))!;
|
||||||
|
@ -74,6 +90,32 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateExpiresAt(userId: string, timestamp: number | null): Promise<void> {
|
||||||
|
const expiresAt = timestamp ? moment.utc(timestamp).format("YYYY-MM-DD HH:mm:ss") : null;
|
||||||
|
await this.mutes.update(
|
||||||
|
{
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expires_at: expiresAt,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTimeoutExpiresAt(userId: string, timestamp: number): Promise<void> {
|
||||||
|
const timeoutExpiresAt = moment.utc(timestamp).format(DBDateFormat);
|
||||||
|
await this.mutes.update(
|
||||||
|
{
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout_expires_at: timeoutExpiresAt,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getActiveMutes(): Promise<Mute[]> {
|
async getActiveMutes(): Promise<Mute[]> {
|
||||||
return this.mutes
|
return this.mutes
|
||||||
.createQueryBuilder("mutes")
|
.createQueryBuilder("mutes")
|
||||||
|
@ -104,4 +146,16 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fillMissingMuteRole(muteRole: string): Promise<void> {
|
||||||
|
await this.mutes
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||||
|
.andWhere("type = :type", { type: MuteTypes.Role })
|
||||||
|
.andWhere("mute_role IS NULL")
|
||||||
|
.update({
|
||||||
|
mute_role: muteRole,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import { getRepository, In, Repository } from "typeorm";
|
import { In, Repository } from "typeorm";
|
||||||
import { isAPI } from "../globals";
|
import { isAPI } from "../globals";
|
||||||
import { MINUTES, SECONDS } from "../utils";
|
import { MINUTES, SECONDS } from "../utils";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { cleanupNicknames } from "./cleanup/nicknames";
|
import { cleanupNicknames } from "./cleanup/nicknames";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { NicknameHistoryEntry } from "./entities/NicknameHistoryEntry";
|
import { NicknameHistoryEntry } from "./entities/NicknameHistoryEntry";
|
||||||
|
|
||||||
if (!isAPI()) {
|
const CLEANUP_INTERVAL = 5 * MINUTES;
|
||||||
const CLEANUP_INTERVAL = 5 * MINUTES;
|
|
||||||
|
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
await cleanupNicknames();
|
await cleanupNicknames();
|
||||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isAPI()) {
|
||||||
// Start first cleanup 30 seconds after startup
|
// Start first cleanup 30 seconds after startup
|
||||||
|
// TODO: Move to bot startup code
|
||||||
setTimeout(cleanup, 30 * SECONDS);
|
setTimeout(cleanup, 30 * SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +26,7 @@ export class GuildNicknameHistory extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.nicknameHistory = getRepository(NicknameHistoryEntry);
|
this.nicknameHistory = dataSource.getRepository(NicknameHistoryEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByUserId(userId): Promise<NicknameHistoryEntry[]> {
|
async getByUserId(userId): Promise<NicknameHistoryEntry[]> {
|
||||||
|
@ -39,7 +41,7 @@ export class GuildNicknameHistory extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getLastEntry(userId): Promise<NicknameHistoryEntry | undefined> {
|
getLastEntry(userId): Promise<NicknameHistoryEntry | null> {
|
||||||
return this.nicknameHistory.findOne({
|
return this.nicknameHistory.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { PersistedData } from "./entities/PersistedData";
|
import { PersistedData } from "./entities/PersistedData";
|
||||||
|
|
||||||
export interface IPartialPersistData {
|
|
||||||
roles?: string[];
|
|
||||||
nickname?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GuildPersistedData extends BaseGuildRepository {
|
export class GuildPersistedData extends BaseGuildRepository {
|
||||||
private persistedData: Repository<PersistedData>;
|
private persistedData: Repository<PersistedData>;
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.persistedData = getRepository(PersistedData);
|
this.persistedData = dataSource.getRepository(PersistedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async find(userId: string) {
|
async find(userId: string) {
|
||||||
|
@ -24,11 +20,7 @@ export class GuildPersistedData extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(userId: string, data: IPartialPersistData = {}) {
|
async set(userId: string, data: Partial<PersistedData> = {}) {
|
||||||
const finalData: any = {};
|
|
||||||
if (data.roles) finalData.roles = data.roles.join(",");
|
|
||||||
if (data.nickname) finalData.nickname = data.nickname;
|
|
||||||
|
|
||||||
const existing = await this.find(userId);
|
const existing = await this.find(userId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await this.persistedData.update(
|
await this.persistedData.update(
|
||||||
|
@ -36,11 +28,11 @@ export class GuildPersistedData extends BaseGuildRepository {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
},
|
},
|
||||||
finalData,
|
data,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await this.persistedData.insert({
|
await this.persistedData.insert({
|
||||||
...finalData,
|
...data,
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { PingableRole } from "./entities/PingableRole";
|
import { PingableRole } from "./entities/PingableRole";
|
||||||
|
|
||||||
export class GuildPingableRoles extends BaseGuildRepository {
|
export class GuildPingableRoles extends BaseGuildRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class GuildPingableRoles extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.pingableRoles = getRepository(PingableRole);
|
this.pingableRoles = dataSource.getRepository(PingableRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
async all(): Promise<PingableRole[]> {
|
async all(): Promise<PingableRole[]> {
|
||||||
|
@ -27,7 +28,7 @@ export class GuildPingableRoles extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByChannelAndRoleId(channelId: string, roleId: string): Promise<PingableRole | undefined> {
|
async getByChannelAndRoleId(channelId: string, roleId: string): Promise<PingableRole | null> {
|
||||||
return this.pingableRoles.findOne({
|
return this.pingableRoles.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { ReactionRole } from "./entities/ReactionRole";
|
import { ReactionRole } from "./entities/ReactionRole";
|
||||||
|
|
||||||
export class GuildReactionRoles extends BaseGuildRepository {
|
export class GuildReactionRoles extends BaseGuildRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class GuildReactionRoles extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.reactionRoles = getRepository(ReactionRole);
|
this.reactionRoles = dataSource.getRepository(ReactionRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
async all(): Promise<ReactionRole[]> {
|
async all(): Promise<ReactionRole[]> {
|
||||||
|
@ -30,7 +31,7 @@ export class GuildReactionRoles extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByMessageAndEmoji(messageId: string, emoji: string): Promise<ReactionRole | undefined> {
|
async getByMessageAndEmoji(messageId: string, emoji: string): Promise<ReactionRole | null> {
|
||||||
return this.reactionRoles.findOne({
|
return this.reactionRoles.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { Reminder } from "./entities/Reminder";
|
import { Reminder } from "./entities/Reminder";
|
||||||
|
|
||||||
export class GuildReminders extends BaseGuildRepository {
|
export class GuildReminders extends BaseGuildRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class GuildReminders extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.reminders = getRepository(Reminder);
|
this.reminders = dataSource.getRepository(Reminder);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDueReminders(): Promise<Reminder[]> {
|
async getDueReminders(): Promise<Reminder[]> {
|
||||||
|
@ -28,7 +29,9 @@ export class GuildReminders extends BaseGuildRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
find(id: number) {
|
find(id: number) {
|
||||||
return this.reminders.findOne({ id });
|
return this.reminders.findOne({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id) {
|
async delete(id) {
|
||||||
|
|
38
backend/src/data/GuildRoleButtons.ts
Normal file
38
backend/src/data/GuildRoleButtons.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { RoleButtonsItem } from "./entities/RoleButtonsItem";
|
||||||
|
|
||||||
|
export class GuildRoleButtons extends BaseGuildRepository {
|
||||||
|
private roleButtons: Repository<RoleButtonsItem>;
|
||||||
|
|
||||||
|
constructor(guildId) {
|
||||||
|
super(guildId);
|
||||||
|
this.roleButtons = dataSource.getRepository(RoleButtonsItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSavedRoleButtons(): Promise<RoleButtonsItem[]> {
|
||||||
|
return this.roleButtons.find({
|
||||||
|
where: {
|
||||||
|
guild_id: this.guildId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRoleButtonItem(name: string): Promise<void> {
|
||||||
|
await this.roleButtons.delete({
|
||||||
|
guild_id: this.guildId,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveRoleButtonItem(name: string, channelId: string, messageId: string, hash: string): Promise<void> {
|
||||||
|
await this.roleButtons.insert({
|
||||||
|
guild_id: this.guildId,
|
||||||
|
name,
|
||||||
|
channel_id: channelId,
|
||||||
|
message_id: messageId,
|
||||||
|
hash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
44
backend/src/data/GuildRoleQueue.ts
Normal file
44
backend/src/data/GuildRoleQueue.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { RoleQueueItem } from "./entities/RoleQueueItem";
|
||||||
|
|
||||||
|
export class GuildRoleQueue extends BaseGuildRepository {
|
||||||
|
private roleQueue: Repository<RoleQueueItem>;
|
||||||
|
|
||||||
|
constructor(guildId) {
|
||||||
|
super(guildId);
|
||||||
|
this.roleQueue = dataSource.getRepository(RoleQueueItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeNextRoleAssignments(count: number): Promise<RoleQueueItem[]> {
|
||||||
|
return dataSource.transaction(async (entityManager) => {
|
||||||
|
const repository = entityManager.getRepository(RoleQueueItem);
|
||||||
|
|
||||||
|
const nextAssignments = await repository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||||
|
.addOrderBy("priority", "DESC")
|
||||||
|
.addOrderBy("id", "ASC")
|
||||||
|
.take(count)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
if (nextAssignments.length > 0) {
|
||||||
|
const ids = nextAssignments.map((assignment) => assignment.id);
|
||||||
|
await repository.createQueryBuilder().where("id IN (:ids)", { ids }).delete().execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextAssignments;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async addQueueItem(userId: string, roleId: string, shouldAdd: boolean, priority = 0) {
|
||||||
|
await this.roleQueue.insert({
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
role_id: roleId,
|
||||||
|
should_add: shouldAdd,
|
||||||
|
priority,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,14 @@
|
||||||
import { GuildChannel, Message } from "discord.js";
|
import { GuildChannel, Message } from "discord.js";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
|
||||||
import { QueuedEventEmitter } from "../QueuedEventEmitter";
|
import { QueuedEventEmitter } from "../QueuedEventEmitter";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
|
||||||
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
|
|
||||||
import { buildEntity } from "./buildEntity";
|
|
||||||
import { noop } from "../utils";
|
import { noop } from "../utils";
|
||||||
import { decrypt } from "../utils/crypt";
|
|
||||||
import { decryptJson, encryptJson } from "../utils/cryptHelpers";
|
|
||||||
import { asyncMap } from "../utils/async";
|
import { asyncMap } from "../utils/async";
|
||||||
|
import { decryptJson, encryptJson } from "../utils/cryptHelpers";
|
||||||
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { buildEntity } from "./buildEntity";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
|
||||||
|
|
||||||
export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
||||||
private messages: Repository<SavedMessage>;
|
private messages: Repository<SavedMessage>;
|
||||||
|
@ -19,7 +18,7 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.messages = getRepository(SavedMessage);
|
this.messages = dataSource.getRepository(SavedMessage);
|
||||||
this.events = new QueuedEventEmitter();
|
this.events = new QueuedEventEmitter();
|
||||||
|
|
||||||
this.toBePermanent = new Set();
|
this.toBePermanent = new Set();
|
||||||
|
@ -53,13 +52,13 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
||||||
title: embed.title,
|
title: embed.title,
|
||||||
description: embed.description,
|
description: embed.description,
|
||||||
url: embed.url,
|
url: embed.url,
|
||||||
timestamp: embed.timestamp,
|
timestamp: embed.timestamp ? Date.parse(embed.timestamp) : null,
|
||||||
color: embed.color,
|
color: embed.color,
|
||||||
|
|
||||||
fields: embed.fields.map((field) => ({
|
fields: embed.fields.map((field) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
value: field.value,
|
value: field.value,
|
||||||
inline: field.inline,
|
inline: field.inline ?? false,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
author: embed.author
|
author: embed.author
|
||||||
|
@ -128,7 +127,7 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.data = await decryptJson(entity.data as unknown as string);
|
entity.data = (await decryptJson(entity.data as unknown as string)) as ISavedMessageData;
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +138,7 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
async find(id: string, includeDeleted = false): Promise<SavedMessage | undefined> {
|
async find(id: string, includeDeleted = false): Promise<SavedMessage | null> {
|
||||||
let query = this.messages
|
let query = this.messages
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||||
|
|
||||||
export class GuildScheduledPosts extends BaseGuildRepository {
|
export class GuildScheduledPosts extends BaseGuildRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class GuildScheduledPosts extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.scheduledPosts = getRepository(ScheduledPost);
|
this.scheduledPosts = dataSource.getRepository(ScheduledPost);
|
||||||
}
|
}
|
||||||
|
|
||||||
all(): Promise<ScheduledPost[]> {
|
all(): Promise<ScheduledPost[]> {
|
||||||
|
@ -23,7 +24,11 @@ export class GuildScheduledPosts extends BaseGuildRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
find(id: number) {
|
find(id: number) {
|
||||||
return this.scheduledPosts.findOne({ id });
|
return this.scheduledPosts.findOne({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id) {
|
async delete(id) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { SlowmodeChannel } from "./entities/SlowmodeChannel";
|
import { SlowmodeChannel } from "./entities/SlowmodeChannel";
|
||||||
import { SlowmodeUser } from "./entities/SlowmodeUser";
|
import { SlowmodeUser } from "./entities/SlowmodeUser";
|
||||||
|
|
||||||
|
@ -10,11 +11,11 @@ export class GuildSlowmodes extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.slowmodeChannels = getRepository(SlowmodeChannel);
|
this.slowmodeChannels = dataSource.getRepository(SlowmodeChannel);
|
||||||
this.slowmodeUsers = getRepository(SlowmodeUser);
|
this.slowmodeUsers = dataSource.getRepository(SlowmodeUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChannelSlowmode(channelId): Promise<SlowmodeChannel | undefined> {
|
async getChannelSlowmode(channelId): Promise<SlowmodeChannel | null> {
|
||||||
return this.slowmodeChannels.findOne({
|
return this.slowmodeChannels.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
@ -51,11 +52,13 @@ export class GuildSlowmodes extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChannelSlowmodeUser(channelId, userId): Promise<SlowmodeUser | undefined> {
|
async getChannelSlowmodeUser(channelId, userId): Promise<SlowmodeUser | null> {
|
||||||
return this.slowmodeUsers.findOne({
|
return this.slowmodeUsers.findOne({
|
||||||
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
channel_id: channelId,
|
channel_id: channelId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { StarboardMessage } from "./entities/StarboardMessage";
|
import { StarboardMessage } from "./entities/StarboardMessage";
|
||||||
|
|
||||||
export class GuildStarboardMessages extends BaseGuildRepository {
|
export class GuildStarboardMessages extends BaseGuildRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class GuildStarboardMessages extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.allStarboardMessages = getRepository(StarboardMessage);
|
this.allStarboardMessages = dataSource.getRepository(StarboardMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStarboardMessagesForMessageId(messageId: string) {
|
async getStarboardMessagesForMessageId(messageId: string) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { StarboardReaction } from "./entities/StarboardReaction";
|
import { StarboardReaction } from "./entities/StarboardReaction";
|
||||||
|
|
||||||
export class GuildStarboardReactions extends BaseGuildRepository {
|
export class GuildStarboardReactions extends BaseGuildRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class GuildStarboardReactions extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.allStarboardReactions = getRepository(StarboardReaction);
|
this.allStarboardReactions = dataSource.getRepository(StarboardReaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllReactionsForMessageId(messageId: string) {
|
async getAllReactionsForMessageId(messageId: string) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { StatValue } from "./entities/StatValue";
|
import { StatValue } from "./entities/StatValue";
|
||||||
|
|
||||||
export class GuildStats extends BaseGuildRepository {
|
export class GuildStats extends BaseGuildRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class GuildStats extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.stats = getRepository(StatValue);
|
this.stats = dataSource.getRepository(StatValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveValue(source: string, key: string, value: number): Promise<void> {
|
async saveValue(source: string, key: string, value: number): Promise<void> {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { Tag } from "./entities/Tag";
|
import { Tag } from "./entities/Tag";
|
||||||
import { TagResponse } from "./entities/TagResponse";
|
import { TagResponse } from "./entities/TagResponse";
|
||||||
|
|
||||||
|
@ -9,8 +10,8 @@ export class GuildTags extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.tags = getRepository(Tag);
|
this.tags = dataSource.getRepository(Tag);
|
||||||
this.tagResponses = getRepository(TagResponse);
|
this.tagResponses = dataSource.getRepository(TagResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
async all(): Promise<Tag[]> {
|
async all(): Promise<Tag[]> {
|
||||||
|
@ -21,7 +22,7 @@ export class GuildTags extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async find(tag): Promise<Tag | undefined> {
|
async find(tag): Promise<Tag | null> {
|
||||||
return this.tags.findOne({
|
return this.tags.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
@ -61,7 +62,7 @@ export class GuildTags extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findResponseByCommandMessageId(messageId: string): Promise<TagResponse | undefined> {
|
async findResponseByCommandMessageId(messageId: string): Promise<TagResponse | null> {
|
||||||
return this.tagResponses.findOne({
|
return this.tagResponses.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
@ -70,7 +71,7 @@ export class GuildTags extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findResponseByResponseMessageId(messageId: string): Promise<TagResponse | undefined> {
|
async findResponseByResponseMessageId(messageId: string): Promise<TagResponse | null> {
|
||||||
return this.tagResponses.findOne({
|
return this.tagResponses.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { Tempban } from "./entities/Tempban";
|
import { Tempban } from "./entities/Tempban";
|
||||||
|
|
||||||
export class GuildTempbans extends BaseGuildRepository {
|
export class GuildTempbans extends BaseGuildRepository {
|
||||||
|
@ -8,7 +9,7 @@ export class GuildTempbans extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.tempbans = getRepository(Tempban);
|
this.tempbans = dataSource.getRepository(Tempban);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getExpiredTempbans(): Promise<Tempban[]> {
|
async getExpiredTempbans(): Promise<Tempban[]> {
|
||||||
|
@ -20,7 +21,7 @@ export class GuildTempbans extends BaseGuildRepository {
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
async findExistingTempbanForUserId(userId: string): Promise<Tempban | undefined> {
|
async findExistingTempbanForUserId(userId: string): Promise<Tempban | null> {
|
||||||
return this.tempbans.findOne({
|
return this.tempbans.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { VCAlert } from "./entities/VCAlert";
|
import { VCAlert } from "./entities/VCAlert";
|
||||||
|
|
||||||
export class GuildVCAlerts extends BaseGuildRepository {
|
export class GuildVCAlerts extends BaseGuildRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class GuildVCAlerts extends BaseGuildRepository {
|
||||||
|
|
||||||
constructor(guildId) {
|
constructor(guildId) {
|
||||||
super(guildId);
|
super(guildId);
|
||||||
this.allAlerts = getRepository(VCAlert);
|
this.allAlerts = dataSource.getRepository(VCAlert);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOutdatedAlerts(): Promise<VCAlert[]> {
|
async getOutdatedAlerts(): Promise<VCAlert[]> {
|
||||||
|
@ -41,7 +42,9 @@ export class GuildVCAlerts extends BaseGuildRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
find(id: number) {
|
find(id: number) {
|
||||||
return this.allAlerts.findOne({ id });
|
return this.allAlerts.findOne({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id) {
|
async delete(id) {
|
||||||
|
|
30
backend/src/data/MemberCache.ts
Normal file
30
backend/src/data/MemberCache.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
import { DAYS } from "../utils";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { MemberCacheItem } from "./entities/MemberCacheItem";
|
||||||
|
|
||||||
|
const STALE_PERIOD = 90 * DAYS;
|
||||||
|
|
||||||
|
export class MemberCache extends BaseRepository {
|
||||||
|
#memberCache: Repository<MemberCacheItem>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.#memberCache = dataSource.getRepository(MemberCacheItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteStaleData(): Promise<void> {
|
||||||
|
const cutoff = moment().subtract(STALE_PERIOD, "ms").format("YYYY-MM-DD");
|
||||||
|
await this.#memberCache.createQueryBuilder().where("last_seen < :cutoff", { cutoff }).delete().execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMarkedToBeDeletedEntries(): Promise<void> {
|
||||||
|
await this.#memberCache
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where("delete_at IS NOT NULL AND delete_at <= NOW()")
|
||||||
|
.delete()
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
4
backend/src/data/MuteTypes.ts
Normal file
4
backend/src/data/MuteTypes.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export enum MuteTypes {
|
||||||
|
Role = 1,
|
||||||
|
Timeout = 2,
|
||||||
|
}
|
|
@ -1,20 +1,35 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { Brackets, getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { Mute } from "./entities/Mute";
|
|
||||||
import { DAYS, DBDateFormat } from "../utils";
|
import { DAYS, DBDateFormat } from "../utils";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { MuteTypes } from "./MuteTypes";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { Mute } from "./entities/Mute";
|
||||||
|
|
||||||
const OLD_EXPIRED_MUTE_THRESHOLD = 7 * DAYS;
|
const OLD_EXPIRED_MUTE_THRESHOLD = 7 * DAYS;
|
||||||
|
|
||||||
|
export const MAX_TIMEOUT_DURATION = 27 * DAYS;
|
||||||
|
// When a timeout is under this duration but the mute expires later, the timeout will be reset to max duration
|
||||||
|
export const TIMEOUT_RENEWAL_THRESHOLD = 21 * DAYS;
|
||||||
|
|
||||||
export class Mutes extends BaseRepository {
|
export class Mutes extends BaseRepository {
|
||||||
private mutes: Repository<Mute>;
|
private mutes: Repository<Mute>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.mutes = getRepository(Mute);
|
this.mutes = dataSource.getRepository(Mute);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSoonExpiringMutes(threshold: number): Promise<Mute[]> {
|
findMute(guildId: string, userId: string): Promise<Mute | null> {
|
||||||
|
return this.mutes.findOne({
|
||||||
|
where: {
|
||||||
|
guild_id: guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSoonExpiringMutes(threshold: number): Promise<Mute[]> {
|
||||||
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
|
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
|
||||||
return this.mutes
|
return this.mutes
|
||||||
.createQueryBuilder("mutes")
|
.createQueryBuilder("mutes")
|
||||||
|
@ -23,6 +38,16 @@ export class Mutes extends BaseRepository {
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTimeoutMutesToRenew(threshold: number): Promise<Mute[]> {
|
||||||
|
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
|
||||||
|
return this.mutes
|
||||||
|
.createQueryBuilder("mutes")
|
||||||
|
.andWhere("type = :type", { type: MuteTypes.Timeout })
|
||||||
|
.andWhere("(expires_at IS NULL OR timeout_expires_at < expires_at)")
|
||||||
|
.andWhere("timeout_expires_at <= :date", { date: thresholdDateStr })
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
async clearOldExpiredMutes(): Promise<void> {
|
async clearOldExpiredMutes(): Promise<void> {
|
||||||
const thresholdDateStr = moment.utc().subtract(OLD_EXPIRED_MUTE_THRESHOLD, "ms").format(DBDateFormat);
|
const thresholdDateStr = moment.utc().subtract(OLD_EXPIRED_MUTE_THRESHOLD, "ms").format(DBDateFormat);
|
||||||
await this.mutes
|
await this.mutes
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
|
||||||
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry";
|
|
||||||
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman";
|
|
||||||
import fetch, { Headers } from "node-fetch";
|
|
||||||
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
|
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
|
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
import { env } from "../env";
|
||||||
|
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry";
|
||||||
|
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
|
||||||
|
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman";
|
||||||
|
|
||||||
const API_URL = "https://api.phisherman.gg";
|
const API_URL = "https://api.phisherman.gg";
|
||||||
const MASTER_API_KEY = process.env.PHISHERMAN_API_KEY;
|
const MASTER_API_KEY = env.PHISHERMAN_API_KEY;
|
||||||
|
|
||||||
let caughtDomainTrackingMap: Map<string, Map<string, number[]>> = new Map();
|
let caughtDomainTrackingMap: Map<string, Map<string, number[]>> = new Map();
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ const KEY_VALIDITY_LIFETIME = 24 * HOURS;
|
||||||
let cacheRepository: Repository<PhishermanCacheEntry> | null = null;
|
let cacheRepository: Repository<PhishermanCacheEntry> | null = null;
|
||||||
function getCacheRepository(): Repository<PhishermanCacheEntry> {
|
function getCacheRepository(): Repository<PhishermanCacheEntry> {
|
||||||
if (cacheRepository == null) {
|
if (cacheRepository == null) {
|
||||||
cacheRepository = getRepository(PhishermanCacheEntry);
|
cacheRepository = dataSource.getRepository(PhishermanCacheEntry);
|
||||||
}
|
}
|
||||||
return cacheRepository;
|
return cacheRepository;
|
||||||
}
|
}
|
||||||
|
@ -47,7 +48,7 @@ function getCacheRepository(): Repository<PhishermanCacheEntry> {
|
||||||
let keyCacheRepository: Repository<PhishermanKeyCacheEntry> | null = null;
|
let keyCacheRepository: Repository<PhishermanKeyCacheEntry> | null = null;
|
||||||
function getKeyCacheRepository(): Repository<PhishermanKeyCacheEntry> {
|
function getKeyCacheRepository(): Repository<PhishermanKeyCacheEntry> {
|
||||||
if (keyCacheRepository == null) {
|
if (keyCacheRepository == null) {
|
||||||
keyCacheRepository = getRepository(PhishermanKeyCacheEntry);
|
keyCacheRepository = dataSource.getRepository(PhishermanKeyCacheEntry);
|
||||||
}
|
}
|
||||||
return keyCacheRepository;
|
return keyCacheRepository;
|
||||||
}
|
}
|
||||||
|
@ -153,7 +154,9 @@ export async function getPhishermanDomainInfo(domain: string): Promise<Phisherma
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbCache = getCacheRepository();
|
const dbCache = getCacheRepository();
|
||||||
const existingCachedEntry = await dbCache.findOne({ domain });
|
const existingCachedEntry = await dbCache.findOne({
|
||||||
|
where: { domain },
|
||||||
|
});
|
||||||
if (existingCachedEntry) {
|
if (existingCachedEntry) {
|
||||||
return existingCachedEntry.data;
|
return existingCachedEntry.data;
|
||||||
}
|
}
|
||||||
|
@ -196,7 +199,9 @@ export async function phishermanApiKeyIsValid(apiKey: string): Promise<boolean>
|
||||||
|
|
||||||
const keyCache = getKeyCacheRepository();
|
const keyCache = getKeyCacheRepository();
|
||||||
const hash = crypto.createHash("sha256").update(apiKey).digest("hex");
|
const hash = crypto.createHash("sha256").update(apiKey).digest("hex");
|
||||||
const entry = await keyCache.findOne({ hash });
|
const entry = await keyCache.findOne({
|
||||||
|
where: { hash },
|
||||||
|
});
|
||||||
if (entry) {
|
if (entry) {
|
||||||
return entry.is_valid;
|
return entry.is_valid;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
|
||||||
import { Reminder } from "./entities/Reminder";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
import { DBDateFormat } from "../utils";
|
import { DBDateFormat } from "../utils";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { Reminder } from "./entities/Reminder";
|
||||||
|
|
||||||
export class Reminders extends BaseRepository {
|
export class Reminders extends BaseRepository {
|
||||||
private reminders: Repository<Reminder>;
|
private reminders: Repository<Reminder>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.reminders = getRepository(Reminder);
|
this.reminders = dataSource.getRepository(Reminder);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRemindersDueSoon(threshold: number): Promise<Reminder[]> {
|
async getRemindersDueSoon(threshold: number): Promise<Reminder[]> {
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
|
||||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
import { DBDateFormat } from "../utils";
|
import { DBDateFormat } from "../utils";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||||
|
|
||||||
export class ScheduledPosts extends BaseRepository {
|
export class ScheduledPosts extends BaseRepository {
|
||||||
private scheduledPosts: Repository<ScheduledPost>;
|
private scheduledPosts: Repository<ScheduledPost>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.scheduledPosts = getRepository(ScheduledPost);
|
this.scheduledPosts = dataSource.getRepository(ScheduledPost);
|
||||||
}
|
}
|
||||||
|
|
||||||
getScheduledPostsDueSoon(threshold: number): Promise<ScheduledPost[]> {
|
getScheduledPostsDueSoon(threshold: number): Promise<ScheduledPost[]> {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { Supporter } from "./entities/Supporter";
|
import { Supporter } from "./entities/Supporter";
|
||||||
|
|
||||||
export class Supporters extends BaseRepository {
|
export class Supporters extends BaseRepository {
|
||||||
|
@ -7,7 +8,7 @@ export class Supporters extends BaseRepository {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.supporters = getRepository(Supporter);
|
this.supporters = dataSource.getRepository(Supporter);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll() {
|
getAll() {
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { Tempban } from "./entities/Tempban";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import { DBDateFormat } from "../utils";
|
import { DBDateFormat } from "../utils";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { Tempban } from "./entities/Tempban";
|
||||||
|
|
||||||
export class Tempbans extends BaseRepository {
|
export class Tempbans extends BaseRepository {
|
||||||
private tempbans: Repository<Tempban>;
|
private tempbans: Repository<Tempban>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.tempbans = getRepository(Tempban);
|
this.tempbans = dataSource.getRepository(Tempban);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSoonExpiringTempbans(threshold: number): Promise<Tempban[]> {
|
getSoonExpiringTempbans(threshold: number): Promise<Tempban[]> {
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import { getRepository, In, Repository } from "typeorm";
|
import { In, Repository } from "typeorm";
|
||||||
import { isAPI } from "../globals";
|
import { isAPI } from "../globals";
|
||||||
import { MINUTES, SECONDS } from "../utils";
|
import { MINUTES, SECONDS } from "../utils";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
import { cleanupUsernames } from "./cleanup/usernames";
|
import { cleanupUsernames } from "./cleanup/usernames";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
import { UsernameHistoryEntry } from "./entities/UsernameHistoryEntry";
|
import { UsernameHistoryEntry } from "./entities/UsernameHistoryEntry";
|
||||||
|
|
||||||
if (!isAPI()) {
|
const CLEANUP_INTERVAL = 5 * MINUTES;
|
||||||
const CLEANUP_INTERVAL = 5 * MINUTES;
|
|
||||||
|
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
await cleanupUsernames();
|
await cleanupUsernames();
|
||||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isAPI()) {
|
||||||
// Start first cleanup 30 seconds after startup
|
// Start first cleanup 30 seconds after startup
|
||||||
|
// TODO: Move to bot startup code
|
||||||
setTimeout(cleanup, 30 * SECONDS);
|
setTimeout(cleanup, 30 * SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +26,7 @@ export class UsernameHistory extends BaseRepository {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.usernameHistory = getRepository(UsernameHistoryEntry);
|
this.usernameHistory = dataSource.getRepository(UsernameHistoryEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByUserId(userId): Promise<UsernameHistoryEntry[]> {
|
async getByUserId(userId): Promise<UsernameHistoryEntry[]> {
|
||||||
|
@ -39,7 +41,7 @@ export class UsernameHistory extends BaseRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getLastEntry(userId): Promise<UsernameHistoryEntry | undefined> {
|
getLastEntry(userId): Promise<UsernameHistoryEntry | null> {
|
||||||
return this.usernameHistory.findOne({
|
return this.usernameHistory.findOne({
|
||||||
where: {
|
where: {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
|
||||||
import { VCAlert } from "./entities/VCAlert";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
import { DBDateFormat } from "../utils";
|
import { DBDateFormat } from "../utils";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { VCAlert } from "./entities/VCAlert";
|
||||||
|
|
||||||
export class VCAlerts extends BaseRepository {
|
export class VCAlerts extends BaseRepository {
|
||||||
private allAlerts: Repository<VCAlert>;
|
private allAlerts: Repository<VCAlert>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.allAlerts = getRepository(VCAlert);
|
this.allAlerts = dataSource.getRepository(VCAlert);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSoonExpiringAlerts(threshold: number): Promise<VCAlert[]> {
|
async getSoonExpiringAlerts(threshold: number): Promise<VCAlert[]> {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { Webhook } from "./entities/Webhook";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
import { decrypt, encrypt } from "../utils/crypt";
|
import { decrypt, encrypt } from "../utils/crypt";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { dataSource } from "./dataSource";
|
||||||
|
import { Webhook } from "./entities/Webhook";
|
||||||
|
|
||||||
export class Webhooks extends BaseRepository {
|
export class Webhooks extends BaseRepository {
|
||||||
repository: Repository<Webhook> = getRepository(Webhook);
|
repository: Repository<Webhook> = dataSource.getRepository(Webhook);
|
||||||
|
|
||||||
protected async _processEntityFromDB(entity) {
|
protected async _processEntityFromDB(entity) {
|
||||||
entity.token = await decrypt(entity.token);
|
entity.token = await decrypt(entity.token);
|
||||||
|
|
|
@ -19,8 +19,6 @@ export type RemoveApiPermissionEventData = {
|
||||||
target_id: string;
|
target_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditConfigEventData = {};
|
|
||||||
|
|
||||||
export interface AuditLogEventData extends Record<AuditLogEventType, unknown> {
|
export interface AuditLogEventData extends Record<AuditLogEventType, unknown> {
|
||||||
ADD_API_PERMISSION: {
|
ADD_API_PERMISSION: {
|
||||||
type: ApiPermissionTypes;
|
type: ApiPermissionTypes;
|
||||||
|
@ -41,7 +39,7 @@ export interface AuditLogEventData extends Record<AuditLogEventType, unknown> {
|
||||||
target_id: string;
|
target_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
EDIT_CONFIG: {};
|
EDIT_CONFIG: Record<string, never>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnyAuditLogEventData = AuditLogEventData[AuditLogEventType];
|
export type AnyAuditLogEventData = AuditLogEventData[AuditLogEventType];
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export function buildEntity<T extends any>(Entity: new () => T, data: Partial<T>): T {
|
export function buildEntity<T extends object>(Entity: new () => T, data: Partial<T>): T {
|
||||||
const instance = new Entity();
|
const instance = new Entity();
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
instance[key] = value;
|
instance[key] = value;
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
import { DBDateFormat } from "../../utils";
|
import { DBDateFormat } from "../../utils";
|
||||||
import { connection } from "../db";
|
import { dataSource } from "../dataSource";
|
||||||
import { Config } from "../entities/Config";
|
import { Config } from "../entities/Config";
|
||||||
|
|
||||||
const CLEAN_PER_LOOP = 50;
|
const CLEAN_PER_LOOP = 50;
|
||||||
|
|
||||||
export async function cleanupConfigs() {
|
export async function cleanupConfigs() {
|
||||||
const configRepository = getRepository(Config);
|
const configRepository = dataSource.getRepository(Config);
|
||||||
|
|
||||||
|
// FIXME: The query below doesn't work on MySQL 8.0. Pending an update.
|
||||||
|
return;
|
||||||
|
|
||||||
let cleaned = 0;
|
let cleaned = 0;
|
||||||
let rows;
|
let rows;
|
||||||
|
@ -15,7 +18,7 @@ export async function cleanupConfigs() {
|
||||||
// >1 month old: 1 config retained per month
|
// >1 month old: 1 config retained per month
|
||||||
const oneMonthCutoff = moment.utc().subtract(30, "days").format(DBDateFormat);
|
const oneMonthCutoff = moment.utc().subtract(30, "days").format(DBDateFormat);
|
||||||
do {
|
do {
|
||||||
rows = await connection.query(
|
rows = await dataSource.query(
|
||||||
`
|
`
|
||||||
WITH _configs
|
WITH _configs
|
||||||
AS (
|
AS (
|
||||||
|
@ -53,7 +56,7 @@ export async function cleanupConfigs() {
|
||||||
// >2 weeks old: 1 config retained per day
|
// >2 weeks old: 1 config retained per day
|
||||||
const twoWeekCutoff = moment.utc().subtract(2, "weeks").format(DBDateFormat);
|
const twoWeekCutoff = moment.utc().subtract(2, "weeks").format(DBDateFormat);
|
||||||
do {
|
do {
|
||||||
rows = await connection.query(
|
rows = await dataSource.query(
|
||||||
`
|
`
|
||||||
WITH _configs
|
WITH _configs
|
||||||
AS (
|
AS (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
import { DAYS, DBDateFormat, MINUTES, SECONDS, sleep } from "../../utils";
|
import { DAYS, DBDateFormat, MINUTES, SECONDS, sleep } from "../../utils";
|
||||||
import { connection } from "../db";
|
import { dataSource } from "../dataSource";
|
||||||
import { SavedMessage } from "../entities/SavedMessage";
|
import { SavedMessage } from "../entities/SavedMessage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,7 +16,7 @@ const CLEAN_PER_LOOP = 100;
|
||||||
export async function cleanupMessages(): Promise<number> {
|
export async function cleanupMessages(): Promise<number> {
|
||||||
let cleaned = 0;
|
let cleaned = 0;
|
||||||
|
|
||||||
const messagesRepository = getRepository(SavedMessage);
|
const messagesRepository = dataSource.getRepository(SavedMessage);
|
||||||
|
|
||||||
const deletedAtThreshold = moment.utc().subtract(DELETED_MESSAGE_RETENTION_PERIOD, "ms").format(DBDateFormat);
|
const deletedAtThreshold = moment.utc().subtract(DELETED_MESSAGE_RETENTION_PERIOD, "ms").format(DBDateFormat);
|
||||||
const postedAtThreshold = moment.utc().subtract(RETENTION_PERIOD, "ms").format(DBDateFormat);
|
const postedAtThreshold = moment.utc().subtract(RETENTION_PERIOD, "ms").format(DBDateFormat);
|
||||||
|
@ -27,7 +27,7 @@ export async function cleanupMessages(): Promise<number> {
|
||||||
// when a message was being inserted at the same time
|
// when a message was being inserted at the same time
|
||||||
let ids: string[];
|
let ids: string[];
|
||||||
do {
|
do {
|
||||||
const deletedMessageRows = await connection.query(
|
const deletedMessageRows = await dataSource.query(
|
||||||
`
|
`
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM messages
|
FROM messages
|
||||||
|
@ -40,7 +40,7 @@ export async function cleanupMessages(): Promise<number> {
|
||||||
[deletedAtThreshold],
|
[deletedAtThreshold],
|
||||||
);
|
);
|
||||||
|
|
||||||
const oldPostedRows = await connection.query(
|
const oldPostedRows = await dataSource.query(
|
||||||
`
|
`
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM messages
|
FROM messages
|
||||||
|
@ -53,7 +53,7 @@ export async function cleanupMessages(): Promise<number> {
|
||||||
[postedAtThreshold],
|
[postedAtThreshold],
|
||||||
);
|
);
|
||||||
|
|
||||||
const oldBotPostedRows = await connection.query(
|
const oldBotPostedRows = await dataSource.query(
|
||||||
`
|
`
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM messages
|
FROM messages
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
import { DAYS, DBDateFormat } from "../../utils";
|
import { DAYS, DBDateFormat } from "../../utils";
|
||||||
import { connection } from "../db";
|
import { dataSource } from "../dataSource";
|
||||||
import { NicknameHistoryEntry } from "../entities/NicknameHistoryEntry";
|
import { NicknameHistoryEntry } from "../entities/NicknameHistoryEntry";
|
||||||
|
|
||||||
export const NICKNAME_RETENTION_PERIOD = 30 * DAYS;
|
export const NICKNAME_RETENTION_PERIOD = 30 * DAYS;
|
||||||
|
@ -10,13 +10,13 @@ const CLEAN_PER_LOOP = 500;
|
||||||
export async function cleanupNicknames(): Promise<number> {
|
export async function cleanupNicknames(): Promise<number> {
|
||||||
let cleaned = 0;
|
let cleaned = 0;
|
||||||
|
|
||||||
const nicknameHistoryRepository = getRepository(NicknameHistoryEntry);
|
const nicknameHistoryRepository = dataSource.getRepository(NicknameHistoryEntry);
|
||||||
const dateThreshold = moment.utc().subtract(NICKNAME_RETENTION_PERIOD, "ms").format(DBDateFormat);
|
const dateThreshold = moment.utc().subtract(NICKNAME_RETENTION_PERIOD, "ms").format(DBDateFormat);
|
||||||
|
|
||||||
// Clean old nicknames (NICKNAME_RETENTION_PERIOD)
|
// Clean old nicknames (NICKNAME_RETENTION_PERIOD)
|
||||||
let rows;
|
let rows;
|
||||||
do {
|
do {
|
||||||
rows = await connection.query(
|
rows = await dataSource.query(
|
||||||
`
|
`
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM nickname_history
|
FROM nickname_history
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { getRepository, In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
import { DAYS, DBDateFormat } from "../../utils";
|
import { DAYS, DBDateFormat } from "../../utils";
|
||||||
import { connection } from "../db";
|
import { dataSource } from "../dataSource";
|
||||||
import { UsernameHistoryEntry } from "../entities/UsernameHistoryEntry";
|
import { UsernameHistoryEntry } from "../entities/UsernameHistoryEntry";
|
||||||
|
|
||||||
export const USERNAME_RETENTION_PERIOD = 30 * DAYS;
|
export const USERNAME_RETENTION_PERIOD = 30 * DAYS;
|
||||||
|
@ -10,13 +10,13 @@ const CLEAN_PER_LOOP = 500;
|
||||||
export async function cleanupUsernames(): Promise<number> {
|
export async function cleanupUsernames(): Promise<number> {
|
||||||
let cleaned = 0;
|
let cleaned = 0;
|
||||||
|
|
||||||
const usernameHistoryRepository = getRepository(UsernameHistoryEntry);
|
const usernameHistoryRepository = dataSource.getRepository(UsernameHistoryEntry);
|
||||||
const dateThreshold = moment.utc().subtract(USERNAME_RETENTION_PERIOD, "ms").format(DBDateFormat);
|
const dateThreshold = moment.utc().subtract(USERNAME_RETENTION_PERIOD, "ms").format(DBDateFormat);
|
||||||
|
|
||||||
// Clean old usernames (USERNAME_RETENTION_PERIOD)
|
// Clean old usernames (USERNAME_RETENTION_PERIOD)
|
||||||
let rows;
|
let rows;
|
||||||
do {
|
do {
|
||||||
rows = await connection.query(
|
rows = await dataSource.query(
|
||||||
`
|
`
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM username_history
|
FROM username_history
|
||||||
|
|
45
backend/src/data/dataSource.ts
Normal file
45
backend/src/data/dataSource.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import path from "path";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import { env } from "../env";
|
||||||
|
import { backendDir } from "../paths";
|
||||||
|
|
||||||
|
moment.tz.setDefault("UTC");
|
||||||
|
|
||||||
|
const entities = path.relative(process.cwd(), path.resolve(backendDir, "dist/backend/src/data/entities/*.js"));
|
||||||
|
const migrations = path.relative(process.cwd(), path.resolve(backendDir, "dist/backend/src/migrations/*.js"));
|
||||||
|
|
||||||
|
export const dataSource = new DataSource({
|
||||||
|
type: "mysql",
|
||||||
|
host: env.DB_HOST,
|
||||||
|
port: env.DB_PORT,
|
||||||
|
username: env.DB_USER,
|
||||||
|
password: env.DB_PASSWORD,
|
||||||
|
database: env.DB_DATABASE,
|
||||||
|
charset: "utf8mb4",
|
||||||
|
supportBigNumbers: true,
|
||||||
|
bigNumberStrings: true,
|
||||||
|
dateStrings: true,
|
||||||
|
synchronize: false,
|
||||||
|
connectTimeout: 2000,
|
||||||
|
|
||||||
|
logging: ["error", "warn"],
|
||||||
|
|
||||||
|
// Entities
|
||||||
|
entities: [entities],
|
||||||
|
|
||||||
|
// Pool options
|
||||||
|
extra: {
|
||||||
|
typeCast(field, next) {
|
||||||
|
if (field.type === "DATETIME") {
|
||||||
|
const val = field.string();
|
||||||
|
return val != null ? moment.utc(val).format("YYYY-MM-DD HH:mm:ss") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Migrations
|
||||||
|
migrations: [migrations],
|
||||||
|
});
|
|
@ -1,28 +1,15 @@
|
||||||
import { Connection, createConnection } from "typeorm";
|
|
||||||
import { SimpleError } from "../SimpleError";
|
import { SimpleError } from "../SimpleError";
|
||||||
import connectionOptions from "../../ormconfig";
|
import { dataSource } from "./dataSource";
|
||||||
import { QueryLogger } from "./queryLogger";
|
|
||||||
|
|
||||||
let connectionPromise: Promise<Connection>;
|
let connectionPromise: Promise<void>;
|
||||||
|
|
||||||
export let connection: Connection;
|
|
||||||
|
|
||||||
export function connect() {
|
export function connect() {
|
||||||
if (!connectionPromise) {
|
if (!connectionPromise) {
|
||||||
connectionPromise = createConnection({
|
connectionPromise = dataSource.initialize().then(async (initializedDataSource) => {
|
||||||
...(connectionOptions as any),
|
const tzResult = await initializedDataSource.query("SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP) AS tz");
|
||||||
logging: ["query", "error"],
|
if (tzResult[0].tz !== "00:00:00") {
|
||||||
logger: new QueryLogger(),
|
throw new SimpleError(`Database timezone must be UTC (detected ${tzResult[0].tz})`);
|
||||||
}).then((newConnection) => {
|
|
||||||
// Verify the DB timezone is set to UTC
|
|
||||||
return newConnection.query("SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP) AS tz").then((r) => {
|
|
||||||
if (r[0].tz !== "00:00:00") {
|
|
||||||
throw new SimpleError(`Database timezone must be UTC (detected ${r[0].tz})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connection = newConnection;
|
|
||||||
return newConnection;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
|
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||||
import { ApiUserInfo } from "./ApiUserInfo";
|
|
||||||
import { AuditLogEventData, AuditLogEventType } from "../apiAuditLogTypes";
|
import { AuditLogEventData, AuditLogEventType } from "../apiAuditLogTypes";
|
||||||
|
|
||||||
@Entity("api_audit_log")
|
@Entity("api_audit_log")
|
||||||
|
|
|
@ -19,7 +19,7 @@ export class ApiLogin {
|
||||||
@Column()
|
@Column()
|
||||||
expires_at: string;
|
expires_at: string;
|
||||||
|
|
||||||
@ManyToOne((type) => ApiUserInfo, (userInfo) => userInfo.logins)
|
@ManyToOne(() => ApiUserInfo, (userInfo) => userInfo.logins)
|
||||||
@JoinColumn({ name: "user_id" })
|
@JoinColumn({ name: "user_id" })
|
||||||
userInfo: ApiUserInfo;
|
userInfo: ApiUserInfo;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
|
||||||
import { ApiUserInfo } from "./ApiUserInfo";
|
|
||||||
import { ApiPermissionTypes } from "../ApiPermissionAssignments";
|
import { ApiPermissionTypes } from "../ApiPermissionAssignments";
|
||||||
|
import { ApiUserInfo } from "./ApiUserInfo";
|
||||||
|
|
||||||
@Entity("api_permissions")
|
@Entity("api_permissions")
|
||||||
export class ApiPermissionAssignment {
|
export class ApiPermissionAssignment {
|
||||||
|
@ -22,7 +22,7 @@ export class ApiPermissionAssignment {
|
||||||
@Column({ type: String, nullable: true })
|
@Column({ type: String, nullable: true })
|
||||||
expires_at: string | null;
|
expires_at: string | null;
|
||||||
|
|
||||||
@ManyToOne((type) => ApiUserInfo, (userInfo) => userInfo.permissionAssignments)
|
@ManyToOne(() => ApiUserInfo, (userInfo) => userInfo.permissionAssignments)
|
||||||
@JoinColumn({ name: "target_id" })
|
@JoinColumn({ name: "target_id" })
|
||||||
userInfo: ApiUserInfo;
|
userInfo: ApiUserInfo;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ export class ApiUserInfo {
|
||||||
@Column()
|
@Column()
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|
||||||
@OneToMany((type) => ApiLogin, (login) => login.userInfo)
|
@OneToMany(() => ApiLogin, (login) => login.userInfo)
|
||||||
logins: ApiLogin[];
|
logins: ApiLogin[];
|
||||||
|
|
||||||
@OneToMany((type) => ApiPermissionAssignment, (p) => p.userInfo)
|
@OneToMany(() => ApiPermissionAssignment, (p) => p.userInfo)
|
||||||
permissionAssignments: ApiPermissionAssignment[];
|
permissionAssignments: ApiPermissionAssignment[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,6 @@ export class Case {
|
||||||
*/
|
*/
|
||||||
@Column({ type: String, nullable: true }) log_message_id: string | null;
|
@Column({ type: String, nullable: true }) log_message_id: string | null;
|
||||||
|
|
||||||
@OneToMany((type) => CaseNote, (note) => note.case)
|
@OneToMany(() => CaseNote, (note) => note.case)
|
||||||
notes: CaseNote[];
|
notes: CaseNote[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class CaseNote {
|
||||||
|
|
||||||
@Column() created_at: string;
|
@Column() created_at: string;
|
||||||
|
|
||||||
@ManyToOne((type) => Case, (theCase) => theCase.notes)
|
@ManyToOne(() => Case, (theCase) => theCase.notes)
|
||||||
@JoinColumn({ name: "case_id" })
|
@JoinColumn({ name: "case_id" })
|
||||||
case: Case;
|
case: Case;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export class Config {
|
||||||
@Column()
|
@Column()
|
||||||
edited_at: string;
|
edited_at: string;
|
||||||
|
|
||||||
@ManyToOne((type) => ApiUserInfo)
|
@ManyToOne(() => ApiUserInfo)
|
||||||
@JoinColumn({ name: "edited_by" })
|
@JoinColumn({ name: "edited_by" })
|
||||||
userInfo: ApiUserInfo;
|
userInfo: ApiUserInfo;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
export const TRIGGER_COMPARISON_OPS = ["=", "!=", ">", "<", ">=", "<="] as const;
|
export const TRIGGER_COMPARISON_OPS = ["=", "!=", ">", "<", ">=", "<="] as const;
|
||||||
|
|
||||||
export type TriggerComparisonOp = typeof TRIGGER_COMPARISON_OPS[number];
|
export type TriggerComparisonOp = (typeof TRIGGER_COMPARISON_OPS)[number];
|
||||||
|
|
||||||
const REVERSE_OPS: Record<TriggerComparisonOp, TriggerComparisonOp> = {
|
const REVERSE_OPS: Record<TriggerComparisonOp, TriggerComparisonOp> = {
|
||||||
"=": "!=",
|
"=": "!=",
|
||||||
|
|
20
backend/src/data/entities/MemberCacheItem.ts
Normal file
20
backend/src/data/entities/MemberCacheItem.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
|
@Entity("member_cache")
|
||||||
|
export class MemberCacheItem {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
|
||||||
|
@Column() guild_id: string;
|
||||||
|
|
||||||
|
@Column() user_id: string;
|
||||||
|
|
||||||
|
@Column() username: string;
|
||||||
|
|
||||||
|
@Column({ type: String, nullable: true }) nickname: string | null;
|
||||||
|
|
||||||
|
@Column("simple-json") roles: string[];
|
||||||
|
|
||||||
|
@Column() last_seen: string;
|
||||||
|
|
||||||
|
@Column({ type: String, nullable: true }) delete_at: string | null;
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ export class Mute {
|
||||||
@PrimaryColumn()
|
@PrimaryColumn()
|
||||||
user_id: string;
|
user_id: string;
|
||||||
|
|
||||||
|
@Column() type: number;
|
||||||
|
|
||||||
@Column() created_at: string;
|
@Column() created_at: string;
|
||||||
|
|
||||||
@Column({ type: String, nullable: true }) expires_at: string | null;
|
@Column({ type: String, nullable: true }) expires_at: string | null;
|
||||||
|
@ -17,4 +19,8 @@ export class Mute {
|
||||||
@Column() case_id: number;
|
@Column() case_id: number;
|
||||||
|
|
||||||
@Column("simple-array") roles_to_restore: string[];
|
@Column("simple-array") roles_to_restore: string[];
|
||||||
|
|
||||||
|
@Column({ type: String, nullable: true }) mute_role: string | null;
|
||||||
|
|
||||||
|
@Column({ type: String, nullable: true }) timeout_expires_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,5 +14,5 @@ export class PersistedData {
|
||||||
|
|
||||||
@Column() nickname: string;
|
@Column() nickname: string;
|
||||||
|
|
||||||
@Column() is_voice_muted: number;
|
@Column({ type: "boolean" }) is_voice_muted: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
@Entity("reminders")
|
@Entity("reminders")
|
||||||
export class Reminder {
|
export class Reminder {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue