mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-07-06 18:47:20 +00:00
Merge branch 'master' into reactions-fix
This commit is contained in:
commit
3c6b4976c6
682 changed files with 24588 additions and 35617 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:
|
||||
matrix:
|
||||
node-version: [16.6]
|
||||
node-version: [18.16]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -6,6 +6,9 @@
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.clinic
|
||||
.clinic-bot
|
||||
.clinic-api
|
||||
|
||||
# Runtime data
|
||||
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.
|
||||
|
||||
## Usage documentation
|
||||
For information on how to use the bot, see https://zeppelin.gg/docs
|
||||
|
||||
## 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
|
||||
1. `cd backend`
|
||||
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
|
||||
## Production
|
||||
See [PRODUCTION.md](./PRODUCTION.md) for instructions on how to run the bot in production.
|
||||
|
||||
### Running the API server
|
||||
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
|
||||
```
|
||||
Once you have the environment up and running, see [MANAGEMENT.md](./MANAGEMENT.md) for how to manage your bot.
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
14803
backend/package-lock.json
generated
14803
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\"",
|
||||
"build": "rimraf dist && tsc",
|
||||
"start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js",
|
||||
"start-bot-dev-debug": "NODE_ENV=development DEBUG=true clinic heapprofiler --collect-only --dest .clinic-bot -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9229 dist/backend/src/index.js",
|
||||
"start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js",
|
||||
"start-bot-prod-debug": "NODE_ENV=production DEBUG=true clinic heapprofiler --collect-only --dest .clinic-bot -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/index.js",
|
||||
"watch-bot": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-bot-dev\"",
|
||||
"start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
|
||||
"start-api-dev-debug": "NODE_ENV=development DEBUG=true clinic heapprofiler --collect-only --dest .clinic-api -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
|
||||
"start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js",
|
||||
"start-api-prod-debug": "NODE_ENV=production DEBUG=true clinic heapprofiler --collect-only --dest .clinic-api -- node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --enable-source-maps --stack-trace-limit=30 dist/backend/src/api/index.js",
|
||||
"watch-api": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-api-dev\"",
|
||||
"typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js",
|
||||
"migrate-prod": "npm run typeorm -- migration:run",
|
||||
"migrate-dev": "npm run build && npm run typeorm -- migration:run",
|
||||
"migrate-rollback-prod": "npm run typeorm -- migration:revert",
|
||||
"migrate-rollback-dev": "npm run build && npm run typeorm -- migration:revert",
|
||||
"migrate": "npm run typeorm -- migration:run -d dist/backend/src/data/dataSource.js",
|
||||
"migrate-prod": "cross-env NODE_ENV=production npm run migrate",
|
||||
"migrate-dev": "cross-env NODE_ENV=development npm run build && npm run migrate",
|
||||
"migrate-rollback": "npm run typeorm -- migration:revert -d dist/backend/src/data/dataSource.js",
|
||||
"migrate-rollback-prod": "cross-env NODE_ENV=production npm run migrate",
|
||||
"migrate-rollback-dev": "cross-env NODE_ENV=development npm run build && npm run migrate",
|
||||
"test": "npm run build && npm run run-tests",
|
||||
"run-tests": "ava",
|
||||
"test-watch": "tsc-watch --onSuccess \"npx ava\""
|
||||
|
@ -25,21 +31,21 @@
|
|||
"dependencies": {
|
||||
"@silvia-odwyer/photon-node": "^0.3.1",
|
||||
"bufferutil": "^4.0.3",
|
||||
"clinic": "^13.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^5.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"deep-diff": "^1.0.2",
|
||||
"discord-api-types": "^0.22.0",
|
||||
"discord.js": "^13.3.1",
|
||||
"discord.js": "^14.11.0",
|
||||
"dotenv": "^4.0.0",
|
||||
"emoji-regex": "^8.0.0",
|
||||
"erlpack": "github:almeidx/erlpack#f0c535f73817fd914806d6ca26a7730c14e0fb7c",
|
||||
"erlpack": "github:discord/erlpack",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"express": "^4.17.0",
|
||||
"fp-ts": "^2.0.1",
|
||||
"humanize-duration": "^3.15.0",
|
||||
"io-ts": "^2.0.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"knub": "^30.0.0-beta.46",
|
||||
"knub": "^32.0.0-next.16",
|
||||
"knub-command-manager": "^9.1.0",
|
||||
"last-commit-log": "^2.1.0",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
|
@ -49,13 +55,12 @@
|
|||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"moment-timezone": "^0.5.21",
|
||||
"multer": "^1.4.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql": "^2.16.0",
|
||||
"node-fetch": "^2.6.5",
|
||||
"parse-color": "^1.0.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-custom": "^1.0.5",
|
||||
"passport-oauth2": "^1.5.0",
|
||||
"passport-oauth2": "^1.6.1",
|
||||
"pkg-up": "^3.1.0",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"regexp-worker": "^1.1.0",
|
||||
|
@ -67,9 +72,9 @@
|
|||
"tmp": "0.0.33",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"twemoji": "^12.1.4",
|
||||
"typeorm": "^0.2.31",
|
||||
"typeorm": "^0.3.17",
|
||||
"utf-8-validate": "^5.0.5",
|
||||
"uuid": "^3.3.2",
|
||||
"uuid": "^9.0.0",
|
||||
"yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build",
|
||||
"zlib-sync": "^0.1.7",
|
||||
"zod": "^3.7.2"
|
||||
|
@ -82,18 +87,17 @@
|
|||
"@types/lodash.at": "^4.6.3",
|
||||
"@types/moment-timezone": "^0.5.6",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^14.0.14",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/node": "^18.16.3",
|
||||
"@types/passport": "^1.0.0",
|
||||
"@types/passport-oauth2": "^1.4.8",
|
||||
"@types/passport-strategy": "^0.2.35",
|
||||
"@types/safe-regex": "^1.1.2",
|
||||
"@types/tmp": "0.0.33",
|
||||
"@types/twemoji": "^12.1.0",
|
||||
"ava": "^3.10.0",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"ava": "^5.3.1",
|
||||
"rimraf": "^2.6.2",
|
||||
"source-map-support": "^0.5.16",
|
||||
"tsc-watch": "^4.0.0"
|
||||
"source-map-support": "^0.5.16"
|
||||
},
|
||||
"ava": {
|
||||
"files": [
|
||||
|
|
40
backend/src/Blocker.ts
Normal file
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();
|
||||
}
|
||||
}
|
|
@ -9,6 +9,8 @@ export enum ERRORS {
|
|||
INVALID_USER,
|
||||
INVALID_MUTE_ROLE_ID,
|
||||
MUTE_ROLE_ABOVE_ZEP,
|
||||
USER_ABOVE_ZEP,
|
||||
USER_NOT_MODERATABLE,
|
||||
}
|
||||
|
||||
export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
|
||||
|
@ -20,6 +22,8 @@ export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
|
|||
[ERRORS.INVALID_USER]: "Invalid user",
|
||||
[ERRORS.INVALID_MUTE_ROLE_ID]: "Specified mute role is not valid",
|
||||
[ERRORS.MUTE_ROLE_ABOVE_ZEP]: "Specified mute role is above Zeppelin in the role hierarchy",
|
||||
[ERRORS.USER_ABOVE_ZEP]: "Cannot mute user, specified user is above Zeppelin in the role hierarchy",
|
||||
[ERRORS.USER_NOT_MODERATABLE]: "Cannot mute user, specified user is not moderatable",
|
||||
};
|
||||
|
||||
export class RecoverablePluginError extends Error {
|
||||
|
|
|
@ -7,7 +7,7 @@ export class SimpleError extends Error {
|
|||
super(message);
|
||||
}
|
||||
|
||||
[util.inspect.custom](depth, options) {
|
||||
[util.inspect.custom]() {
|
||||
return `Error: ${this.message}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ApiLogins } from "../data/ApiLogins";
|
|||
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
||||
import { ApiUserInfo } from "../data/ApiUserInfo";
|
||||
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
||||
import { env } from "../env";
|
||||
import { ok } from "./responses";
|
||||
|
||||
interface IPassportApiUser {
|
||||
|
@ -17,7 +18,6 @@ interface IPassportApiUser {
|
|||
|
||||
declare global {
|
||||
namespace Express {
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
interface User extends IPassportApiUser {}
|
||||
}
|
||||
}
|
||||
|
@ -54,24 +54,8 @@ function simpleDiscordAPIRequest(bearerToken, path): Promise<any> {
|
|||
export function initAuth(app: express.Express) {
|
||||
app.use(passport.initialize());
|
||||
|
||||
if (!process.env.CLIENT_ID) {
|
||||
throw new Error("Auth: CLIENT ID missing");
|
||||
}
|
||||
|
||||
if (!process.env.CLIENT_SECRET) {
|
||||
throw new Error("Auth: CLIENT SECRET missing");
|
||||
}
|
||||
|
||||
if (!process.env.OAUTH_CALLBACK_URL) {
|
||||
throw new Error("Auth: OAUTH CALLBACK URL missing");
|
||||
}
|
||||
|
||||
if (!process.env.DASHBOARD_URL) {
|
||||
throw new Error("DASHBOARD_URL missing!");
|
||||
}
|
||||
|
||||
passport.serializeUser((user, done) => done(null, user));
|
||||
passport.deserializeUser((user, done) => done(null, user));
|
||||
passport.deserializeUser((user, done) => done(null, user as IPassportApiUser));
|
||||
|
||||
const apiLogins = new ApiLogins();
|
||||
const apiUserInfo = new ApiUserInfo();
|
||||
|
@ -101,9 +85,9 @@ export function initAuth(app: express.Express) {
|
|||
{
|
||||
authorizationURL: "https://discord.com/api/oauth2/authorize",
|
||||
tokenURL: "https://discord.com/api/oauth2/token",
|
||||
clientID: process.env.CLIENT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
callbackURL: process.env.OAUTH_CALLBACK_URL,
|
||||
clientID: env.CLIENT_ID,
|
||||
clientSecret: env.CLIENT_SECRET,
|
||||
callbackURL: `${env.API_URL}/auth/oauth-callback`,
|
||||
scope: ["identify"],
|
||||
},
|
||||
async (accessToken, refreshToken, profile, cb) => {
|
||||
|
@ -132,9 +116,9 @@ export function initAuth(app: express.Express) {
|
|||
passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
|
||||
(req: Request, res: Response) => {
|
||||
if (req.user && req.user.apiKey) {
|
||||
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
|
||||
res.redirect(`${env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
|
||||
} else {
|
||||
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?error=noAccess`);
|
||||
res.redirect(`${env.DASHBOARD_URL}/login-callback/?error=noAccess`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -165,7 +149,8 @@ export function initAuth(app: express.Express) {
|
|||
|
||||
export function apiTokenAuthHandlers() {
|
||||
return [
|
||||
passport.authenticate("api-token", { failWithError: true }),
|
||||
passport.authenticate("api-token", { failWithError: true, session: false }),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
(err, req: Request, res: Response, next) => {
|
||||
return res.status(401).json({ error: err.message });
|
||||
},
|
||||
|
|
|
@ -54,9 +54,10 @@ export function initDocs(app: express.Express) {
|
|||
}
|
||||
|
||||
const name = plugin.name;
|
||||
const info = plugin.info || {};
|
||||
const info = { ...(plugin.info || {}) };
|
||||
delete info.configSchema;
|
||||
|
||||
const commands = (plugin.commands || []).map((cmd) => ({
|
||||
const messageCommands = (plugin.messageCommands || []).map((cmd) => ({
|
||||
trigger: cmd.trigger,
|
||||
permission: cmd.permission,
|
||||
signature: cmd.signature,
|
||||
|
@ -66,14 +67,14 @@ export function initDocs(app: express.Express) {
|
|||
}));
|
||||
|
||||
const defaultOptions = plugin.defaultOptions || {};
|
||||
const configSchema = plugin.configSchema && formatConfigSchema(plugin.configSchema);
|
||||
const configSchema = plugin.info?.configSchema && formatConfigSchema(plugin.info.configSchema);
|
||||
|
||||
res.json({
|
||||
name,
|
||||
info,
|
||||
configSchema,
|
||||
defaultOptions,
|
||||
commands,
|
||||
messageCommands,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { ApiPermissions } from "@shared/apiPermissions";
|
||||
import express, { Request, Response } from "express";
|
||||
import { YAMLException } from "js-yaml";
|
||||
import moment from "moment-timezone";
|
||||
import { Queue } from "../Queue";
|
||||
import { validateGuildConfig } from "../configValidator";
|
||||
import { AllowedGuilds } from "../data/AllowedGuilds";
|
||||
import { ApiAuditLog } from "../data/ApiAuditLog";
|
||||
import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments";
|
||||
import { Configs } from "../data/Configs";
|
||||
import { AuditLogEventTypes } from "../data/apiAuditLogTypes";
|
||||
import { isSnowflake } from "../utils";
|
||||
import { loadYamlSafely } from "../utils/loadYamlSafely";
|
||||
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
|
||||
import { apiTokenAuthHandlers } from "./auth";
|
||||
import { hasGuildPermission, requireGuildPermission } from "./permissions";
|
||||
import { clientError, ok, serverError, unauthorized } from "./responses";
|
||||
import { loadYamlSafely } from "../utils/loadYamlSafely";
|
||||
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
|
||||
import { isSnowflake } from "../utils";
|
||||
import moment from "moment-timezone";
|
||||
import { ApiAuditLog } from "../data/ApiAuditLog";
|
||||
import { AuditLogEventTypes } from "../data/apiAuditLogTypes";
|
||||
import { Queue } from "../Queue";
|
||||
|
||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||
const auditLog = new ApiAuditLog();
|
||||
|
@ -126,7 +126,7 @@ export function initGuildsAPI(app: express.Express) {
|
|||
if (type !== ApiPermissionTypes.User) {
|
||||
return clientError(res, "Invalid type");
|
||||
}
|
||||
if (!isSnowflake(targetId)) {
|
||||
if (!isSnowflake(targetId) || targetId === req.user!.userId) {
|
||||
return clientError(res, "Invalid targetId");
|
||||
}
|
||||
const validPermissions = new Set(Object.values(ApiPermissions));
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { ApiPermissions } from "@shared/apiPermissions";
|
||||
import express, { Request, Response } from "express";
|
||||
import { requireGuildPermission } from "../permissions";
|
||||
import { clientError, ok } from "../responses";
|
||||
import { GuildCases } from "../../data/GuildCases";
|
||||
import { z } from "zod";
|
||||
import { Case } from "../../data/entities/Case";
|
||||
import { rateLimit } from "../rateLimits";
|
||||
import { MINUTES } from "../../utils";
|
||||
import moment from "moment-timezone";
|
||||
import { z } from "zod";
|
||||
import { GuildCases } from "../../data/GuildCases";
|
||||
import { Case } from "../../data/entities/Case";
|
||||
import { MINUTES } from "../../utils";
|
||||
import { requireGuildPermission } from "../permissions";
|
||||
import { rateLimit } from "../rateLimits";
|
||||
import { clientError, ok } from "../responses";
|
||||
|
||||
const caseHandlingModeSchema = z.union([
|
||||
z.literal("replace"),
|
||||
|
@ -50,7 +50,7 @@ export function initGuildsImportExportAPI(guildRouter: express.Router) {
|
|||
importExportRouter.get(
|
||||
"/:guildId/pre-import",
|
||||
requireGuildPermission(ApiPermissions.ManageAccess),
|
||||
async (req: Request, res: Response) => {
|
||||
async (req: Request) => {
|
||||
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
||||
const minNum = await guildCases.getMinCaseNumber();
|
||||
const maxNum = await guildCases.getMaxCaseNumber();
|
||||
|
@ -75,7 +75,10 @@ export function initGuildsImportExportAPI(guildRouter: express.Router) {
|
|||
try {
|
||||
data = importExportData.parse(req.body.data);
|
||||
} 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;
|
||||
}
|
||||
|
||||
|
@ -87,6 +90,14 @@ export function initGuildsImportExportAPI(guildRouter: express.Router) {
|
|||
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);
|
||||
|
||||
// Prepare cases
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import express from "express";
|
||||
import { apiTokenAuthHandlers } from "../auth";
|
||||
import { initGuildsMiscAPI } from "./misc";
|
||||
import { initGuildsImportExportAPI } from "./importExport";
|
||||
import { initGuildsMiscAPI } from "./misc";
|
||||
|
||||
export function initGuildsAPI(app: express.Express) {
|
||||
const guildRouter = express.Router();
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
import { ApiPermissions } from "@shared/apiPermissions";
|
||||
import express, { Request, Response } from "express";
|
||||
import { YAMLException } from "js-yaml";
|
||||
import moment from "moment-timezone";
|
||||
import { Queue } from "../../Queue";
|
||||
import { validateGuildConfig } from "../../configValidator";
|
||||
import { AllowedGuilds } from "../../data/AllowedGuilds";
|
||||
import { ApiAuditLog } from "../../data/ApiAuditLog";
|
||||
import { ApiPermissionAssignments, ApiPermissionTypes } from "../../data/ApiPermissionAssignments";
|
||||
import { Configs } from "../../data/Configs";
|
||||
import { apiTokenAuthHandlers } from "../auth";
|
||||
import { hasGuildPermission, requireGuildPermission } from "../permissions";
|
||||
import { clientError, ok, serverError, unauthorized } from "../responses";
|
||||
import { AuditLogEventTypes } from "../../data/apiAuditLogTypes";
|
||||
import { isSnowflake } from "../../utils";
|
||||
import { loadYamlSafely } from "../../utils/loadYamlSafely";
|
||||
import { ObjectAliasError } from "../../utils/validateNoObjectAliases";
|
||||
import { isSnowflake } from "../../utils";
|
||||
import moment from "moment-timezone";
|
||||
import { ApiAuditLog } from "../../data/ApiAuditLog";
|
||||
import { AuditLogEventTypes } from "../../data/apiAuditLogTypes";
|
||||
import { Queue } from "../../Queue";
|
||||
import { GuildCases } from "../../data/GuildCases";
|
||||
import { z } from "zod";
|
||||
import { hasGuildPermission, requireGuildPermission } from "../permissions";
|
||||
import { clientError, ok, serverError, unauthorized } from "../responses";
|
||||
|
||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||
const auditLog = new ApiAuditLog();
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { connect } from "../data/db";
|
||||
import { setIsAPI } from "../globals";
|
||||
import "./loadEnv";
|
||||
// KEEP THIS AS FIRST IMPORT
|
||||
// See comment in module for details
|
||||
import "../threadsSignalFix";
|
||||
|
||||
if (!process.env.KEY) {
|
||||
import { connect } from "../data/db";
|
||||
import { env } from "../env";
|
||||
import { setIsAPI } from "../globals";
|
||||
|
||||
if (!env.KEY) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error("Project root .env with KEY is required!");
|
||||
process.exit(1);
|
||||
|
@ -20,5 +24,5 @@ setIsAPI(true);
|
|||
// Connect to the database before loading the rest of the code (that depend on the database connection)
|
||||
console.log("Connecting to database..."); // tslint:disable-line
|
||||
connect().then(() => {
|
||||
import("./start");
|
||||
import("./start.js");
|
||||
});
|
||||
|
|
|
@ -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" });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import cors from "cors";
|
||||
import express from "express";
|
||||
import multer from "multer";
|
||||
import { TokenError } from "passport-oauth2";
|
||||
import { env } from "../env";
|
||||
import { initArchives } from "./archives";
|
||||
import { initAuth } from "./auth";
|
||||
import { initDocs } from "./docs";
|
||||
import { initGuildsAPI } from "./guilds/index";
|
||||
import { clientError, error, notFound } from "./responses";
|
||||
import { startBackgroundTasks } from "./tasks";
|
||||
import multer from "multer";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: process.env.DASHBOARD_URL,
|
||||
origin: env.DASHBOARD_URL,
|
||||
}),
|
||||
);
|
||||
app.use(
|
||||
|
@ -34,6 +35,7 @@ app.get("/", (req, res) => {
|
|||
});
|
||||
|
||||
// Error response
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
app.use((err, req, res, next) => {
|
||||
if (err instanceof TokenError) {
|
||||
clientError(res, "Invalid code");
|
||||
|
@ -44,11 +46,12 @@ app.use((err, req, res, next) => {
|
|||
});
|
||||
|
||||
// 404 response
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
app.use((req, res, next) => {
|
||||
return notFound(res);
|
||||
});
|
||||
|
||||
const port = (process.env.PORT && parseInt(process.env.PORT, 10)) || 3000;
|
||||
const port = env.API_PORT;
|
||||
app.listen(port, "0.0.0.0", () => console.log(`API server listening on port ${port}`)); // tslint:disable-line
|
||||
|
||||
startBackgroundTasks();
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
import { GuildChannel, GuildMember, Snowflake, Util, User } from "discord.js";
|
||||
import { baseCommandParameterTypeHelpers, baseTypeConverters, CommandContext, TypeConversionError } from "knub";
|
||||
import {
|
||||
escapeCodeBlock,
|
||||
escapeInlineCode,
|
||||
GuildChannel,
|
||||
GuildMember,
|
||||
GuildTextBasedChannel,
|
||||
Snowflake,
|
||||
User,
|
||||
} from "discord.js";
|
||||
import {
|
||||
baseCommandParameterTypeHelpers,
|
||||
CommandContext,
|
||||
messageCommandBaseTypeConverters,
|
||||
TypeConversionError,
|
||||
} from "knub";
|
||||
import { createTypeHelper } from "knub-command-manager";
|
||||
import {
|
||||
channelMentionRegex,
|
||||
|
@ -16,7 +29,7 @@ import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarge
|
|||
import { inputPatternToRegExp } from "./validatorUtils";
|
||||
|
||||
export const commandTypes = {
|
||||
...baseTypeConverters,
|
||||
...messageCommandBaseTypeConverters,
|
||||
|
||||
delay(value) {
|
||||
const result = convertDelayStringToMS(value);
|
||||
|
@ -30,7 +43,7 @@ export const commandTypes = {
|
|||
async resolvedUser(value, context: CommandContext<any>) {
|
||||
const result = await resolveUser(context.pluginData.client, value);
|
||||
if (result == null || result instanceof UnknownUser) {
|
||||
throw new TypeConversionError(`User \`${Util.escapeCodeBlock(value)}\` was not found`);
|
||||
throw new TypeConversionError(`User \`${escapeCodeBlock(value)}\` was not found`);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
@ -38,7 +51,7 @@ export const commandTypes = {
|
|||
async resolvedUserLoose(value, context: CommandContext<any>) {
|
||||
const result = await resolveUser(context.pluginData.client, value);
|
||||
if (result == null) {
|
||||
throw new TypeConversionError(`Invalid user: \`${Util.escapeCodeBlock(value)}\``);
|
||||
throw new TypeConversionError(`Invalid user: \`${escapeCodeBlock(value)}\``);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
@ -50,9 +63,7 @@ export const commandTypes = {
|
|||
|
||||
const result = await resolveMember(context.pluginData.client, context.message.channel.guild, value);
|
||||
if (result == null) {
|
||||
throw new TypeConversionError(
|
||||
`Member \`${Util.escapeCodeBlock(value)}\` was not found or they have left the server`,
|
||||
);
|
||||
throw new TypeConversionError(`Member \`${escapeCodeBlock(value)}\` was not found or they have left the server`);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
@ -62,7 +73,7 @@ export const commandTypes = {
|
|||
|
||||
const result = await resolveMessageTarget(context.pluginData, value);
|
||||
if (!result) {
|
||||
throw new TypeConversionError(`Unknown message \`${Util.escapeInlineCode(value)}\``);
|
||||
throw new TypeConversionError(`Unknown message \`${escapeInlineCode(value)}\``);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -82,24 +93,28 @@ export const commandTypes = {
|
|||
return value as Snowflake;
|
||||
}
|
||||
|
||||
throw new TypeConversionError(`Could not parse ID: \`${Util.escapeInlineCode(value)}\``);
|
||||
throw new TypeConversionError(`Could not parse ID: \`${escapeInlineCode(value)}\``);
|
||||
},
|
||||
|
||||
regex(value: string, context: CommandContext<any>): RegExp {
|
||||
regex(value: string): RegExp {
|
||||
try {
|
||||
return inputPatternToRegExp(value);
|
||||
} catch (e) {
|
||||
throw new TypeConversionError(`Could not parse RegExp: \`${Util.escapeInlineCode(e.message)}\``);
|
||||
throw new TypeConversionError(`Could not parse RegExp: \`${escapeInlineCode(e.message)}\``);
|
||||
}
|
||||
},
|
||||
|
||||
timezone(value: string) {
|
||||
if (!isValidTimezone(value)) {
|
||||
throw new TypeConversionError(`Invalid timezone: ${Util.escapeInlineCode(value)}`);
|
||||
throw new TypeConversionError(`Invalid timezone: ${escapeInlineCode(value)}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
guildTextBasedChannel(value: string, context: CommandContext<any>) {
|
||||
return messageCommandBaseTypeConverters.textChannel(value, context);
|
||||
},
|
||||
};
|
||||
|
||||
export const commandTypeHelpers = {
|
||||
|
@ -113,4 +128,5 @@ export const commandTypeHelpers = {
|
|||
anyId: createTypeHelper<Promise<Snowflake>>(commandTypes.anyId),
|
||||
regex: createTypeHelper<RegExp>(commandTypes.regex),
|
||||
timezone: createTypeHelper<string>(commandTypes.timezone),
|
||||
guildTextBasedChannel: createTypeHelper<GuildTextBasedChannel>(commandTypes.guildTextBasedChannel),
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { configUtils, ConfigValidationError, PluginOptions } from "knub";
|
||||
import { ConfigValidationError, PluginConfigManager } from "knub";
|
||||
import moment from "moment-timezone";
|
||||
import { guildPlugins } from "./plugins/availablePlugins";
|
||||
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
||||
import { guildPlugins } from "./plugins/availablePlugins";
|
||||
import { PartialZeppelinGuildConfigSchema, ZeppelinGuildConfig } from "./types";
|
||||
import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils";
|
||||
import { StrictValidationError, decodeAndValidateStrict } from "./validatorUtils";
|
||||
|
||||
const pluginNameToPlugin = new Map<string, ZeppelinPlugin>();
|
||||
for (const plugin of guildPlugins) {
|
||||
|
@ -34,9 +34,12 @@ export async function validateGuildConfig(config: any): Promise<string | null> {
|
|||
}
|
||||
|
||||
const plugin = pluginNameToPlugin.get(pluginName)!;
|
||||
const configManager = new PluginConfigManager(plugin.defaultOptions || { config: {} }, pluginOptions, {
|
||||
levels: {},
|
||||
parser: plugin.configParser,
|
||||
});
|
||||
try {
|
||||
const mergedOptions = configUtils.mergeConfig(plugin.defaultOptions || {}, pluginOptions);
|
||||
await plugin.configPreprocessor?.(mergedOptions as unknown as PluginOptions<any>, true);
|
||||
await configManager.init();
|
||||
} catch (err) {
|
||||
if (err instanceof ConfigValidationError || err instanceof StrictValidationError) {
|
||||
return `${pluginName}: ${err.message}`;
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import moment from "moment-timezone";
|
||||
import { Repository } from "typeorm";
|
||||
import { DBDateFormat } from "../utils";
|
||||
import { ApiPermissionTypes } from "./ApiPermissionAssignments";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { AllowedGuild } from "./entities/AllowedGuild";
|
||||
import moment from "moment-timezone";
|
||||
import { DBDateFormat } from "../utils";
|
||||
|
||||
export class AllowedGuilds extends BaseRepository {
|
||||
private allowedGuilds: Repository<AllowedGuild>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.allowedGuilds = getRepository(AllowedGuild);
|
||||
this.allowedGuilds = dataSource.getRepository(AllowedGuild);
|
||||
}
|
||||
|
||||
async isAllowed(guildId) {
|
||||
async isAllowed(guildId: string) {
|
||||
const count = await this.allowedGuilds.count({
|
||||
where: {
|
||||
id: guildId,
|
||||
|
@ -22,11 +23,15 @@ export class AllowedGuilds extends BaseRepository {
|
|||
return count !== 0;
|
||||
}
|
||||
|
||||
find(guildId) {
|
||||
return this.allowedGuilds.findOne(guildId);
|
||||
find(guildId: string) {
|
||||
return this.allowedGuilds.findOne({
|
||||
where: {
|
||||
id: guildId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getForApiUser(userId) {
|
||||
getForApiUser(userId: string) {
|
||||
return this.allowedGuilds
|
||||
.createQueryBuilder("allowed_guilds")
|
||||
.innerJoin(
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { Repository } from "typeorm/index";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { getRepository, Repository } from "typeorm/index";
|
||||
import { ApiAuditLogEntry } from "./entities/ApiAuditLogEntry";
|
||||
import { ApiLogin } from "./entities/ApiLogin";
|
||||
import { AuditLogEventData, AuditLogEventType } from "./apiAuditLogTypes";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ApiAuditLogEntry } from "./entities/ApiAuditLogEntry";
|
||||
|
||||
export class ApiAuditLog extends BaseRepository {
|
||||
private auditLog: Repository<ApiAuditLogEntry<any>>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.auditLog = getRepository(ApiAuditLogEntry);
|
||||
this.auditLog = dataSource.getRepository(ApiAuditLogEntry);
|
||||
}
|
||||
|
||||
addEntry<TEventType extends AuditLogEventType>(
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import crypto from "crypto";
|
||||
import moment from "moment-timezone";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
// tslint:disable-next-line:no-submodule-imports
|
||||
import uuidv4 from "uuid/v4";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { DAYS, DBDateFormat } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ApiLogin } from "./entities/ApiLogin";
|
||||
|
||||
const LOGIN_EXPIRY_TIME = 1 * DAYS;
|
||||
|
@ -14,7 +15,7 @@ export class ApiLogins extends BaseRepository {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
this.apiLogins = getRepository(ApiLogin);
|
||||
this.apiLogins = dataSource.getRepository(ApiLogin);
|
||||
}
|
||||
|
||||
async getUserIdByApiKey(apiKey: string): Promise<string | null> {
|
||||
|
@ -90,10 +91,15 @@ export class ApiLogins extends BaseRepository {
|
|||
const [loginId, token] = apiKey.split(".");
|
||||
if (!loginId || !token) return;
|
||||
|
||||
const updatedTime = moment().utc().add(LOGIN_EXPIRY_TIME, "ms");
|
||||
|
||||
const login = await this.apiLogins.createQueryBuilder().where("id = :id", { id: loginId }).getOne();
|
||||
if (!login || moment.utc(login.expires_at).isSameOrAfter(updatedTime)) return;
|
||||
|
||||
await this.apiLogins.update(
|
||||
{ id: loginId },
|
||||
{
|
||||
expires_at: moment().utc().add(LOGIN_EXPIRY_TIME, "ms").format(DBDateFormat),
|
||||
expires_at: updatedTime.format(DBDateFormat),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { ApiPermissions } from "@shared/apiPermissions";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
|
||||
import { Permissions } from "discord.js";
|
||||
import { Repository } from "typeorm";
|
||||
import { ApiAuditLog } from "./ApiAuditLog";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { AuditLogEventTypes } from "./apiAuditLogTypes";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
|
||||
|
||||
export enum ApiPermissionTypes {
|
||||
User = "USER",
|
||||
|
@ -17,7 +17,7 @@ export class ApiPermissionAssignments extends BaseRepository {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
this.apiPermissions = getRepository(ApiPermissionAssignment);
|
||||
this.apiPermissions = dataSource.getRepository(ApiPermissionAssignment);
|
||||
this.auditLogs = new ApiAuditLog();
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,8 @@ export class ApiPermissionAssignments extends BaseRepository {
|
|||
.createQueryBuilder()
|
||||
.where("expires_at IS NOT NULL")
|
||||
.andWhere("expires_at <= NOW()")
|
||||
.delete();
|
||||
.delete()
|
||||
.execute();
|
||||
}
|
||||
|
||||
async applyOwnerChange(guildId: string, newOwnerId: string) {
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import moment from "moment-timezone";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { DBDateFormat } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { connection } from "./db";
|
||||
import { ApiUserInfo as ApiUserInfoEntity, ApiUserInfoData } from "./entities/ApiUserInfo";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ApiUserInfoData, ApiUserInfo as ApiUserInfoEntity } from "./entities/ApiUserInfo";
|
||||
|
||||
export class ApiUserInfo extends BaseRepository {
|
||||
private apiUserInfo: Repository<ApiUserInfoEntity>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.apiUserInfo = getRepository(ApiUserInfoEntity);
|
||||
this.apiUserInfo = dataSource.getRepository(ApiUserInfoEntity);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
|
@ -22,7 +22,7 @@ export class ApiUserInfo extends BaseRepository {
|
|||
}
|
||||
|
||||
update(id, data: ApiUserInfoData) {
|
||||
return connection.transaction(async (entityManager) => {
|
||||
return dataSource.transaction(async (entityManager) => {
|
||||
const repo = entityManager.getRepository(ApiUserInfoEntity);
|
||||
|
||||
const existingInfo = await repo.findOne({ where: { id } });
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
||||
|
||||
export class Archives extends BaseRepository {
|
||||
protected archives: Repository<ArchiveEntry>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.archives = getRepository(ArchiveEntry);
|
||||
this.archives = dataSource.getRepository(ArchiveEntry);
|
||||
}
|
||||
|
||||
public deleteExpiredArchives() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { BaseRepository } from "./BaseRepository";
|
||||
|
||||
export class BaseGuildRepository<TEntity extends unknown = unknown> extends BaseRepository<TEntity> {
|
||||
export class BaseGuildRepository<TEntity = unknown> extends BaseRepository<TEntity> {
|
||||
private static guildInstances: Map<string, any>;
|
||||
|
||||
protected guildId: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { asyncMap } from "../utils/async";
|
||||
|
||||
export class BaseRepository<TEntity extends unknown = unknown> {
|
||||
export class BaseRepository<TEntity = unknown> {
|
||||
private nextRelations: string[];
|
||||
|
||||
constructor() {
|
||||
|
@ -40,7 +40,7 @@ export class BaseRepository<TEntity extends unknown = unknown> {
|
|||
return entity;
|
||||
}
|
||||
|
||||
protected async processEntityFromDB<T extends TEntity | undefined>(entity: T): Promise<T> {
|
||||
protected async processEntityFromDB<T extends TEntity | null>(entity: T): Promise<T> {
|
||||
return this._processEntityFromDB(entity);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { isAPI } from "../globals";
|
||||
import { HOURS, SECONDS } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { cleanupConfigs } from "./cleanup/configs";
|
||||
import { connection } from "./db";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Config } from "./entities/Config";
|
||||
|
||||
const CLEANUP_INTERVAL = 1 * HOURS;
|
||||
|
||||
async function cleanup() {
|
||||
await cleanupConfigs();
|
||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
if (isAPI()) {
|
||||
const CLEANUP_INTERVAL = 1 * HOURS;
|
||||
|
||||
async function cleanup() {
|
||||
await cleanupConfigs();
|
||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
// Start first cleanup 30 seconds after startup
|
||||
// TODO: Move to bot startup code
|
||||
setTimeout(cleanup, 30 * SECONDS);
|
||||
}
|
||||
|
||||
|
@ -23,7 +24,7 @@ export class Configs extends BaseRepository {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
this.configs = getRepository(Config);
|
||||
this.configs = dataSource.getRepository(Config);
|
||||
}
|
||||
|
||||
getActiveByKey(key) {
|
||||
|
@ -36,7 +37,7 @@ export class Configs extends BaseRepository {
|
|||
}
|
||||
|
||||
async getHighestId(): Promise<number> {
|
||||
const rows = await connection.query("SELECT MAX(id) AS highest_id FROM configs");
|
||||
const rows = await dataSource.query("SELECT MAX(id) AS highest_id FROM configs");
|
||||
return (rows.length && rows[0].highest_id) || 0;
|
||||
}
|
||||
|
||||
|
@ -61,7 +62,7 @@ export class Configs extends BaseRepository {
|
|||
}
|
||||
|
||||
async saveNewRevision(key, config, editedBy) {
|
||||
return connection.transaction(async (entityManager) => {
|
||||
return dataSource.transaction(async (entityManager) => {
|
||||
const repo = entityManager.getRepository(Config);
|
||||
// Mark all old revisions inactive
|
||||
await repo.update({ key }, { is_active: false });
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
"MEMBER_UNBAN": "{timestamp} 🔓 User (`{userId}`) was unbanned by {userMention(mod)}",
|
||||
"MEMBER_FORCEBAN": "{timestamp} 🔨 User (`{userId}`) was forcebanned by {userMention(mod)}",
|
||||
"MEMBER_SOFTBAN": "{timestamp} 🔨 {userMention(member)} was softbanned by {userMention(mod)}",
|
||||
"MEMBER_JOIN": "{timestamp} 📥 {new} {userMention(member)} joined (created {account_age} ago)",
|
||||
"MEMBER_JOIN": "{timestamp} 📥 {new} {userMention(member)} joined (created <t:{account_age_ts}:R>)",
|
||||
"MEMBER_LEAVE": "{timestamp} 📤 {userMention(member)} left the server",
|
||||
"MEMBER_ROLE_ADD": "{timestamp} 🔑 {userMention(member)} received roles: **{roles}**",
|
||||
"MEMBER_ROLE_REMOVE": "{timestamp} 🔑 {userMention(member)} lost roles: **{roles}**",
|
||||
"MEMBER_ROLE_ADD": "{timestamp} 🔑 {userMention(mod)} added roles for {userMention(member)}: **{roles}**",
|
||||
"MEMBER_ROLE_REMOVE": "{timestamp} 🔑 {userMention(mod)} removed roles from {userMention(member)}: **{roles}**",
|
||||
"MEMBER_ROLE_CHANGES": "{timestamp} 🔑 {userMention(member)} had role changes: received **{addedRoles}**, lost **{removedRoles}**",
|
||||
"MEMBER_NICK_CHANGE": "{timestamp} ✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**",
|
||||
"MEMBER_USERNAME_CHANGE": "{timestamp} ✏ {userMention(user)}: username changed from **{oldName}** to **{newName}**",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { AntiraidLevel } from "./entities/AntiraidLevel";
|
||||
|
||||
export class GuildAntiraidLevels extends BaseGuildRepository {
|
||||
|
@ -7,7 +8,7 @@ export class GuildAntiraidLevels extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId: string) {
|
||||
super(guildId);
|
||||
this.antiraidLevels = getRepository(AntiraidLevel);
|
||||
this.antiraidLevels = dataSource.getRepository(AntiraidLevel);
|
||||
}
|
||||
|
||||
async get() {
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import { Guild, Snowflake, User } from "discord.js";
|
||||
import { Guild, Snowflake } from "discord.js";
|
||||
import moment from "moment-timezone";
|
||||
import { isDefaultSticker } from "src/utils/isDefaultSticker";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { renderTemplate, TemplateSafeValueContainer } from "../templateFormatter";
|
||||
import { trimLines } from "../utils";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
||||
import {
|
||||
channelToTemplateSafeChannel,
|
||||
guildToTemplateSafeGuild,
|
||||
userToTemplateSafeUser,
|
||||
} from "../utils/templateSafeObjects";
|
||||
import { SavedMessage } from "./entities/SavedMessage";
|
||||
import { Repository } from "typeorm";
|
||||
import { TemplateSafeValueContainer, renderTemplate } from "../templateFormatter";
|
||||
import { renderUsername, trimLines } from "../utils";
|
||||
import { decrypt, encrypt } from "../utils/crypt";
|
||||
import { channelToTemplateSafeChannel, guildToTemplateSafeGuild } from "../utils/templateSafeObjects";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ArchiveEntry } from "./entities/ArchiveEntry";
|
||||
import { SavedMessage } from "./entities/SavedMessage";
|
||||
|
||||
const DEFAULT_EXPIRY_DAYS = 30;
|
||||
|
||||
|
@ -20,14 +17,14 @@ const MESSAGE_ARCHIVE_HEADER_FORMAT = trimLines(`
|
|||
Server: {guild.name} ({guild.id})
|
||||
`);
|
||||
const MESSAGE_ARCHIVE_MESSAGE_FORMAT =
|
||||
"[#{channel.name}] [{user.id}] [{timestamp}] {user.username}#{user.discriminator}: {content}{attachments}{stickers}";
|
||||
"[#{channel.name}] [{user.id}] [{timestamp}] {username}: {content}{attachments}{stickers}";
|
||||
|
||||
export class GuildArchives extends BaseGuildRepository<ArchiveEntry> {
|
||||
protected archives: Repository<ArchiveEntry>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.archives = getRepository(ArchiveEntry);
|
||||
this.archives = dataSource.getRepository(ArchiveEntry);
|
||||
}
|
||||
|
||||
protected async _processEntityFromDB(entity: ArchiveEntry | undefined) {
|
||||
|
@ -46,7 +43,7 @@ export class GuildArchives extends BaseGuildRepository<ArchiveEntry> {
|
|||
return entity;
|
||||
}
|
||||
|
||||
async find(id: string): Promise<ArchiveEntry | undefined> {
|
||||
async find(id: string): Promise<ArchiveEntry | null> {
|
||||
const result = await this.archives.findOne({
|
||||
where: { id },
|
||||
relations: this.getRelations(),
|
||||
|
@ -101,6 +98,7 @@ export class GuildArchives extends BaseGuildRepository<ArchiveEntry> {
|
|||
}),
|
||||
user: partialUser,
|
||||
channel: channel ? channelToTemplateSafeChannel(channel) : null,
|
||||
username: renderUsername(msg.data.author.username, msg.data.author.discriminator),
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { AutoReaction } from "./entities/AutoReaction";
|
||||
|
||||
export class GuildAutoReactions extends BaseGuildRepository {
|
||||
|
@ -7,7 +8,7 @@ export class GuildAutoReactions extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.autoReactions = getRepository(AutoReaction);
|
||||
this.autoReactions = dataSource.getRepository(AutoReaction);
|
||||
}
|
||||
|
||||
async all(): Promise<AutoReaction[]> {
|
||||
|
@ -18,7 +19,7 @@ export class GuildAutoReactions extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async getForChannel(channelId: string): Promise<AutoReaction | undefined> {
|
||||
async getForChannel(channelId: string): Promise<AutoReaction | null> {
|
||||
return this.autoReactions.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
|
|
@ -12,15 +12,19 @@ export class GuildButtonRoles extends BaseGuildRepository {
|
|||
|
||||
async getForButtonId(buttonId: string) {
|
||||
return this.buttonRoles.findOne({
|
||||
guild_id: this.guildId,
|
||||
button_id: buttonId,
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
button_id: buttonId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getAllForMessageId(messageId: string) {
|
||||
return this.buttonRoles.find({
|
||||
guild_id: this.guildId,
|
||||
message_id: messageId,
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
message_id: messageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -40,8 +44,10 @@ export class GuildButtonRoles extends BaseGuildRepository {
|
|||
|
||||
async getForButtonGroup(buttonGroup: string) {
|
||||
return this.buttonRoles.find({
|
||||
guild_id: this.guildId,
|
||||
button_group: buttonGroup,
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
button_group: buttonGroup,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import { getRepository, In, InsertResult, Repository } from "typeorm";
|
||||
import { In, InsertResult, Repository } from "typeorm";
|
||||
import { Queue } from "../Queue";
|
||||
import { chunkArray } from "../utils";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { CaseTypes } from "./CaseTypes";
|
||||
import { connection } from "./db";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Case } from "./entities/Case";
|
||||
import { CaseNote } from "./entities/CaseNote";
|
||||
import moment from "moment-timezone";
|
||||
import { chunkArray } from "../utils";
|
||||
import { Queue } from "../Queue";
|
||||
|
||||
const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
|
||||
|
||||
export class GuildCases extends BaseGuildRepository {
|
||||
private cases: Repository<Case>;
|
||||
|
@ -18,8 +15,8 @@ export class GuildCases extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.cases = getRepository(Case);
|
||||
this.caseNotes = getRepository(CaseNote);
|
||||
this.cases = dataSource.getRepository(Case);
|
||||
this.caseNotes = dataSource.getRepository(CaseNote);
|
||||
this.createQueue = new Queue();
|
||||
}
|
||||
|
||||
|
@ -33,7 +30,7 @@ export class GuildCases extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async find(id: number): Promise<Case | undefined> {
|
||||
async find(id: number): Promise<Case | null> {
|
||||
return this.cases.findOne({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
|
@ -43,7 +40,7 @@ export class GuildCases extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async findByCaseNumber(caseNumber: number): Promise<Case | undefined> {
|
||||
async findByCaseNumber(caseNumber: number): Promise<Case | null> {
|
||||
return this.cases.findOne({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
|
@ -53,7 +50,7 @@ export class GuildCases extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async findLatestByModId(modId: string): Promise<Case | undefined> {
|
||||
async findLatestByModId(modId: string): Promise<Case | null> {
|
||||
return this.cases.findOne({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
|
@ -66,7 +63,7 @@ export class GuildCases extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async findByAuditLogId(auditLogId: string): Promise<Case | undefined> {
|
||||
async findByAuditLogId(auditLogId: string): Promise<Case | null> {
|
||||
return this.cases.findOne({
|
||||
relations: this.getRelations(),
|
||||
where: {
|
||||
|
@ -91,7 +88,7 @@ export class GuildCases extends BaseGuildRepository {
|
|||
where: {
|
||||
guild_id: this.guildId,
|
||||
mod_id: modId,
|
||||
is_hidden: 0,
|
||||
is_hidden: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -102,7 +99,7 @@ export class GuildCases extends BaseGuildRepository {
|
|||
where: {
|
||||
guild_id: this.guildId,
|
||||
mod_id: modId,
|
||||
is_hidden: 0,
|
||||
is_hidden: false,
|
||||
},
|
||||
skip,
|
||||
take: count,
|
||||
|
@ -184,7 +181,7 @@ export class GuildCases extends BaseGuildRepository {
|
|||
}
|
||||
|
||||
async softDelete(id: number, deletedById: string, deletedByName: string, deletedByText: string) {
|
||||
return connection.transaction(async (entityManager) => {
|
||||
return dataSource.transaction(async (entityManager) => {
|
||||
const cases = entityManager.getRepository(Case);
|
||||
const caseNotes = entityManager.getRepository(CaseNote);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { DeleteResult, getRepository, InsertResult, Repository } from "typeorm";
|
||||
import { DeleteResult, InsertResult, Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ContextMenuLink } from "./entities/ContextMenuLink";
|
||||
|
||||
export class GuildContextMenuLinks extends BaseGuildRepository {
|
||||
|
@ -7,10 +8,10 @@ export class GuildContextMenuLinks extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.contextLinks = getRepository(ContextMenuLink);
|
||||
this.contextLinks = dataSource.getRepository(ContextMenuLink);
|
||||
}
|
||||
|
||||
async get(id: string): Promise<ContextMenuLink | undefined> {
|
||||
async get(id: string): Promise<ContextMenuLink | null> {
|
||||
return this.contextLinks.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import moment from "moment-timezone";
|
||||
import { FindConditions, getRepository, In, IsNull, Not, Repository } from "typeorm";
|
||||
import { FindOptionsWhere, In, IsNull, Not, Repository } from "typeorm";
|
||||
import { Queue } from "../Queue";
|
||||
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { connection } from "./db";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Counter } from "./entities/Counter";
|
||||
import { CounterTrigger, isValidCounterComparisonOp, TriggerComparisonOp } from "./entities/CounterTrigger";
|
||||
import { CounterTrigger, TriggerComparisonOp, isValidCounterComparisonOp } from "./entities/CounterTrigger";
|
||||
import { CounterTriggerState } from "./entities/CounterTriggerState";
|
||||
import { CounterValue } from "./entities/CounterValue";
|
||||
|
||||
|
@ -17,11 +17,11 @@ const MAX_COUNTER_VALUE = 2147483647; // 2^31-1, for MySQL INT
|
|||
const decayQueue = new Queue();
|
||||
|
||||
async function deleteCountersMarkedToBeDeleted(): Promise<void> {
|
||||
await getRepository(Counter).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
|
||||
await dataSource.getRepository(Counter).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
|
||||
}
|
||||
|
||||
async function deleteTriggersMarkedToBeDeleted(): Promise<void> {
|
||||
await getRepository(CounterTrigger).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
|
||||
await dataSource.getRepository(CounterTrigger).createQueryBuilder().where("delete_at <= NOW()").delete().execute();
|
||||
}
|
||||
|
||||
setInterval(deleteCountersMarkedToBeDeleted, 1 * HOURS);
|
||||
|
@ -38,10 +38,10 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.counters = getRepository(Counter);
|
||||
this.counterValues = getRepository(CounterValue);
|
||||
this.counterTriggers = getRepository(CounterTrigger);
|
||||
this.counterTriggerStates = getRepository(CounterTriggerState);
|
||||
this.counters = dataSource.getRepository(Counter);
|
||||
this.counterValues = dataSource.getRepository(CounterValue);
|
||||
this.counterTriggers = dataSource.getRepository(CounterTrigger);
|
||||
this.counterTriggerStates = dataSource.getRepository(CounterTriggerState);
|
||||
}
|
||||
|
||||
async findOrCreateCounter(name: string, perChannel: boolean, perUser: boolean): Promise<Counter> {
|
||||
|
@ -80,7 +80,7 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
}
|
||||
|
||||
async markUnusedCountersToBeDeleted(idsToKeep: number[]): Promise<void> {
|
||||
const criteria: FindConditions<Counter> = {
|
||||
const criteria: FindOptionsWhere<Counter> = {
|
||||
guild_id: this.guildId,
|
||||
delete_at: IsNull(),
|
||||
};
|
||||
|
@ -161,7 +161,7 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
}
|
||||
|
||||
const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
|
||||
if (decayAmountToApply === 0) {
|
||||
if (decayAmountToApply === 0 || Number.isNaN(decayAmountToApply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -256,10 +256,12 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
throw new Error(`Invalid comparison value: ${reverseComparisonValue}`);
|
||||
}
|
||||
|
||||
return connection.transaction(async (entityManager) => {
|
||||
return dataSource.transaction(async (entityManager) => {
|
||||
const existing = await entityManager.findOne(CounterTrigger, {
|
||||
counter_id: counterId,
|
||||
name: triggerName,
|
||||
where: {
|
||||
counter_id: counterId,
|
||||
name: triggerName,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
|
@ -283,7 +285,11 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
reverse_comparison_value: reverseComparisonValue,
|
||||
});
|
||||
|
||||
return (await entityManager.findOne(CounterTrigger, insertResult.identifiers[0].id))!;
|
||||
return (await entityManager.findOne(CounterTrigger, {
|
||||
where: {
|
||||
id: insertResult.identifiers[0].id,
|
||||
},
|
||||
}))!;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -308,11 +314,13 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
channelId = channelId || "0";
|
||||
userId = userId || "0";
|
||||
|
||||
return connection.transaction(async (entityManager) => {
|
||||
return dataSource.transaction(async (entityManager) => {
|
||||
const previouslyTriggered = await entityManager.findOne(CounterTriggerState, {
|
||||
trigger_id: counterTrigger.id,
|
||||
user_id: userId!,
|
||||
channel_id: channelId!,
|
||||
where: {
|
||||
trigger_id: counterTrigger.id,
|
||||
user_id: userId!,
|
||||
channel_id: channelId!,
|
||||
},
|
||||
});
|
||||
|
||||
if (previouslyTriggered) {
|
||||
|
@ -356,7 +364,7 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
async checkAllValuesForTrigger(
|
||||
counterTrigger: CounterTrigger,
|
||||
): Promise<Array<{ channelId: string; userId: string }>> {
|
||||
return connection.transaction(async (entityManager) => {
|
||||
return dataSource.transaction(async (entityManager) => {
|
||||
const matchingValues = await entityManager
|
||||
.createQueryBuilder(CounterValue, "cv")
|
||||
.leftJoin(
|
||||
|
@ -407,7 +415,7 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
channelId = channelId || "0";
|
||||
userId = userId || "0";
|
||||
|
||||
return connection.transaction(async (entityManager) => {
|
||||
return dataSource.transaction(async (entityManager) => {
|
||||
const matchingValue = await entityManager
|
||||
.createQueryBuilder(CounterValue, "cv")
|
||||
.innerJoin(
|
||||
|
@ -446,7 +454,7 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
async checkAllValuesForReverseTrigger(
|
||||
counterTrigger: CounterTrigger,
|
||||
): Promise<Array<{ channelId: string; userId: string }>> {
|
||||
return connection.transaction(async (entityManager) => {
|
||||
return dataSource.transaction(async (entityManager) => {
|
||||
const matchingValues: Array<{
|
||||
id: string;
|
||||
triggerStateId: string;
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Mute } from "./entities/Mute";
|
||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||
import { Reminder } from "./entities/Reminder";
|
||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||
import { Tempban } from "./entities/Tempban";
|
||||
import { VCAlert } from "./entities/VCAlert";
|
||||
|
||||
interface GuildEventArgs extends Record<string, unknown[]> {
|
||||
expiredMute: [Mute];
|
||||
timeoutMuteToRenew: [Mute];
|
||||
scheduledPost: [ScheduledPost];
|
||||
reminder: [Reminder];
|
||||
expiredTempban: [Tempban];
|
||||
|
|
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 { connection } from "./db";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { MemberTimezone } from "./entities/MemberTimezone";
|
||||
|
||||
export class GuildMemberTimezones extends BaseGuildRepository {
|
||||
|
@ -8,22 +8,26 @@ export class GuildMemberTimezones extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId: string) {
|
||||
super(guildId);
|
||||
this.memberTimezones = getRepository(MemberTimezone);
|
||||
this.memberTimezones = dataSource.getRepository(MemberTimezone);
|
||||
}
|
||||
|
||||
get(memberId: string) {
|
||||
return this.memberTimezones.findOne({
|
||||
guild_id: this.guildId,
|
||||
member_id: memberId,
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
member_id: memberId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async set(memberId, timezone: string) {
|
||||
await connection.transaction(async (entityManager) => {
|
||||
await dataSource.transaction(async (entityManager) => {
|
||||
const repo = entityManager.getRepository(MemberTimezone);
|
||||
const existingRow = await repo.findOne({
|
||||
guild_id: this.guildId,
|
||||
member_id: memberId,
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
member_id: memberId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRow) {
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
import moment from "moment-timezone";
|
||||
import { Brackets, getRepository, Repository } from "typeorm";
|
||||
import { Brackets, Repository } from "typeorm";
|
||||
import { DBDateFormat } from "../utils";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { MuteTypes } from "./MuteTypes";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Mute } from "./entities/Mute";
|
||||
|
||||
export type AddMuteParams = {
|
||||
userId: Mute["user_id"];
|
||||
type: MuteTypes;
|
||||
expiresAt: number | null;
|
||||
rolesToRestore?: Mute["roles_to_restore"];
|
||||
muteRole?: string | null;
|
||||
timeoutExpiresAt?: number;
|
||||
};
|
||||
|
||||
export class GuildMutes extends BaseGuildRepository {
|
||||
private mutes: Repository<Mute>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.mutes = getRepository(Mute);
|
||||
this.mutes = dataSource.getRepository(Mute);
|
||||
}
|
||||
|
||||
async getExpiredMutes(): Promise<Mute[]> {
|
||||
|
@ -20,7 +32,7 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
.getMany();
|
||||
}
|
||||
|
||||
async findExistingMuteForUserId(userId: string): Promise<Mute | undefined> {
|
||||
async findExistingMuteForUserId(userId: string): Promise<Mute | null> {
|
||||
return this.mutes.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
@ -34,14 +46,18 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
return mute != null;
|
||||
}
|
||||
|
||||
async addMute(userId, expiryTime, rolesToRestore?: string[]): Promise<Mute> {
|
||||
const expiresAt = expiryTime ? moment.utc().add(expiryTime, "ms").format("YYYY-MM-DD HH:mm:ss") : null;
|
||||
async addMute(params: AddMuteParams): Promise<Mute> {
|
||||
const expiresAt = params.expiresAt ? moment.utc(params.expiresAt).format(DBDateFormat) : null;
|
||||
const timeoutExpiresAt = params.timeoutExpiresAt ? moment.utc(params.timeoutExpiresAt).format(DBDateFormat) : null;
|
||||
|
||||
const result = await this.mutes.insert({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
user_id: params.userId,
|
||||
type: params.type,
|
||||
expires_at: expiresAt,
|
||||
roles_to_restore: rolesToRestore ?? [],
|
||||
roles_to_restore: params.rolesToRestore ?? [],
|
||||
mute_role: params.muteRole,
|
||||
timeout_expires_at: timeoutExpiresAt,
|
||||
});
|
||||
|
||||
return (await this.mutes.findOne({ where: result.identifiers[0] }))!;
|
||||
|
@ -74,6 +90,32 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
}
|
||||
}
|
||||
|
||||
async updateExpiresAt(userId: string, timestamp: number | null): Promise<void> {
|
||||
const expiresAt = timestamp ? moment.utc(timestamp).format("YYYY-MM-DD HH:mm:ss") : null;
|
||||
await this.mutes.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
expires_at: expiresAt,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async updateTimeoutExpiresAt(userId: string, timestamp: number): Promise<void> {
|
||||
const timeoutExpiresAt = moment.utc(timestamp).format(DBDateFormat);
|
||||
await this.mutes.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
timeout_expires_at: timeoutExpiresAt,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getActiveMutes(): Promise<Mute[]> {
|
||||
return this.mutes
|
||||
.createQueryBuilder("mutes")
|
||||
|
@ -104,4 +146,16 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
user_id: userId,
|
||||
});
|
||||
}
|
||||
|
||||
async fillMissingMuteRole(muteRole: string): Promise<void> {
|
||||
await this.mutes
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("type = :type", { type: MuteTypes.Role })
|
||||
.andWhere("mute_role IS NULL")
|
||||
.update({
|
||||
mute_role: muteRole,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import { getRepository, In, Repository } from "typeorm";
|
||||
import { In, Repository } from "typeorm";
|
||||
import { isAPI } from "../globals";
|
||||
import { MINUTES, SECONDS } from "../utils";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { cleanupNicknames } from "./cleanup/nicknames";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { NicknameHistoryEntry } from "./entities/NicknameHistoryEntry";
|
||||
|
||||
const CLEANUP_INTERVAL = 5 * MINUTES;
|
||||
|
||||
async function cleanup() {
|
||||
await cleanupNicknames();
|
||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
if (!isAPI()) {
|
||||
const CLEANUP_INTERVAL = 5 * MINUTES;
|
||||
|
||||
async function cleanup() {
|
||||
await cleanupNicknames();
|
||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
// Start first cleanup 30 seconds after startup
|
||||
// TODO: Move to bot startup code
|
||||
setTimeout(cleanup, 30 * SECONDS);
|
||||
}
|
||||
|
||||
|
@ -24,7 +26,7 @@ export class GuildNicknameHistory extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.nicknameHistory = getRepository(NicknameHistoryEntry);
|
||||
this.nicknameHistory = dataSource.getRepository(NicknameHistoryEntry);
|
||||
}
|
||||
|
||||
async getByUserId(userId): Promise<NicknameHistoryEntry[]> {
|
||||
|
@ -39,7 +41,7 @@ export class GuildNicknameHistory extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
getLastEntry(userId): Promise<NicknameHistoryEntry | undefined> {
|
||||
getLastEntry(userId): Promise<NicknameHistoryEntry | null> {
|
||||
return this.nicknameHistory.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { PersistedData } from "./entities/PersistedData";
|
||||
|
||||
export interface IPartialPersistData {
|
||||
roles?: string[];
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export class GuildPersistedData extends BaseGuildRepository {
|
||||
private persistedData: Repository<PersistedData>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.persistedData = getRepository(PersistedData);
|
||||
this.persistedData = dataSource.getRepository(PersistedData);
|
||||
}
|
||||
|
||||
async find(userId: string) {
|
||||
|
@ -24,11 +20,7 @@ export class GuildPersistedData extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async set(userId: string, data: IPartialPersistData = {}) {
|
||||
const finalData: any = {};
|
||||
if (data.roles) finalData.roles = data.roles.join(",");
|
||||
if (data.nickname) finalData.nickname = data.nickname;
|
||||
|
||||
async set(userId: string, data: Partial<PersistedData> = {}) {
|
||||
const existing = await this.find(userId);
|
||||
if (existing) {
|
||||
await this.persistedData.update(
|
||||
|
@ -36,11 +28,11 @@ export class GuildPersistedData extends BaseGuildRepository {
|
|||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
finalData,
|
||||
data,
|
||||
);
|
||||
} else {
|
||||
await this.persistedData.insert({
|
||||
...finalData,
|
||||
...data,
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { PingableRole } from "./entities/PingableRole";
|
||||
|
||||
export class GuildPingableRoles extends BaseGuildRepository {
|
||||
|
@ -7,7 +8,7 @@ export class GuildPingableRoles extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.pingableRoles = getRepository(PingableRole);
|
||||
this.pingableRoles = dataSource.getRepository(PingableRole);
|
||||
}
|
||||
|
||||
async all(): Promise<PingableRole[]> {
|
||||
|
@ -27,7 +28,7 @@ export class GuildPingableRoles extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async getByChannelAndRoleId(channelId: string, roleId: string): Promise<PingableRole | undefined> {
|
||||
async getByChannelAndRoleId(channelId: string, roleId: string): Promise<PingableRole | null> {
|
||||
return this.pingableRoles.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ReactionRole } from "./entities/ReactionRole";
|
||||
|
||||
export class GuildReactionRoles extends BaseGuildRepository {
|
||||
|
@ -7,7 +8,7 @@ export class GuildReactionRoles extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.reactionRoles = getRepository(ReactionRole);
|
||||
this.reactionRoles = dataSource.getRepository(ReactionRole);
|
||||
}
|
||||
|
||||
async all(): Promise<ReactionRole[]> {
|
||||
|
@ -30,7 +31,7 @@ export class GuildReactionRoles extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async getByMessageAndEmoji(messageId: string, emoji: string): Promise<ReactionRole | undefined> {
|
||||
async getByMessageAndEmoji(messageId: string, emoji: string): Promise<ReactionRole | null> {
|
||||
return this.reactionRoles.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Reminder } from "./entities/Reminder";
|
||||
|
||||
export class GuildReminders extends BaseGuildRepository {
|
||||
|
@ -7,7 +8,7 @@ export class GuildReminders extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.reminders = getRepository(Reminder);
|
||||
this.reminders = dataSource.getRepository(Reminder);
|
||||
}
|
||||
|
||||
async getDueReminders(): Promise<Reminder[]> {
|
||||
|
@ -28,7 +29,9 @@ export class GuildReminders extends BaseGuildRepository {
|
|||
}
|
||||
|
||||
find(id: number) {
|
||||
return this.reminders.findOne({ id });
|
||||
return this.reminders.findOne({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
|
|
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 moment from "moment-timezone";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
import { Repository } from "typeorm";
|
||||
import { QueuedEventEmitter } from "../QueuedEventEmitter";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
|
||||
import { buildEntity } from "./buildEntity";
|
||||
import { noop } from "../utils";
|
||||
import { decrypt } from "../utils/crypt";
|
||||
import { decryptJson, encryptJson } from "../utils/cryptHelpers";
|
||||
import { asyncMap } from "../utils/async";
|
||||
import { decryptJson, encryptJson } from "../utils/cryptHelpers";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { buildEntity } from "./buildEntity";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ISavedMessageData, SavedMessage } from "./entities/SavedMessage";
|
||||
|
||||
export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
||||
private messages: Repository<SavedMessage>;
|
||||
|
@ -19,7 +18,7 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.messages = getRepository(SavedMessage);
|
||||
this.messages = dataSource.getRepository(SavedMessage);
|
||||
this.events = new QueuedEventEmitter();
|
||||
|
||||
this.toBePermanent = new Set();
|
||||
|
@ -53,13 +52,13 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
|||
title: embed.title,
|
||||
description: embed.description,
|
||||
url: embed.url,
|
||||
timestamp: embed.timestamp,
|
||||
timestamp: embed.timestamp ? Date.parse(embed.timestamp) : null,
|
||||
color: embed.color,
|
||||
|
||||
fields: embed.fields.map((field) => ({
|
||||
name: field.name,
|
||||
value: field.value,
|
||||
inline: field.inline,
|
||||
inline: field.inline ?? false,
|
||||
})),
|
||||
|
||||
author: embed.author
|
||||
|
@ -128,7 +127,7 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
|||
return entity;
|
||||
}
|
||||
|
||||
entity.data = await decryptJson(entity.data as unknown as string);
|
||||
entity.data = (await decryptJson(entity.data as unknown as string)) as ISavedMessageData;
|
||||
return entity;
|
||||
}
|
||||
|
||||
|
@ -139,7 +138,7 @@ export class GuildSavedMessages extends BaseGuildRepository<SavedMessage> {
|
|||
return entity;
|
||||
}
|
||||
|
||||
async find(id: string, includeDeleted = false): Promise<SavedMessage | undefined> {
|
||||
async find(id: string, includeDeleted = false): Promise<SavedMessage | null> {
|
||||
let query = this.messages
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||
|
||||
export class GuildScheduledPosts extends BaseGuildRepository {
|
||||
|
@ -7,7 +8,7 @@ export class GuildScheduledPosts extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.scheduledPosts = getRepository(ScheduledPost);
|
||||
this.scheduledPosts = dataSource.getRepository(ScheduledPost);
|
||||
}
|
||||
|
||||
all(): Promise<ScheduledPost[]> {
|
||||
|
@ -23,7 +24,11 @@ export class GuildScheduledPosts extends BaseGuildRepository {
|
|||
}
|
||||
|
||||
find(id: number) {
|
||||
return this.scheduledPosts.findOne({ id });
|
||||
return this.scheduledPosts.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import moment from "moment-timezone";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { SlowmodeChannel } from "./entities/SlowmodeChannel";
|
||||
import { SlowmodeUser } from "./entities/SlowmodeUser";
|
||||
|
||||
|
@ -10,11 +11,11 @@ export class GuildSlowmodes extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.slowmodeChannels = getRepository(SlowmodeChannel);
|
||||
this.slowmodeUsers = getRepository(SlowmodeUser);
|
||||
this.slowmodeChannels = dataSource.getRepository(SlowmodeChannel);
|
||||
this.slowmodeUsers = dataSource.getRepository(SlowmodeUser);
|
||||
}
|
||||
|
||||
async getChannelSlowmode(channelId): Promise<SlowmodeChannel | undefined> {
|
||||
async getChannelSlowmode(channelId): Promise<SlowmodeChannel | null> {
|
||||
return this.slowmodeChannels.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
@ -51,11 +52,13 @@ export class GuildSlowmodes extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async getChannelSlowmodeUser(channelId, userId): Promise<SlowmodeUser | undefined> {
|
||||
async getChannelSlowmodeUser(channelId, userId): Promise<SlowmodeUser | null> {
|
||||
return this.slowmodeUsers.findOne({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
user_id: userId,
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { StarboardMessage } from "./entities/StarboardMessage";
|
||||
|
||||
export class GuildStarboardMessages extends BaseGuildRepository {
|
||||
|
@ -7,7 +8,7 @@ export class GuildStarboardMessages extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.allStarboardMessages = getRepository(StarboardMessage);
|
||||
this.allStarboardMessages = dataSource.getRepository(StarboardMessage);
|
||||
}
|
||||
|
||||
async getStarboardMessagesForMessageId(messageId: string) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { StarboardReaction } from "./entities/StarboardReaction";
|
||||
|
||||
export class GuildStarboardReactions extends BaseGuildRepository {
|
||||
|
@ -7,7 +8,7 @@ export class GuildStarboardReactions extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.allStarboardReactions = getRepository(StarboardReaction);
|
||||
this.allStarboardReactions = dataSource.getRepository(StarboardReaction);
|
||||
}
|
||||
|
||||
async getAllReactionsForMessageId(messageId: string) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { StatValue } from "./entities/StatValue";
|
||||
|
||||
export class GuildStats extends BaseGuildRepository {
|
||||
|
@ -7,7 +8,7 @@ export class GuildStats extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.stats = getRepository(StatValue);
|
||||
this.stats = dataSource.getRepository(StatValue);
|
||||
}
|
||||
|
||||
async saveValue(source: string, key: string, value: number): Promise<void> {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Tag } from "./entities/Tag";
|
||||
import { TagResponse } from "./entities/TagResponse";
|
||||
|
||||
|
@ -9,8 +10,8 @@ export class GuildTags extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.tags = getRepository(Tag);
|
||||
this.tagResponses = getRepository(TagResponse);
|
||||
this.tags = dataSource.getRepository(Tag);
|
||||
this.tagResponses = dataSource.getRepository(TagResponse);
|
||||
}
|
||||
|
||||
async all(): Promise<Tag[]> {
|
||||
|
@ -21,7 +22,7 @@ export class GuildTags extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async find(tag): Promise<Tag | undefined> {
|
||||
async find(tag): Promise<Tag | null> {
|
||||
return this.tags.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
@ -61,7 +62,7 @@ export class GuildTags extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async findResponseByCommandMessageId(messageId: string): Promise<TagResponse | undefined> {
|
||||
async findResponseByCommandMessageId(messageId: string): Promise<TagResponse | null> {
|
||||
return this.tagResponses.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
@ -70,7 +71,7 @@ export class GuildTags extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async findResponseByResponseMessageId(messageId: string): Promise<TagResponse | undefined> {
|
||||
async findResponseByResponseMessageId(messageId: string): Promise<TagResponse | null> {
|
||||
return this.tagResponses.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import moment from "moment-timezone";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Tempban } from "./entities/Tempban";
|
||||
|
||||
export class GuildTempbans extends BaseGuildRepository {
|
||||
|
@ -8,7 +9,7 @@ export class GuildTempbans extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.tempbans = getRepository(Tempban);
|
||||
this.tempbans = dataSource.getRepository(Tempban);
|
||||
}
|
||||
|
||||
async getExpiredTempbans(): Promise<Tempban[]> {
|
||||
|
@ -20,7 +21,7 @@ export class GuildTempbans extends BaseGuildRepository {
|
|||
.getMany();
|
||||
}
|
||||
|
||||
async findExistingTempbanForUserId(userId: string): Promise<Tempban | undefined> {
|
||||
async findExistingTempbanForUserId(userId: string): Promise<Tempban | null> {
|
||||
return this.tempbans.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { VCAlert } from "./entities/VCAlert";
|
||||
|
||||
export class GuildVCAlerts extends BaseGuildRepository {
|
||||
|
@ -7,7 +8,7 @@ export class GuildVCAlerts extends BaseGuildRepository {
|
|||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.allAlerts = getRepository(VCAlert);
|
||||
this.allAlerts = dataSource.getRepository(VCAlert);
|
||||
}
|
||||
|
||||
async getOutdatedAlerts(): Promise<VCAlert[]> {
|
||||
|
@ -41,7 +42,9 @@ export class GuildVCAlerts extends BaseGuildRepository {
|
|||
}
|
||||
|
||||
find(id: number) {
|
||||
return this.allAlerts.findOne({ id });
|
||||
return this.allAlerts.findOne({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
|
|
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 { Brackets, getRepository, Repository } from "typeorm";
|
||||
import { Mute } from "./entities/Mute";
|
||||
import { Repository } from "typeorm";
|
||||
import { DAYS, DBDateFormat } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { MuteTypes } from "./MuteTypes";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Mute } from "./entities/Mute";
|
||||
|
||||
const OLD_EXPIRED_MUTE_THRESHOLD = 7 * DAYS;
|
||||
|
||||
export const MAX_TIMEOUT_DURATION = 27 * DAYS;
|
||||
// When a timeout is under this duration but the mute expires later, the timeout will be reset to max duration
|
||||
export const TIMEOUT_RENEWAL_THRESHOLD = 21 * DAYS;
|
||||
|
||||
export class Mutes extends BaseRepository {
|
||||
private mutes: Repository<Mute>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.mutes = getRepository(Mute);
|
||||
this.mutes = dataSource.getRepository(Mute);
|
||||
}
|
||||
|
||||
async getSoonExpiringMutes(threshold: number): Promise<Mute[]> {
|
||||
findMute(guildId: string, userId: string): Promise<Mute | null> {
|
||||
return this.mutes.findOne({
|
||||
where: {
|
||||
guild_id: guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getSoonExpiringMutes(threshold: number): Promise<Mute[]> {
|
||||
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
|
||||
return this.mutes
|
||||
.createQueryBuilder("mutes")
|
||||
|
@ -23,6 +38,16 @@ export class Mutes extends BaseRepository {
|
|||
.getMany();
|
||||
}
|
||||
|
||||
getTimeoutMutesToRenew(threshold: number): Promise<Mute[]> {
|
||||
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
|
||||
return this.mutes
|
||||
.createQueryBuilder("mutes")
|
||||
.andWhere("type = :type", { type: MuteTypes.Timeout })
|
||||
.andWhere("(expires_at IS NULL OR timeout_expires_at < expires_at)")
|
||||
.andWhere("timeout_expires_at <= :date", { date: thresholdDateStr })
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async clearOldExpiredMutes(): Promise<void> {
|
||||
const thresholdDateStr = moment.utc().subtract(OLD_EXPIRED_MUTE_THRESHOLD, "ms").format(DBDateFormat);
|
||||
await this.mutes
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry";
|
||||
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman";
|
||||
import fetch, { Headers } from "node-fetch";
|
||||
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
|
||||
import moment from "moment-timezone";
|
||||
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
|
||||
import crypto from "crypto";
|
||||
import moment from "moment-timezone";
|
||||
import { Repository } from "typeorm";
|
||||
import { env } from "../env";
|
||||
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry";
|
||||
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
|
||||
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman";
|
||||
|
||||
const API_URL = "https://api.phisherman.gg";
|
||||
const MASTER_API_KEY = process.env.PHISHERMAN_API_KEY;
|
||||
const MASTER_API_KEY = env.PHISHERMAN_API_KEY;
|
||||
|
||||
let caughtDomainTrackingMap: Map<string, Map<string, number[]>> = new Map();
|
||||
|
||||
|
@ -39,7 +40,7 @@ const KEY_VALIDITY_LIFETIME = 24 * HOURS;
|
|||
let cacheRepository: Repository<PhishermanCacheEntry> | null = null;
|
||||
function getCacheRepository(): Repository<PhishermanCacheEntry> {
|
||||
if (cacheRepository == null) {
|
||||
cacheRepository = getRepository(PhishermanCacheEntry);
|
||||
cacheRepository = dataSource.getRepository(PhishermanCacheEntry);
|
||||
}
|
||||
return cacheRepository;
|
||||
}
|
||||
|
@ -47,7 +48,7 @@ function getCacheRepository(): Repository<PhishermanCacheEntry> {
|
|||
let keyCacheRepository: Repository<PhishermanKeyCacheEntry> | null = null;
|
||||
function getKeyCacheRepository(): Repository<PhishermanKeyCacheEntry> {
|
||||
if (keyCacheRepository == null) {
|
||||
keyCacheRepository = getRepository(PhishermanKeyCacheEntry);
|
||||
keyCacheRepository = dataSource.getRepository(PhishermanKeyCacheEntry);
|
||||
}
|
||||
return keyCacheRepository;
|
||||
}
|
||||
|
@ -153,7 +154,9 @@ export async function getPhishermanDomainInfo(domain: string): Promise<Phisherma
|
|||
}
|
||||
|
||||
const dbCache = getCacheRepository();
|
||||
const existingCachedEntry = await dbCache.findOne({ domain });
|
||||
const existingCachedEntry = await dbCache.findOne({
|
||||
where: { domain },
|
||||
});
|
||||
if (existingCachedEntry) {
|
||||
return existingCachedEntry.data;
|
||||
}
|
||||
|
@ -196,7 +199,9 @@ export async function phishermanApiKeyIsValid(apiKey: string): Promise<boolean>
|
|||
|
||||
const keyCache = getKeyCacheRepository();
|
||||
const hash = crypto.createHash("sha256").update(apiKey).digest("hex");
|
||||
const entry = await keyCache.findOne({ hash });
|
||||
const entry = await keyCache.findOne({
|
||||
where: { hash },
|
||||
});
|
||||
if (entry) {
|
||||
return entry.is_valid;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Reminder } from "./entities/Reminder";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import moment from "moment-timezone";
|
||||
import { Repository } from "typeorm";
|
||||
import { DBDateFormat } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Reminder } from "./entities/Reminder";
|
||||
|
||||
export class Reminders extends BaseRepository {
|
||||
private reminders: Repository<Reminder>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.reminders = getRepository(Reminder);
|
||||
this.reminders = dataSource.getRepository(Reminder);
|
||||
}
|
||||
|
||||
async getRemindersDueSoon(threshold: number): Promise<Reminder[]> {
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import moment from "moment-timezone";
|
||||
import { Repository } from "typeorm";
|
||||
import { DBDateFormat } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { ScheduledPost } from "./entities/ScheduledPost";
|
||||
|
||||
export class ScheduledPosts extends BaseRepository {
|
||||
private scheduledPosts: Repository<ScheduledPost>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.scheduledPosts = getRepository(ScheduledPost);
|
||||
this.scheduledPosts = dataSource.getRepository(ScheduledPost);
|
||||
}
|
||||
|
||||
getScheduledPostsDueSoon(threshold: number): Promise<ScheduledPost[]> {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Supporter } from "./entities/Supporter";
|
||||
|
||||
export class Supporters extends BaseRepository {
|
||||
|
@ -7,7 +8,7 @@ export class Supporters extends BaseRepository {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
this.supporters = getRepository(Supporter);
|
||||
this.supporters = dataSource.getRepository(Supporter);
|
||||
}
|
||||
|
||||
getAll() {
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import moment from "moment-timezone";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { Tempban } from "./entities/Tempban";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { Repository } from "typeorm";
|
||||
import { DBDateFormat } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Tempban } from "./entities/Tempban";
|
||||
|
||||
export class Tempbans extends BaseRepository {
|
||||
private tempbans: Repository<Tempban>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.tempbans = getRepository(Tempban);
|
||||
this.tempbans = dataSource.getRepository(Tempban);
|
||||
}
|
||||
|
||||
getSoonExpiringTempbans(threshold: number): Promise<Tempban[]> {
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import { getRepository, In, Repository } from "typeorm";
|
||||
import { In, Repository } from "typeorm";
|
||||
import { isAPI } from "../globals";
|
||||
import { MINUTES, SECONDS } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { cleanupUsernames } from "./cleanup/usernames";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { UsernameHistoryEntry } from "./entities/UsernameHistoryEntry";
|
||||
|
||||
const CLEANUP_INTERVAL = 5 * MINUTES;
|
||||
|
||||
async function cleanup() {
|
||||
await cleanupUsernames();
|
||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
if (!isAPI()) {
|
||||
const CLEANUP_INTERVAL = 5 * MINUTES;
|
||||
|
||||
async function cleanup() {
|
||||
await cleanupUsernames();
|
||||
setTimeout(cleanup, CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
// Start first cleanup 30 seconds after startup
|
||||
// TODO: Move to bot startup code
|
||||
setTimeout(cleanup, 30 * SECONDS);
|
||||
}
|
||||
|
||||
|
@ -24,7 +26,7 @@ export class UsernameHistory extends BaseRepository {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
this.usernameHistory = getRepository(UsernameHistoryEntry);
|
||||
this.usernameHistory = dataSource.getRepository(UsernameHistoryEntry);
|
||||
}
|
||||
|
||||
async getByUserId(userId): Promise<UsernameHistoryEntry[]> {
|
||||
|
@ -39,7 +41,7 @@ export class UsernameHistory extends BaseRepository {
|
|||
});
|
||||
}
|
||||
|
||||
getLastEntry(userId): Promise<UsernameHistoryEntry | undefined> {
|
||||
getLastEntry(userId): Promise<UsernameHistoryEntry | null> {
|
||||
return this.usernameHistory.findOne({
|
||||
where: {
|
||||
user_id: userId,
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { VCAlert } from "./entities/VCAlert";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import moment from "moment-timezone";
|
||||
import { Repository } from "typeorm";
|
||||
import { DBDateFormat } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { VCAlert } from "./entities/VCAlert";
|
||||
|
||||
export class VCAlerts extends BaseRepository {
|
||||
private allAlerts: Repository<VCAlert>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.allAlerts = getRepository(VCAlert);
|
||||
this.allAlerts = dataSource.getRepository(VCAlert);
|
||||
}
|
||||
|
||||
async getSoonExpiringAlerts(threshold: number): Promise<VCAlert[]> {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { getRepository, Repository } from "typeorm";
|
||||
import { Webhook } from "./entities/Webhook";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { Repository } from "typeorm";
|
||||
import { decrypt, encrypt } from "../utils/crypt";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { dataSource } from "./dataSource";
|
||||
import { Webhook } from "./entities/Webhook";
|
||||
|
||||
export class Webhooks extends BaseRepository {
|
||||
repository: Repository<Webhook> = getRepository(Webhook);
|
||||
repository: Repository<Webhook> = dataSource.getRepository(Webhook);
|
||||
|
||||
protected async _processEntityFromDB(entity) {
|
||||
entity.token = await decrypt(entity.token);
|
||||
|
|
|
@ -19,8 +19,6 @@ export type RemoveApiPermissionEventData = {
|
|||
target_id: string;
|
||||
};
|
||||
|
||||
export type EditConfigEventData = {};
|
||||
|
||||
export interface AuditLogEventData extends Record<AuditLogEventType, unknown> {
|
||||
ADD_API_PERMISSION: {
|
||||
type: ApiPermissionTypes;
|
||||
|
@ -41,7 +39,7 @@ export interface AuditLogEventData extends Record<AuditLogEventType, unknown> {
|
|||
target_id: string;
|
||||
};
|
||||
|
||||
EDIT_CONFIG: {};
|
||||
EDIT_CONFIG: Record<string, never>;
|
||||
}
|
||||
|
||||
export type AnyAuditLogEventData = AuditLogEventData[AuditLogEventType];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export function buildEntity<T extends any>(Entity: new () => T, data: Partial<T>): T {
|
||||
export function buildEntity<T extends object>(Entity: new () => T, data: Partial<T>): T {
|
||||
const instance = new Entity();
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
instance[key] = value;
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import moment from "moment-timezone";
|
||||
import { getRepository, In } from "typeorm";
|
||||
import { In } from "typeorm";
|
||||
import { DBDateFormat } from "../../utils";
|
||||
import { connection } from "../db";
|
||||
import { dataSource } from "../dataSource";
|
||||
import { Config } from "../entities/Config";
|
||||
|
||||
const CLEAN_PER_LOOP = 50;
|
||||
|
||||
export async function cleanupConfigs() {
|
||||
const configRepository = getRepository(Config);
|
||||
const configRepository = dataSource.getRepository(Config);
|
||||
|
||||
// FIXME: The query below doesn't work on MySQL 8.0. Pending an update.
|
||||
return;
|
||||
|
||||
let cleaned = 0;
|
||||
let rows;
|
||||
|
@ -15,7 +18,7 @@ export async function cleanupConfigs() {
|
|||
// >1 month old: 1 config retained per month
|
||||
const oneMonthCutoff = moment.utc().subtract(30, "days").format(DBDateFormat);
|
||||
do {
|
||||
rows = await connection.query(
|
||||
rows = await dataSource.query(
|
||||
`
|
||||
WITH _configs
|
||||
AS (
|
||||
|
@ -53,7 +56,7 @@ export async function cleanupConfigs() {
|
|||
// >2 weeks old: 1 config retained per day
|
||||
const twoWeekCutoff = moment.utc().subtract(2, "weeks").format(DBDateFormat);
|
||||
do {
|
||||
rows = await connection.query(
|
||||
rows = await dataSource.query(
|
||||
`
|
||||
WITH _configs
|
||||
AS (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import moment from "moment-timezone";
|
||||
import { getRepository, In } from "typeorm";
|
||||
import { In } from "typeorm";
|
||||
import { DAYS, DBDateFormat, MINUTES, SECONDS, sleep } from "../../utils";
|
||||
import { connection } from "../db";
|
||||
import { dataSource } from "../dataSource";
|
||||
import { SavedMessage } from "../entities/SavedMessage";
|
||||
|
||||
/**
|
||||
|
@ -16,7 +16,7 @@ const CLEAN_PER_LOOP = 100;
|
|||
export async function cleanupMessages(): Promise<number> {
|
||||
let cleaned = 0;
|
||||
|
||||
const messagesRepository = getRepository(SavedMessage);
|
||||
const messagesRepository = dataSource.getRepository(SavedMessage);
|
||||
|
||||
const deletedAtThreshold = moment.utc().subtract(DELETED_MESSAGE_RETENTION_PERIOD, "ms").format(DBDateFormat);
|
||||
const postedAtThreshold = moment.utc().subtract(RETENTION_PERIOD, "ms").format(DBDateFormat);
|
||||
|
@ -27,7 +27,7 @@ export async function cleanupMessages(): Promise<number> {
|
|||
// when a message was being inserted at the same time
|
||||
let ids: string[];
|
||||
do {
|
||||
const deletedMessageRows = await connection.query(
|
||||
const deletedMessageRows = await dataSource.query(
|
||||
`
|
||||
SELECT id
|
||||
FROM messages
|
||||
|
@ -40,7 +40,7 @@ export async function cleanupMessages(): Promise<number> {
|
|||
[deletedAtThreshold],
|
||||
);
|
||||
|
||||
const oldPostedRows = await connection.query(
|
||||
const oldPostedRows = await dataSource.query(
|
||||
`
|
||||
SELECT id
|
||||
FROM messages
|
||||
|
@ -53,7 +53,7 @@ export async function cleanupMessages(): Promise<number> {
|
|||
[postedAtThreshold],
|
||||
);
|
||||
|
||||
const oldBotPostedRows = await connection.query(
|
||||
const oldBotPostedRows = await dataSource.query(
|
||||
`
|
||||
SELECT id
|
||||
FROM messages
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import moment from "moment-timezone";
|
||||
import { getRepository, In } from "typeorm";
|
||||
import { In } from "typeorm";
|
||||
import { DAYS, DBDateFormat } from "../../utils";
|
||||
import { connection } from "../db";
|
||||
import { dataSource } from "../dataSource";
|
||||
import { NicknameHistoryEntry } from "../entities/NicknameHistoryEntry";
|
||||
|
||||
export const NICKNAME_RETENTION_PERIOD = 30 * DAYS;
|
||||
|
@ -10,13 +10,13 @@ const CLEAN_PER_LOOP = 500;
|
|||
export async function cleanupNicknames(): Promise<number> {
|
||||
let cleaned = 0;
|
||||
|
||||
const nicknameHistoryRepository = getRepository(NicknameHistoryEntry);
|
||||
const nicknameHistoryRepository = dataSource.getRepository(NicknameHistoryEntry);
|
||||
const dateThreshold = moment.utc().subtract(NICKNAME_RETENTION_PERIOD, "ms").format(DBDateFormat);
|
||||
|
||||
// Clean old nicknames (NICKNAME_RETENTION_PERIOD)
|
||||
let rows;
|
||||
do {
|
||||
rows = await connection.query(
|
||||
rows = await dataSource.query(
|
||||
`
|
||||
SELECT id
|
||||
FROM nickname_history
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import moment from "moment-timezone";
|
||||
import { getRepository, In } from "typeorm";
|
||||
import { In } from "typeorm";
|
||||
import { DAYS, DBDateFormat } from "../../utils";
|
||||
import { connection } from "../db";
|
||||
import { dataSource } from "../dataSource";
|
||||
import { UsernameHistoryEntry } from "../entities/UsernameHistoryEntry";
|
||||
|
||||
export const USERNAME_RETENTION_PERIOD = 30 * DAYS;
|
||||
|
@ -10,13 +10,13 @@ const CLEAN_PER_LOOP = 500;
|
|||
export async function cleanupUsernames(): Promise<number> {
|
||||
let cleaned = 0;
|
||||
|
||||
const usernameHistoryRepository = getRepository(UsernameHistoryEntry);
|
||||
const usernameHistoryRepository = dataSource.getRepository(UsernameHistoryEntry);
|
||||
const dateThreshold = moment.utc().subtract(USERNAME_RETENTION_PERIOD, "ms").format(DBDateFormat);
|
||||
|
||||
// Clean old usernames (USERNAME_RETENTION_PERIOD)
|
||||
let rows;
|
||||
do {
|
||||
rows = await connection.query(
|
||||
rows = await dataSource.query(
|
||||
`
|
||||
SELECT id
|
||||
FROM username_history
|
||||
|
|
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 connectionOptions from "../../ormconfig";
|
||||
import { QueryLogger } from "./queryLogger";
|
||||
import { dataSource } from "./dataSource";
|
||||
|
||||
let connectionPromise: Promise<Connection>;
|
||||
|
||||
export let connection: Connection;
|
||||
let connectionPromise: Promise<void>;
|
||||
|
||||
export function connect() {
|
||||
if (!connectionPromise) {
|
||||
connectionPromise = createConnection({
|
||||
...(connectionOptions as any),
|
||||
logging: ["query", "error"],
|
||||
logger: new QueryLogger(),
|
||||
}).then((newConnection) => {
|
||||
// Verify the DB timezone is set to UTC
|
||||
return newConnection.query("SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP) AS tz").then((r) => {
|
||||
if (r[0].tz !== "00:00:00") {
|
||||
throw new SimpleError(`Database timezone must be UTC (detected ${r[0].tz})`);
|
||||
}
|
||||
|
||||
connection = newConnection;
|
||||
return newConnection;
|
||||
});
|
||||
connectionPromise = dataSource.initialize().then(async (initializedDataSource) => {
|
||||
const tzResult = await initializedDataSource.query("SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP) AS tz");
|
||||
if (tzResult[0].tz !== "00:00:00") {
|
||||
throw new SimpleError(`Database timezone must be UTC (detected ${tzResult[0].tz})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
|
||||
import { ApiUserInfo } from "./ApiUserInfo";
|
||||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
import { AuditLogEventData, AuditLogEventType } from "../apiAuditLogTypes";
|
||||
|
||||
@Entity("api_audit_log")
|
||||
|
|
|
@ -19,7 +19,7 @@ export class ApiLogin {
|
|||
@Column()
|
||||
expires_at: string;
|
||||
|
||||
@ManyToOne((type) => ApiUserInfo, (userInfo) => userInfo.logins)
|
||||
@ManyToOne(() => ApiUserInfo, (userInfo) => userInfo.logins)
|
||||
@JoinColumn({ name: "user_id" })
|
||||
userInfo: ApiUserInfo;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
|
||||
import { ApiUserInfo } from "./ApiUserInfo";
|
||||
import { ApiPermissionTypes } from "../ApiPermissionAssignments";
|
||||
import { ApiUserInfo } from "./ApiUserInfo";
|
||||
|
||||
@Entity("api_permissions")
|
||||
export class ApiPermissionAssignment {
|
||||
|
@ -22,7 +22,7 @@ export class ApiPermissionAssignment {
|
|||
@Column({ type: String, nullable: true })
|
||||
expires_at: string | null;
|
||||
|
||||
@ManyToOne((type) => ApiUserInfo, (userInfo) => userInfo.permissionAssignments)
|
||||
@ManyToOne(() => ApiUserInfo, (userInfo) => userInfo.permissionAssignments)
|
||||
@JoinColumn({ name: "target_id" })
|
||||
userInfo: ApiUserInfo;
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ export class ApiUserInfo {
|
|||
@Column()
|
||||
updated_at: string;
|
||||
|
||||
@OneToMany((type) => ApiLogin, (login) => login.userInfo)
|
||||
@OneToMany(() => ApiLogin, (login) => login.userInfo)
|
||||
logins: ApiLogin[];
|
||||
|
||||
@OneToMany((type) => ApiPermissionAssignment, (p) => p.userInfo)
|
||||
@OneToMany(() => ApiPermissionAssignment, (p) => p.userInfo)
|
||||
permissionAssignments: ApiPermissionAssignment[];
|
||||
}
|
||||
|
|
|
@ -35,6 +35,6 @@ export class Case {
|
|||
*/
|
||||
@Column({ type: String, nullable: true }) log_message_id: string | null;
|
||||
|
||||
@OneToMany((type) => CaseNote, (note) => note.case)
|
||||
@OneToMany(() => CaseNote, (note) => note.case)
|
||||
notes: CaseNote[];
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export class CaseNote {
|
|||
|
||||
@Column() created_at: string;
|
||||
|
||||
@ManyToOne((type) => Case, (theCase) => theCase.notes)
|
||||
@ManyToOne(() => Case, (theCase) => theCase.notes)
|
||||
@JoinColumn({ name: "case_id" })
|
||||
case: Case;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ export class Config {
|
|||
@Column()
|
||||
edited_at: string;
|
||||
|
||||
@ManyToOne((type) => ApiUserInfo)
|
||||
@ManyToOne(() => ApiUserInfo)
|
||||
@JoinColumn({ name: "edited_by" })
|
||||
userInfo: ApiUserInfo;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
|||
|
||||
export const TRIGGER_COMPARISON_OPS = ["=", "!=", ">", "<", ">=", "<="] as const;
|
||||
|
||||
export type TriggerComparisonOp = typeof TRIGGER_COMPARISON_OPS[number];
|
||||
export type TriggerComparisonOp = (typeof TRIGGER_COMPARISON_OPS)[number];
|
||||
|
||||
const REVERSE_OPS: Record<TriggerComparisonOp, TriggerComparisonOp> = {
|
||||
"=": "!=",
|
||||
|
|
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()
|
||||
user_id: string;
|
||||
|
||||
@Column() type: number;
|
||||
|
||||
@Column() created_at: string;
|
||||
|
||||
@Column({ type: String, nullable: true }) expires_at: string | null;
|
||||
|
@ -17,4 +19,8 @@ export class Mute {
|
|||
@Column() case_id: number;
|
||||
|
||||
@Column("simple-array") roles_to_restore: string[];
|
||||
|
||||
@Column({ type: String, nullable: true }) mute_role: string | null;
|
||||
|
||||
@Column({ type: String, nullable: true }) timeout_expires_at: string | null;
|
||||
}
|
||||
|
|
|
@ -14,5 +14,5 @@ export class PersistedData {
|
|||
|
||||
@Column() nickname: string;
|
||||
|
||||
@Column() is_voice_muted: number;
|
||||
@Column({ type: "boolean" }) is_voice_muted: boolean;
|
||||
}
|
||||
|
|
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