Compare commits
235 Commits
Author | SHA1 | Date | |
---|---|---|---|
2c0aeef700 | |||
ecd75d6f55 | |||
4a80d42f86 | |||
075fde71df | |||
55136aecdc | |||
63fc2cdcbc | |||
3190738fc5 | |||
8f4810b532 | |||
a5e6c41fa5 | |||
5f0aa0f834 | |||
dbe8e8e358 | |||
85a114e55c | |||
329492b244 | |||
66135ecd08 | |||
382c2a5a1e | |||
b91245a3f7 | |||
6f0bdf9852 | |||
dcee9e0d2a | |||
8e6e1a18b7 | |||
72af0532fa | |||
e83b643d86 | |||
0e0ab053f3 | |||
8c2296b9c8 | |||
1c6103142f | |||
328127c55e | |||
b0e37b56c0 | |||
45f5b6261a | |||
5f6326179c | |||
6254f91841 | |||
60b90a61d4 | |||
90f05758d0 | |||
74b7b5d711 | |||
90550dc2c7 | |||
79e6498245 | |||
a8ef3d03f9 | |||
53e13844f9 | |||
dd7e681285 | |||
6c20bf2a0f | |||
15aa9ccffd | |||
525471bcad | |||
86d53b63b6 | |||
d8f266852a | |||
76a286076b | |||
5e39e16060 | |||
c1305cfb36 | |||
4823754955 | |||
eb92eacb90 | |||
d0833b7bca | |||
b81c3c80c1 | |||
2f6d035efe | |||
96012ce43c | |||
fa7ec8731b | |||
def43bfa78 | |||
e4e9af2bb4 | |||
cce0de7c75 | |||
e7803b98e8 | |||
7aae246388 | |||
a2d442bc54 | |||
59982df827 | |||
7a6372ed02 | |||
14a54471f7 | |||
5d3b77f1cd | |||
1d64c8bb79 | |||
8ba0f02b98 | |||
d36438c6ce | |||
e0c60e2ce3 | |||
e7160215b0 | |||
6eaa6f0f28 | |||
9db0fa2513 | |||
ca13fd4fa7 | |||
55acc8fd16 | |||
145711fa5d | |||
5524215786 | |||
e8bd05893f | |||
e3d3418f99 | |||
2681280a39 | |||
00579428a1 | |||
b8ef999710 | |||
e8f84e281a | |||
8ddff698e5 | |||
541633270c | |||
25286da5e0 | |||
4bad1324b9 | |||
bd1462a00c | |||
56ffc43616 | |||
52cf642455 | |||
0bf578357a | |||
6e9eccb62e | |||
6ea28284ce | |||
a6525f3052 | |||
348639270d | |||
37177c2431 | |||
8587bed703 | |||
6c9af1ae8e | |||
7695b7a476 | |||
651da7b28e | |||
eb086146bf | |||
4ebd705e5e | |||
5a85f1d83a | |||
68ba25886a | |||
e25bf6b828 | |||
5a386daa9d | |||
0d4a02fb1e | |||
e135a74a9b | |||
77f17c8dc2 | |||
6a94f990cf | |||
3aa5bd37aa | |||
fa83fed1af | |||
666cb7fa2f | |||
a5678e15dc | |||
9405cfcee9 | |||
cb25d02cdf | |||
bfe651a125 | |||
dc5e52d9ce | |||
229ada83e1 | |||
13171d6744 | |||
2ad941c94c | |||
924d31e978 | |||
f9a1b23212 | |||
ae5795a7ea | |||
ee36c38eda | |||
eca7df3d9f | |||
902b7e1b4a | |||
db1a53a797 | |||
3605d71b73 | |||
ea2cea573e | |||
d5fa8036e8 | |||
b8707bbc9a | |||
99eea16f62 | |||
88737302f3 | |||
213e3a5100 | |||
8fa1402ecc | |||
e63996bb61 | |||
9ede879630 | |||
88e9826a62 | |||
5d655c7e6d | |||
51c9d8a7ae | |||
90df265114 | |||
e65429aa9c | |||
8d2232f0da | |||
a58b9866ea | |||
b1f25be5d7 | |||
f0f9787326 | |||
302f5835e6 | |||
58c778632e | |||
5671fd462b | |||
5ac9733f15 | |||
01dc0334fd | |||
4a17aac15c | |||
8ce4fc9c6d | |||
b4f07cfc1c | |||
8799089b2d | |||
88c4830209 | |||
4dd3df5cc2 | |||
369a325a46 | |||
1a1a0fdefb | |||
dda8bd3e10 | |||
edbfc92cb9 | |||
6de11f09db | |||
284bfcd9ad | |||
3d627b5bf0 | |||
c3c0dbbbae | |||
64dd81e941 | |||
799298ca34 | |||
fa542bb24f | |||
e025d945cf | |||
bb1c61d0b9 | |||
1519474f93 | |||
9d8622f418 | |||
a66db37b33 | |||
c8c1a171d4 | |||
88cfb829e3 | |||
16be7a328e | |||
04babf7930 | |||
96bc09e8b5 | |||
976fb91ecc | |||
1305b6e64e | |||
cdfe44d958 | |||
c824a36832 | |||
c4bd2c1d18 | |||
561555ab7e | |||
115fbd44cb | |||
aa931328b0 | |||
4b42966284 | |||
523ab7f03a | |||
6e831c8253 | |||
4416e5d175 | |||
734a39a001 | |||
98191d29ee | |||
1c4c4a8b31 | |||
d496c81003 | |||
094d210f64 | |||
314c72e132 | |||
4e0163f2cb | |||
e5b8c418af | |||
3ef8584189 | |||
df2ad09c86 | |||
d70fb24eb1 | |||
3150c7267d | |||
6e65e4ff3d | |||
67a4db2e9a | |||
e9bcb1973f | |||
9b87fd4258 | |||
a49a849917 | |||
aa74a7f9a3 | |||
08e4c6cb57 | |||
6e087bd2dd | |||
e9792e6322 | |||
130504b964 | |||
2a8117d0c1 | |||
94bfd39085 | |||
40cd5f8a36 | |||
133b00a2ce | |||
57336f5c81 | |||
b62d24c024 | |||
8f8235a86e | |||
c8f646a8fa | |||
ecaa382a1e | |||
8991198fd3 | |||
f20b95a482 | |||
8dd7dc6409 | |||
c799d10727 | |||
ceb6fb7b12 | |||
6708abdb0f | |||
a38f6024c1 | |||
7d8748e3ef | |||
bb3386c4e8 | |||
25b84880a5 | |||
7b6e967a5d | |||
2781f2923e | |||
03f08f0a18 | |||
79c86d43f2 | |||
e19af54caf | |||
f4213c6a83 | |||
f56db14720 |
31
.gitignore
vendored
@ -1,7 +1,30 @@
|
|||||||
/target
|
target
|
||||||
.env
|
.env
|
||||||
/venv
|
/venv
|
||||||
.cargo
|
.cargo
|
||||||
assets
|
.idea
|
||||||
out.json
|
web/static/index.html
|
||||||
/.idea
|
web/static/assets
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
3343
Cargo.lock
generated
75
Cargo.toml
@ -1,32 +1,73 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder_rs"
|
name = "reminder-rs"
|
||||||
version = "1.6.0"
|
version = "1.7.4"
|
||||||
authors = ["jellywx <judesouthworth@pm.me>"]
|
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
workspaces = [".", "postman", "web", "entity", "migration"]
|
license = "AGPL-3.0 only"
|
||||||
|
description = "Reminder Bot for Discord, now in Rust"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
poise = "0.2"
|
poise = "0.6.1"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
tokio = { version = "1", features = ["process", "full"] }
|
tokio = { version = "1", features = ["process", "full"] }
|
||||||
reqwest = "0.11"
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
regex = "1.4"
|
regex = "1.10"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.8"
|
env_logger = "0.11"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
chrono-tz = { version = "0.5", features = ["serde"] }
|
chrono-tz = { version = "0.8", features = ["serde"] }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
num-integer = "0.1"
|
num-integer = "0.1"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_repr = "0.1"
|
serde_repr = "0.1"
|
||||||
rmp-serde = "0.15"
|
rmp-serde = "1.1"
|
||||||
rand = "0.7"
|
rand = "0.8"
|
||||||
levenshtein = "1.0"
|
levenshtein = "1.0"
|
||||||
base64 = "0.13.0"
|
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"] }
|
||||||
|
base64 = "0.21"
|
||||||
|
secrecy = "0.8.0"
|
||||||
|
futures = "0.3.30"
|
||||||
|
prometheus = "0.13.3"
|
||||||
|
rocket = { version = "0.5.0", features = ["tls", "secrets", "json"] }
|
||||||
|
rocket_dyn_templates = { version = "0.1.0", features = ["tera"] }
|
||||||
|
serenity = { version = "0.12", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
|
||||||
|
oauth2 = "4"
|
||||||
|
csv = "1.2"
|
||||||
|
axum = "0.7"
|
||||||
|
|
||||||
[dependencies.postman]
|
[dependencies.extract_derive]
|
||||||
path = "postman"
|
path = "extract_derive"
|
||||||
|
|
||||||
[dependencies.reminder_web]
|
[dependencies.recordable_derive]
|
||||||
path = "web"
|
path = "recordable_derive"
|
||||||
|
|
||||||
|
[package.metadata.deb]
|
||||||
|
depends = "$auto, python3-dateparser (>= 1.0.0)"
|
||||||
|
suggests = "mysql-server-8.0, nginx"
|
||||||
|
maintainer-scripts = "debian"
|
||||||
|
assets = [
|
||||||
|
["target/release/reminder-rs", "usr/bin/reminder-rs", "755"],
|
||||||
|
["static/css/*", "lib/reminder-rs/static/css", "644"],
|
||||||
|
["static/favicon/*", "lib/reminder-rs/static/favicon", "644"],
|
||||||
|
["static/img/*", "lib/reminder-rs/static/img", "644"],
|
||||||
|
["static/js/*", "lib/reminder-rs/static/js", "644"],
|
||||||
|
["static/webfonts/*", "lib/reminder-rs/static/webfonts", "644"],
|
||||||
|
["static/site.webmanifest", "lib/reminder-rs/static/site.webmanifest", "644"],
|
||||||
|
["templates/**/*", "lib/reminder-rs/templates", "644"],
|
||||||
|
["reminder-dashboard/dist/static/assets/*", "lib/reminder-rs/static/assets", "644"],
|
||||||
|
["reminder-dashboard/dist/index.html", "lib/reminder-rs/static/index.html", "644"],
|
||||||
|
["conf/default.env", "etc/reminder-rs/config.env", "600"],
|
||||||
|
["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
|
||||||
|
["healthcheck", "lib/reminder-rs/healthcheck", "755"],
|
||||||
|
["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"],
|
||||||
|
# ["nginx/reminder-rs", "etc/nginx/sites-available/reminder-rs", "755"]
|
||||||
|
]
|
||||||
|
conf-files = [
|
||||||
|
"/etc/reminder-rs/config.env",
|
||||||
|
"/etc/reminder-rs/Rocket.toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.deb.systemd-units]
|
||||||
|
unit-scripts = "systemd"
|
||||||
|
start = false
|
||||||
|
9
Containerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM ubuntu:20.04
|
||||||
|
|
||||||
|
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||||
|
CARGO_HOME=/usr/local/cargo \
|
||||||
|
PATH=/usr/local/cargo/bin:$PATH
|
||||||
|
|
||||||
|
RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib cmake pkg-config libssl-dev curl mysql-client-8.0 npm
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly
|
||||||
|
RUN cargo install cargo-deb
|
44
README.md
@ -7,25 +7,36 @@ reminders are paid on the hosted version of the bot. Keep reading if you want to
|
|||||||
|
|
||||||
You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust)
|
You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust)
|
||||||
|
|
||||||
### Compiling
|
### Build APT package
|
||||||
Install build requirements:
|
|
||||||
`sudo apt install gcc gcc-multilib cmake libssl-dev build-essential`
|
|
||||||
|
|
||||||
Install Rust from https://rustup.rs
|
Recommended method.
|
||||||
|
|
||||||
Reminder Bot can then be built by running `cargo build --release` in the top level directory. It is necessary to create a
|
By default, this builds targeting Ubuntu 20.04. Modify the Containerfile if you wish to target a different platform. These instructions are written using `podman`, but `docker` should work too.
|
||||||
folder called 'assets' containing an image file with its name specified in the environment as `WEBHOOK_AVATAR`, of
|
|
||||||
dimensions 128x128px to be used as the webhook avatar.
|
|
||||||
|
|
||||||
#### Compilation environment variables
|
1. Install container software: `sudo apt install podman`.
|
||||||
These environment variables must be provided when compiling the bot
|
2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`
|
||||||
|
3. Install SQLx CLI: `cargo install sqlx-cli`
|
||||||
|
4. From the source code directory, execute `sqlx migrate run`
|
||||||
|
5. Build container image: `podman build -t reminder-rs .`
|
||||||
|
6. Build with podman: `podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=mysql://user@localhost/reminders" reminder-rs cargo deb`
|
||||||
|
|
||||||
|
|
||||||
|
### Compiling for other target
|
||||||
|
|
||||||
|
1. Install requirements:
|
||||||
|
`sudo apt install gcc gcc-multilib cmake libssl-dev build-essential python3-dateparser`
|
||||||
|
2. Install rustup from https://rustup.rs
|
||||||
|
3. Install the nightly toolchain: `rustup toolchain default nightly`
|
||||||
|
4. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`.
|
||||||
|
5. Install `sqlx-cli`: `cargo install sqlx-cli`.
|
||||||
|
6. Run migrations: `sqlx migrate run`.
|
||||||
|
7. Set environment variables:
|
||||||
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
|
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
|
||||||
* `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size**
|
8. Build: `cargo build --release`
|
||||||
|
|
||||||
### Setting up Python
|
|
||||||
Reminder Bot by default looks for a venv within it's working directory to run Python out of. To set up a venv, install `python3-venv` and run `python3 -m venv venv`. Then, run `source venv/bin/activate` to activate the venv, and do `pip install dateparser` to install the required library
|
|
||||||
|
|
||||||
### Environment Variables
|
### Configuring
|
||||||
|
|
||||||
Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory.
|
Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory.
|
||||||
|
|
||||||
__Required Variables__
|
__Required Variables__
|
||||||
@ -37,10 +48,5 @@ __Other Variables__
|
|||||||
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
|
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
|
||||||
* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
|
* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
|
||||||
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
|
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
|
||||||
* `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else
|
* `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else
|
||||||
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds
|
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds
|
||||||
* `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages
|
|
||||||
|
|
||||||
### Todo List
|
|
||||||
|
|
||||||
* Convert aliases to macros
|
|
||||||
|
32
Rocket.toml
@ -1,28 +1,28 @@
|
|||||||
[default]
|
[default]
|
||||||
address = "0.0.0.0"
|
address = "0.0.0.0"
|
||||||
port = 5000
|
port = 18920
|
||||||
template_dir = "web/templates"
|
template_dir = "templates"
|
||||||
limits = { json = "10MiB" }
|
limits = { json = "10MiB" }
|
||||||
|
|
||||||
[debug]
|
[debug]
|
||||||
secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
|
secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
|
||||||
|
|
||||||
[debug.tls]
|
[debug.tls]
|
||||||
certs = "web/private/rsa_sha256_cert.pem"
|
certs = "private/rsa_sha256_cert.pem"
|
||||||
key = "web/private/rsa_sha256_key.pem"
|
key = "private/rsa_sha256_key.pem"
|
||||||
|
|
||||||
[rsa_sha256.tls]
|
[debug.rsa_sha256.tls]
|
||||||
certs = "web/private/rsa_sha256_cert.pem"
|
certs = "private/rsa_sha256_cert.pem"
|
||||||
key = "web/private/rsa_sha256_key.pem"
|
key = "private/rsa_sha256_key.pem"
|
||||||
|
|
||||||
[ecdsa_nistp256_sha256.tls]
|
[debug.ecdsa_nistp256_sha256.tls]
|
||||||
certs = "web/private/ecdsa_nistp256_sha256_cert.pem"
|
certs = "private/ecdsa_nistp256_sha256_cert.pem"
|
||||||
key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem"
|
key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem"
|
||||||
|
|
||||||
[ecdsa_nistp384_sha384.tls]
|
[debug.ecdsa_nistp384_sha384.tls]
|
||||||
certs = "web/private/ecdsa_nistp384_sha384_cert.pem"
|
certs = "private/ecdsa_nistp384_sha384_cert.pem"
|
||||||
key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem"
|
key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem"
|
||||||
|
|
||||||
[ed25519.tls]
|
[debug.ed25519.tls]
|
||||||
certs = "web/private/ed25519_cert.pem"
|
certs = "private/ed25519_cert.pem"
|
||||||
key = "eb/private/ed25519_key.pem"
|
key = "private/ed25519_key.pem"
|
||||||
|
BIN
assets/webhook.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
13
build.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use std::{path::Path, process::Command};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=migrations");
|
||||||
|
println!("cargo:rerun-if-changed=reminder-dashboard");
|
||||||
|
|
||||||
|
Command::new("npm")
|
||||||
|
.arg("run")
|
||||||
|
.arg("build")
|
||||||
|
.current_dir(Path::new("reminder-dashboard"))
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to build NPM");
|
||||||
|
}
|
8
conf/Rocket.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[default]
|
||||||
|
address = "127.0.0.1"
|
||||||
|
port = 18920
|
||||||
|
template_dir = "/lib/reminder-rs/templates"
|
||||||
|
limits = { json = "10MiB" }
|
||||||
|
|
||||||
|
[release]
|
||||||
|
# secret_key = ""
|
19
conf/default.env
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
DATABASE_URL=
|
||||||
|
|
||||||
|
DISCORD_TOKEN=
|
||||||
|
PATREON_GUILD_ID=
|
||||||
|
PATREON_ROLE_ID=
|
||||||
|
|
||||||
|
LOCAL_TIMEZONE=
|
||||||
|
MIN_INTERVAL=
|
||||||
|
PYTHON_LOCATION=/usr/bin/python3
|
||||||
|
DONTRUN=
|
||||||
|
SECRET_KEY=
|
||||||
|
|
||||||
|
REMIND_INTERVAL=
|
||||||
|
OAUTH2_DISCORD_CALLBACK=
|
||||||
|
OAUTH2_CLIENT_ID=
|
||||||
|
OAUTH2_CLIENT_SECRET=
|
||||||
|
|
||||||
|
REPORT_EMAIL=
|
||||||
|
LOG_TO_DATABASE=1
|
1
cron.d/reminder_health
Normal file
@ -0,0 +1 @@
|
|||||||
|
*/10 * * * * reminder /lib/reminder-rs/healthcheck
|
9
debian/postinst
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
id -u reminder &>/dev/null || useradd -r -M reminder
|
||||||
|
|
||||||
|
chown -R reminder /etc/reminder-rs
|
||||||
|
|
||||||
|
#DEBHELPER#
|
7
debian/postrm
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
id -u reminder &>/dev/null || userdel reminder
|
||||||
|
|
||||||
|
#DEBHELPER#
|
46
extract_derive/Cargo.lock
generated
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extract_macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.78"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.49"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
11
extract_derive/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "extract_derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "1.0.35"
|
||||||
|
syn = { version = "2.0.49", features = ["full"] }
|
53
extract_derive/src/lib.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use syn::{spanned::Spanned, Data, Fields};
|
||||||
|
|
||||||
|
#[proc_macro_derive(Extract)]
|
||||||
|
pub fn extract(input: TokenStream) -> TokenStream {
|
||||||
|
let ast = syn::parse_macro_input!(input);
|
||||||
|
|
||||||
|
impl_extract(&ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
|
||||||
|
let name = &ast.ident;
|
||||||
|
|
||||||
|
match &ast.data {
|
||||||
|
// Dispatch over struct: extract args directly from context
|
||||||
|
Data::Struct(st) => match &st.fields {
|
||||||
|
Fields::Named(fields) => {
|
||||||
|
let extracted = fields.named.iter().map(|field| {
|
||||||
|
let ident = &field.ident;
|
||||||
|
let ty = &field.ty;
|
||||||
|
|
||||||
|
quote::quote_spanned! {field.span()=>
|
||||||
|
#ident : crate::utils::extract_arg!(ctx, #ident, #ty)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
TokenStream::from(quote::quote! {
|
||||||
|
impl Extract for #name {
|
||||||
|
fn extract(ctx: crate::ApplicationContext) -> Self {
|
||||||
|
Self {
|
||||||
|
#(#extracted,)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Fields::Unit => TokenStream::from(quote::quote! {
|
||||||
|
impl Extract for #name {
|
||||||
|
fn extract(ctx: crate::ApplicationContext) -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_ => {
|
||||||
|
panic!("Only named/unit structs can derive Extract");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
panic!("Only structs can derive Extract");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
healthcheck
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export $(grep -v '^#' /etc/reminder-rs/config.env | xargs -d '\n')
|
||||||
|
|
||||||
|
REGEX='mysql://([A-Za-z]+)@(.+)/(.+)'
|
||||||
|
[[ $DATABASE_URL =~ $REGEX ]]
|
||||||
|
|
||||||
|
VAR=$(mysql -u "${BASH_REMATCH[1]}" -h "${BASH_REMATCH[2]}" -N -D "${BASH_REMATCH[3]}" -e "SELECT COUNT(1) FROM reminders WHERE utc_time < NOW() - INTERVAL 10 MINUTE AND enabled = 1 AND status = 'pending'")
|
||||||
|
|
||||||
|
if [ "$VAR" -gt 0 ]
|
||||||
|
then
|
||||||
|
echo "This is to inform that there is a reminder backlog which must be resolved." | mail -s "Backlog: $VAR" "$REPORT_EMAIL"
|
||||||
|
fi
|
229
migrations/20210603000000_initial.sql
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
|
CREATE TABLE guilds (
|
||||||
|
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
||||||
|
guild BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
name VARCHAR(100),
|
||||||
|
|
||||||
|
prefix VARCHAR(5) DEFAULT '$' NOT NULL,
|
||||||
|
timezone VARCHAR(32) DEFAULT 'UTC' NOT NULL,
|
||||||
|
|
||||||
|
default_channel_id INT UNSIGNED,
|
||||||
|
default_username VARCHAR(32) DEFAULT 'Reminder' NOT NULL,
|
||||||
|
default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (default_channel_id) REFERENCES channels(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE channels (
|
||||||
|
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
||||||
|
channel BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
name VARCHAR(100),
|
||||||
|
|
||||||
|
nudge SMALLINT NOT NULL DEFAULT 0,
|
||||||
|
blacklisted BOOL NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
webhook_id BIGINT UNSIGNED UNIQUE,
|
||||||
|
webhook_token TEXT,
|
||||||
|
|
||||||
|
paused BOOL NOT NULL DEFAULT 0,
|
||||||
|
paused_until TIMESTAMP,
|
||||||
|
|
||||||
|
guild_id INT UNSIGNED,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
user BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
name VARCHAR(37) NOT NULL,
|
||||||
|
|
||||||
|
dm_channel INT UNSIGNED UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
language VARCHAR(2) DEFAULT 'EN' NOT NULL,
|
||||||
|
timezone VARCHAR(32) DEFAULT 'UTC' NOT NULL,
|
||||||
|
meridian_time BOOLEAN DEFAULT 0 NOT NULL,
|
||||||
|
|
||||||
|
allowed_dm BOOLEAN DEFAULT 1 NOT NULL,
|
||||||
|
|
||||||
|
patreon BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (dm_channel) REFERENCES channels(id) ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE roles (
|
||||||
|
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
||||||
|
role BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
name VARCHAR(100),
|
||||||
|
|
||||||
|
guild_id INT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE embeds (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
title VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
description VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
image_url VARCHAR(512),
|
||||||
|
thumbnail_url VARCHAR(512),
|
||||||
|
|
||||||
|
footer VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
|
footer_icon VARCHAR(512),
|
||||||
|
|
||||||
|
color MEDIUMINT UNSIGNED NOT NULL DEFAULT 0x0,
|
||||||
|
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE embed_fields (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
title VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
value VARCHAR(1024) NOT NULL DEFAULT '',
|
||||||
|
inline BOOL NOT NULL DEFAULT 0,
|
||||||
|
embed_id INT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE messages (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
content VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
|
tts BOOL NOT NULL DEFAULT 0,
|
||||||
|
embed_id INT UNSIGNED,
|
||||||
|
|
||||||
|
attachment MEDIUMBLOB,
|
||||||
|
attachment_name VARCHAR(260),
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE reminders (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
uid VARCHAR(64) UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
name VARCHAR(24) NOT NULL DEFAULT 'Reminder',
|
||||||
|
|
||||||
|
message_id INT UNSIGNED NOT NULL,
|
||||||
|
channel_id INT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
`time` INT UNSIGNED DEFAULT 0 NOT NULL,
|
||||||
|
`interval` INT UNSIGNED DEFAULT NULL,
|
||||||
|
expires TIMESTAMP DEFAULT NULL,
|
||||||
|
|
||||||
|
enabled BOOLEAN DEFAULT 1 NOT NULL,
|
||||||
|
|
||||||
|
avatar VARCHAR(512),
|
||||||
|
username VARCHAR(32),
|
||||||
|
|
||||||
|
method ENUM('remind', 'natural', 'dashboard', 'todo', 'countdown'),
|
||||||
|
set_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
set_by INT UNSIGNED,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER message_cleanup AFTER DELETE ON reminders
|
||||||
|
FOR EACH ROW
|
||||||
|
DELETE FROM messages WHERE id = OLD.message_id;
|
||||||
|
|
||||||
|
CREATE TRIGGER embed_cleanup AFTER DELETE ON messages
|
||||||
|
FOR EACH ROW
|
||||||
|
DELETE FROM embeds WHERE id = OLD.embed_id;
|
||||||
|
|
||||||
|
CREATE TABLE todos (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
user_id INT UNSIGNED,
|
||||||
|
guild_id INT UNSIGNED,
|
||||||
|
channel_id INT UNSIGNED,
|
||||||
|
value VARCHAR(2000) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE command_restrictions (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
role_id INT UNSIGNED NOT NULL,
|
||||||
|
command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY (`role_id`, `command`)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE timers (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
start_time TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
name VARCHAR(32) NOT NULL,
|
||||||
|
owner BIGINT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE events (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
`time` TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
event_name ENUM('edit', 'enable', 'disable', 'delete') NOT NULL,
|
||||||
|
bulk_count INT UNSIGNED,
|
||||||
|
|
||||||
|
guild_id INT UNSIGNED NOT NULL,
|
||||||
|
user_id INT UNSIGNED,
|
||||||
|
reminder_id INT UNSIGNED,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE command_aliases (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
guild_id INT UNSIGNED NOT NULL,
|
||||||
|
name VARCHAR(12) NOT NULL,
|
||||||
|
|
||||||
|
command VARCHAR(2048) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY (`guild_id`, `name`)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE guild_users (
|
||||||
|
guild INT UNSIGNED NOT NULL,
|
||||||
|
user INT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
can_access BOOL NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY (guild, user)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE EVENT event_cleanup
|
||||||
|
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
|
||||||
|
ON COMPLETION PRESERVE
|
||||||
|
DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
|
163
migrations/20210623000000_reminder_message_embed.sql
Normal file
11
migrations/20210922000000_macro.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE macro (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT,
|
||||||
|
guild_id INT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description VARCHAR(100),
|
||||||
|
commands TEXT NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`;
|
||||||
|
ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL;
|
49
migrations/20220211000000_reminder_templates.sql
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
CREATE TABLE reminder_template (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
|
||||||
|
`name` VARCHAR(24) NOT NULL DEFAULT 'Reminder',
|
||||||
|
|
||||||
|
`guild_id` INT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
`username` VARCHAR(32) DEFAULT NULL,
|
||||||
|
`avatar` VARCHAR(512) DEFAULT NULL,
|
||||||
|
|
||||||
|
`content` VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
|
`tts` BOOL NOT NULL DEFAULT 0,
|
||||||
|
`attachment` MEDIUMBLOB,
|
||||||
|
`attachment_name` VARCHAR(260),
|
||||||
|
|
||||||
|
`embed_title` VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
`embed_description` VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
|
`embed_image_url` VARCHAR(512),
|
||||||
|
`embed_thumbnail_url` VARCHAR(512),
|
||||||
|
`embed_footer` VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
|
`embed_footer_url` VARCHAR(512),
|
||||||
|
`embed_author` VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
`embed_author_url` VARCHAR(512),
|
||||||
|
`embed_color` INT UNSIGNED NOT NULL DEFAULT 0x0,
|
||||||
|
`embed_fields` JSON,
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
|
||||||
|
FOREIGN KEY (`guild_id`) REFERENCES guilds (`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE reminders ADD COLUMN embed_fields JSON;
|
||||||
|
|
||||||
|
update reminders
|
||||||
|
inner join embed_fields as E
|
||||||
|
on E.reminder_id = reminders.id
|
||||||
|
set embed_fields = (
|
||||||
|
select JSON_ARRAYAGG(
|
||||||
|
JSON_OBJECT(
|
||||||
|
'title', E.title,
|
||||||
|
'value', E.value,
|
||||||
|
'inline',
|
||||||
|
if(inline = 1, cast(TRUE as json), cast(FALSE as json))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
from embed_fields
|
||||||
|
group by reminder_id
|
||||||
|
having reminder_id = reminders.id
|
||||||
|
);
|
1
migrations/20221210000000_reminder_daily_intervals.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL;
|
1
migrations/20230511125236_reminder_threads.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE reminders ADD COLUMN `thread_id` BIGINT DEFAULT NULL;
|
1
migrations/20230511180231_ephemeral_confirmations.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE guilds ADD COLUMN ephemeral_confirmations BOOL NOT NULL DEFAULT 0;
|
2
migrations/20230722130906_increase_reminder_name.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE reminders MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder';
|
||||||
|
ALTER TABLE reminder_template MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder';
|
9
migrations/20230730134827_stats.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE stat (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
`utc_time` DATETIME NOT NULL DEFAULT NOW(),
|
||||||
|
`type` ENUM('reminder_sent', 'reminder_failed'),
|
||||||
|
`reminder_id` INT UNSIGNED,
|
||||||
|
`message` TEXT,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
);
|
2
migrations/20230731170452_reminder_archive.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE reminders ADD COLUMN `status` ENUM ('pending', 'sent', 'failed', 'deleted') NOT NULL DEFAULT 'pending';
|
||||||
|
ALTER TABLE reminders ADD COLUMN `status_message` TEXT;
|
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `reminder_template` ADD COLUMN `interval_seconds` INT UNSIGNED;
|
||||||
|
ALTER TABLE `reminder_template` ADD COLUMN `interval_days` INT UNSIGNED;
|
||||||
|
ALTER TABLE `reminder_template` ADD COLUMN `interval_months` INT UNSIGNED;
|
50
migrations/20240210133900_macro_restructure.sql
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
CREATE TABLE command_macro (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT,
|
||||||
|
guild_id INT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description VARCHAR(100),
|
||||||
|
commands JSON NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
# New JSON structure is {command_name: "Remind", "<option name>": "<option value>", ...}
|
||||||
|
INSERT INTO command_macro (guild_id, description, name, commands)
|
||||||
|
SELECT
|
||||||
|
guild_id,
|
||||||
|
description,
|
||||||
|
name,
|
||||||
|
(
|
||||||
|
SELECT JSON_ARRAYAGG(
|
||||||
|
(
|
||||||
|
SELECT JSON_OBJECTAGG(t2.name, t2.value)
|
||||||
|
FROM JSON_TABLE(
|
||||||
|
JSON_ARRAY_APPEND(t1.options, '$', JSON_OBJECT('name', 'command_name', 'value', t1.command_name)),
|
||||||
|
'$[*]' COLUMNS (
|
||||||
|
name VARCHAR(64) PATH '$.name' ERROR ON ERROR,
|
||||||
|
value TEXT PATH '$.value' ERROR ON ERROR
|
||||||
|
)) AS t2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FROM macro m2
|
||||||
|
JOIN JSON_TABLE(
|
||||||
|
commands,
|
||||||
|
'$[*]' COLUMNS (
|
||||||
|
command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
|
||||||
|
options JSON PATH '$.options' ERROR ON ERROR
|
||||||
|
)) AS t1
|
||||||
|
WHERE m1.id = m2.id
|
||||||
|
)
|
||||||
|
FROM macro m1;
|
||||||
|
|
||||||
|
# # Check which commands are used in macros
|
||||||
|
# SELECT DISTINCT command_name
|
||||||
|
# FROM macro m2
|
||||||
|
# JOIN JSON_TABLE(
|
||||||
|
# commands,
|
||||||
|
# '$[*]' COLUMNS (
|
||||||
|
# command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
|
||||||
|
# options JSON PATH '$.options' ERROR ON ERROR
|
||||||
|
# )) AS t1
|
5
migrations/20240303125837_add_indexes.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE reminders
|
||||||
|
ADD INDEX `utc_time_index` (`utc_time`);
|
||||||
|
ALTER TABLE reminders
|
||||||
|
ADD INDEX `status_index` (`status`);
|
7
models/Cargo.lock
generated
@ -1,7 +0,0 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "models"
|
|
||||||
version = "0.1.0"
|
|
@ -1,8 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "models"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
@ -1,8 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "entity"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
chrono-tz = "^0.6"
|
|
||||||
sea-orm = { version = "^0.8", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"] }
|
|
@ -1,60 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "channel")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
|
||||||
pub id: i64,
|
|
||||||
pub guild_id: Option<i64>,
|
|
||||||
pub nudge: i32,
|
|
||||||
pub webhook_id: Option<i64>,
|
|
||||||
pub webhook_token: Option<String>,
|
|
||||||
pub paused: bool,
|
|
||||||
pub paused_until: Option<DateTimeUtc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::guild::Entity",
|
|
||||||
from = "Column::GuildId",
|
|
||||||
to = "super::guild::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
Guild,
|
|
||||||
#[sea_orm(has_many = "super::user::Entity")]
|
|
||||||
User,
|
|
||||||
#[sea_orm(has_many = "super::reminder::Entity")]
|
|
||||||
Reminder,
|
|
||||||
#[sea_orm(has_many = "super::todo::Entity")]
|
|
||||||
Todo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::guild::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Guild.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::user::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::User.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::reminder::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Reminder.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::todo::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Todo.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,34 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "command_macro")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: i32,
|
|
||||||
pub guild_id: i64,
|
|
||||||
pub name: String,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub commands: Option<Json>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::guild::Entity",
|
|
||||||
from = "Column::GuildId",
|
|
||||||
to = "super::guild::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
Guild,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::guild::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Guild.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,48 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "guild")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
|
||||||
pub id: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(has_many = "super::reminder_template::Entity")]
|
|
||||||
ReminderTemplate,
|
|
||||||
#[sea_orm(has_many = "super::channel::Entity")]
|
|
||||||
Channel,
|
|
||||||
#[sea_orm(has_many = "super::todo::Entity")]
|
|
||||||
Todo,
|
|
||||||
#[sea_orm(has_many = "super::command_macro::Entity")]
|
|
||||||
CommandMacro,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::reminder_template::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::ReminderTemplate.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::channel::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Channel.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::todo::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Todo.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::command_macro::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::CommandMacro.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1 +0,0 @@
|
|||||||
|
|
@ -1,14 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
pub mod prelude;
|
|
||||||
|
|
||||||
pub mod channel;
|
|
||||||
pub mod command_macro;
|
|
||||||
pub mod guild;
|
|
||||||
pub mod reminder;
|
|
||||||
pub mod reminder_template;
|
|
||||||
pub mod sea_orm_active_enums;
|
|
||||||
pub mod seaql_migrations;
|
|
||||||
pub mod timer;
|
|
||||||
pub mod todo;
|
|
||||||
pub mod user;
|
|
@ -1,8 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
pub use super::{
|
|
||||||
channel::Entity as Channel, command_macro::Entity as CommandMacro, guild::Entity as Guild,
|
|
||||||
reminder::Entity as Reminder, reminder_template::Entity as ReminderTemplate,
|
|
||||||
seaql_migrations::Entity as SeaqlMigrations, timer::Entity as Timer, todo::Entity as Todo,
|
|
||||||
user::Entity as User,
|
|
||||||
};
|
|
@ -1,73 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
use super::sea_orm_active_enums::Timezone;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "reminder")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: i32,
|
|
||||||
pub uid: String,
|
|
||||||
pub name: String,
|
|
||||||
pub channel_id: i64,
|
|
||||||
pub utc_time: DateTimeUtc,
|
|
||||||
pub timezone: Timezone,
|
|
||||||
pub interval_seconds: Option<i32>,
|
|
||||||
pub interval_months: Option<i32>,
|
|
||||||
pub enabled: bool,
|
|
||||||
pub expires: Option<DateTimeUtc>,
|
|
||||||
pub username: Option<String>,
|
|
||||||
pub avatar: Option<String>,
|
|
||||||
pub content: Option<String>,
|
|
||||||
pub tts: bool,
|
|
||||||
pub attachment: Option<Vec<u8>>,
|
|
||||||
pub attachment_name: Option<String>,
|
|
||||||
pub embed_title: Option<String>,
|
|
||||||
pub embed_description: Option<String>,
|
|
||||||
pub embed_image_url: Option<String>,
|
|
||||||
pub embed_thumbnail_url: Option<String>,
|
|
||||||
pub embed_footer: Option<String>,
|
|
||||||
pub embed_footer_url: Option<String>,
|
|
||||||
pub embed_author: Option<String>,
|
|
||||||
pub embed_author_url: Option<String>,
|
|
||||||
pub embed_color: Option<i32>,
|
|
||||||
pub embed_fields: Option<Json>,
|
|
||||||
pub set_at: DateTimeUtc,
|
|
||||||
pub set_by: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::channel::Entity",
|
|
||||||
from = "Column::ChannelId",
|
|
||||||
to = "super::channel::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
Channel,
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::user::Entity",
|
|
||||||
from = "Column::SetBy",
|
|
||||||
to = "super::user::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
User,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::channel::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Channel.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::user::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::User.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,48 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "reminder_template")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: i32,
|
|
||||||
pub guild_id: i64,
|
|
||||||
pub name: String,
|
|
||||||
pub username: Option<String>,
|
|
||||||
pub avatar: Option<String>,
|
|
||||||
pub content: Option<String>,
|
|
||||||
pub tts: bool,
|
|
||||||
pub attachment: Option<Vec<u8>>,
|
|
||||||
pub attachment_name: Option<String>,
|
|
||||||
pub embed_title: Option<String>,
|
|
||||||
pub embed_description: Option<String>,
|
|
||||||
pub embed_image_url: Option<String>,
|
|
||||||
pub embed_thumbnail_url: Option<String>,
|
|
||||||
pub embed_footer: Option<String>,
|
|
||||||
pub embed_footer_url: Option<String>,
|
|
||||||
pub embed_author: Option<String>,
|
|
||||||
pub embed_author_url: Option<String>,
|
|
||||||
pub embed_color: Option<i32>,
|
|
||||||
pub embed_fields: Option<Json>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::guild::Entity",
|
|
||||||
from = "Column::GuildId",
|
|
||||||
to = "super::guild::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
Guild,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::guild::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Guild.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,22 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "seaql_migrations")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
|
||||||
pub version: String,
|
|
||||||
pub applied_at: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
|
||||||
pub enum Relation {}
|
|
||||||
|
|
||||||
impl RelationTrait for Relation {
|
|
||||||
fn def(&self) -> RelationDef {
|
|
||||||
panic!("No RelationDef")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,36 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "timer")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: i32,
|
|
||||||
pub start_time: DateTimeUtc,
|
|
||||||
pub name: String,
|
|
||||||
pub user_id: Option<i64>,
|
|
||||||
pub guild_id: Option<i64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::guild::Entity",
|
|
||||||
from = "Column::GuildId",
|
|
||||||
to = "super::guild::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
Guild2,
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::guild::Entity",
|
|
||||||
from = "Column::UserId",
|
|
||||||
to = "super::guild::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
Guild1,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,62 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "todo")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: i32,
|
|
||||||
pub user_id: Option<i64>,
|
|
||||||
pub guild_id: Option<i64>,
|
|
||||||
pub channel_id: Option<i64>,
|
|
||||||
pub value: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::channel::Entity",
|
|
||||||
from = "Column::ChannelId",
|
|
||||||
to = "super::channel::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
Channel,
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::guild::Entity",
|
|
||||||
from = "Column::GuildId",
|
|
||||||
to = "super::guild::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
Guild,
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::user::Entity",
|
|
||||||
from = "Column::UserId",
|
|
||||||
to = "super::user::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
User,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::channel::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Channel.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::guild::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Guild.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::user::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::User.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,50 +0,0 @@
|
|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
use super::sea_orm_active_enums::Timezone;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "user")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
|
||||||
pub id: i64,
|
|
||||||
pub dm_channel: i64,
|
|
||||||
pub timezone: Timezone,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::channel::Entity",
|
|
||||||
from = "Column::DmChannel",
|
|
||||||
to = "super::channel::Column::Id",
|
|
||||||
on_update = "NoAction",
|
|
||||||
on_delete = "Cascade"
|
|
||||||
)]
|
|
||||||
Channel,
|
|
||||||
#[sea_orm(has_many = "super::reminder::Entity")]
|
|
||||||
Reminder,
|
|
||||||
#[sea_orm(has_many = "super::todo::Entity")]
|
|
||||||
Todo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::channel::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Channel.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::reminder::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Reminder.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::todo::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Todo.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
2400
models/migration/Cargo.lock
generated
@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "migration"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "migration"
|
|
||||||
path = "src/lib.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
entity = { path = "../entity" }
|
|
||||||
chrono-tz = "^0.6"
|
|
||||||
|
|
||||||
[dependencies.sea-orm-migration]
|
|
||||||
version = "^0.8.0"
|
|
@ -1,37 +0,0 @@
|
|||||||
# Running Migrator CLI
|
|
||||||
|
|
||||||
- Apply all pending migrations
|
|
||||||
```sh
|
|
||||||
cargo run
|
|
||||||
```
|
|
||||||
```sh
|
|
||||||
cargo run -- up
|
|
||||||
```
|
|
||||||
- Apply first 10 pending migrations
|
|
||||||
```sh
|
|
||||||
cargo run -- up -n 10
|
|
||||||
```
|
|
||||||
- Rollback last applied migrations
|
|
||||||
```sh
|
|
||||||
cargo run -- down
|
|
||||||
```
|
|
||||||
- Rollback last 10 applied migrations
|
|
||||||
```sh
|
|
||||||
cargo run -- down -n 10
|
|
||||||
```
|
|
||||||
- Drop all tables from the database, then reapply all migrations
|
|
||||||
```sh
|
|
||||||
cargo run -- fresh
|
|
||||||
```
|
|
||||||
- Rollback all applied migrations, then reapply all migrations
|
|
||||||
```sh
|
|
||||||
cargo run -- refresh
|
|
||||||
```
|
|
||||||
- Rollback all applied migrations
|
|
||||||
```sh
|
|
||||||
cargo run -- reset
|
|
||||||
```
|
|
||||||
- Check the status of all migrations
|
|
||||||
```sh
|
|
||||||
cargo run -- status
|
|
||||||
```
|
|
@ -1,12 +0,0 @@
|
|||||||
pub use sea_orm_migration::prelude::*;
|
|
||||||
|
|
||||||
mod m20220101_000001_create_table;
|
|
||||||
|
|
||||||
pub struct Migrator;
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl MigratorTrait for Migrator {
|
|
||||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
|
||||||
vec![Box::new(m20220101_000001_create_table::Migration)]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,553 +0,0 @@
|
|||||||
use chrono_tz::{Tz, TZ_VARIANTS};
|
|
||||||
use sea_orm_migration::prelude::*;
|
|
||||||
|
|
||||||
use crate::extension::postgres::Type;
|
|
||||||
|
|
||||||
pub struct Migration;
|
|
||||||
|
|
||||||
impl MigrationName for Migration {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"m20220101_000001_create_table"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Iden)]
|
|
||||||
pub enum Guild {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Iden)]
|
|
||||||
pub enum Channel {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
GuildId,
|
|
||||||
Nudge,
|
|
||||||
WebhookId,
|
|
||||||
WebhookToken,
|
|
||||||
Paused,
|
|
||||||
PausedUntil,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Iden)]
|
|
||||||
pub enum User {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
DmChannel,
|
|
||||||
Timezone,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Iden)]
|
|
||||||
pub enum Reminder {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
Uid,
|
|
||||||
Name,
|
|
||||||
ChannelId,
|
|
||||||
UtcTime,
|
|
||||||
Timezone,
|
|
||||||
IntervalSeconds,
|
|
||||||
IntervalMonths,
|
|
||||||
Enabled,
|
|
||||||
Expires,
|
|
||||||
Username,
|
|
||||||
Avatar,
|
|
||||||
Content,
|
|
||||||
Tts,
|
|
||||||
Attachment,
|
|
||||||
AttachmentName,
|
|
||||||
EmbedTitle,
|
|
||||||
EmbedDescription,
|
|
||||||
EmbedImageUrl,
|
|
||||||
EmbedThumbnailUrl,
|
|
||||||
EmbedFooter,
|
|
||||||
EmbedFooterUrl,
|
|
||||||
EmbedAuthor,
|
|
||||||
EmbedAuthorUrl,
|
|
||||||
EmbedColor,
|
|
||||||
EmbedFields,
|
|
||||||
SetAt,
|
|
||||||
SetBy,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Iden)]
|
|
||||||
pub enum ReminderTemplate {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
GuildId,
|
|
||||||
Name,
|
|
||||||
Username,
|
|
||||||
Avatar,
|
|
||||||
Content,
|
|
||||||
Tts,
|
|
||||||
Attachment,
|
|
||||||
AttachmentName,
|
|
||||||
EmbedTitle,
|
|
||||||
EmbedDescription,
|
|
||||||
EmbedImageUrl,
|
|
||||||
EmbedThumbnailUrl,
|
|
||||||
EmbedFooter,
|
|
||||||
EmbedFooterUrl,
|
|
||||||
EmbedAuthor,
|
|
||||||
EmbedAuthorUrl,
|
|
||||||
EmbedColor,
|
|
||||||
EmbedFields,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Iden)]
|
|
||||||
pub enum Timer {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
StartTime,
|
|
||||||
Name,
|
|
||||||
UserId,
|
|
||||||
GuildId,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Iden)]
|
|
||||||
pub enum Todo {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
UserId,
|
|
||||||
GuildId,
|
|
||||||
ChannelId,
|
|
||||||
Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Iden)]
|
|
||||||
pub enum CommandMacro {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
GuildId,
|
|
||||||
Name,
|
|
||||||
Description,
|
|
||||||
Commands,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Timezone {
|
|
||||||
Type,
|
|
||||||
Tz(Tz),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Iden for Timezone {
|
|
||||||
fn unquoted(&self, s: &mut dyn Write) {
|
|
||||||
write!(
|
|
||||||
s,
|
|
||||||
"{}",
|
|
||||||
match self {
|
|
||||||
Self::Type => "timezone".to_string(),
|
|
||||||
Self::Tz(tz) => tz.to_string(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl MigrationTrait for Migration {
|
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
manager
|
|
||||||
.create_type(
|
|
||||||
Type::create()
|
|
||||||
.as_enum(Timezone::Type)
|
|
||||||
.values(TZ_VARIANTS.iter().map(|tz| Timezone::Tz(tz.to_owned())))
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
Table::create()
|
|
||||||
.table(Guild::Table)
|
|
||||||
.if_not_exists()
|
|
||||||
.col(ColumnDef::new(Guild::Id).big_integer().not_null().primary_key())
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
Table::create()
|
|
||||||
.table(Channel::Table)
|
|
||||||
.if_not_exists()
|
|
||||||
.col(ColumnDef::new(Channel::Id).big_integer().not_null().primary_key())
|
|
||||||
.col(ColumnDef::new(Channel::GuildId).big_integer())
|
|
||||||
.col(ColumnDef::new(Channel::Nudge).integer().not_null().default(0))
|
|
||||||
.col(ColumnDef::new(Channel::WebhookId).big_integer())
|
|
||||||
.col(ColumnDef::new(Channel::WebhookToken).string())
|
|
||||||
.col(ColumnDef::new(Channel::Paused).boolean().not_null().default(false))
|
|
||||||
.col(ColumnDef::new(Channel::PausedUntil).date_time())
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_channel_guild")
|
|
||||||
.from(Channel::Table, Channel::GuildId)
|
|
||||||
.to(Guild::Table, Guild::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
Table::create()
|
|
||||||
.table(User::Table)
|
|
||||||
.if_not_exists()
|
|
||||||
.col(ColumnDef::new(User::Id).big_integer().not_null().primary_key())
|
|
||||||
.col(ColumnDef::new(User::DmChannel).big_integer().not_null())
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(User::Timezone)
|
|
||||||
.custom(Timezone::Type)
|
|
||||||
.not_null()
|
|
||||||
.default("UTC"),
|
|
||||||
)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_user_channel")
|
|
||||||
.from(User::Table, User::DmChannel)
|
|
||||||
.to(Channel::Table, Channel::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
Table::create()
|
|
||||||
.table(Reminder::Table)
|
|
||||||
.if_not_exists()
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(Reminder::Id)
|
|
||||||
.integer()
|
|
||||||
.not_null()
|
|
||||||
.auto_increment()
|
|
||||||
.primary_key(),
|
|
||||||
)
|
|
||||||
.col(ColumnDef::new(Reminder::Uid).string().char_len(64).not_null())
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(Reminder::Name)
|
|
||||||
.string()
|
|
||||||
.char_len(24)
|
|
||||||
.default("Reminder")
|
|
||||||
.not_null(),
|
|
||||||
)
|
|
||||||
.col(ColumnDef::new(Reminder::ChannelId).big_integer().not_null())
|
|
||||||
.col(ColumnDef::new(Reminder::UtcTime).date_time().not_null())
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(Reminder::Timezone)
|
|
||||||
.custom(Timezone::Type)
|
|
||||||
.not_null()
|
|
||||||
.default("UTC"),
|
|
||||||
)
|
|
||||||
.col(ColumnDef::new(Reminder::IntervalSeconds).integer())
|
|
||||||
.col(ColumnDef::new(Reminder::IntervalMonths).integer())
|
|
||||||
.col(ColumnDef::new(Reminder::Enabled).boolean().not_null().default(false))
|
|
||||||
.col(ColumnDef::new(Reminder::Expires).date_time())
|
|
||||||
.col(ColumnDef::new(Reminder::Username).string_len(32))
|
|
||||||
.col(ColumnDef::new(Reminder::Avatar).string_len(512))
|
|
||||||
.col(ColumnDef::new(Reminder::Content).string_len(2000))
|
|
||||||
.col(ColumnDef::new(Reminder::Tts).boolean().not_null().default(false))
|
|
||||||
.col(ColumnDef::new(Reminder::Attachment).binary_len(8 * 1024 * 1024))
|
|
||||||
.col(ColumnDef::new(Reminder::AttachmentName).string_len(260))
|
|
||||||
.col(ColumnDef::new(Reminder::EmbedTitle).string_len(256))
|
|
||||||
.col(ColumnDef::new(Reminder::EmbedDescription).string_len(4096))
|
|
||||||
.col(ColumnDef::new(Reminder::EmbedImageUrl).string_len(500))
|
|
||||||
.col(ColumnDef::new(Reminder::EmbedThumbnailUrl).string_len(500))
|
|
||||||
.col(ColumnDef::new(Reminder::EmbedFooter).string_len(2048))
|
|
||||||
.col(ColumnDef::new(Reminder::EmbedFooterUrl).string_len(500))
|
|
||||||
.col(ColumnDef::new(Reminder::EmbedAuthor).string_len(256))
|
|
||||||
.col(ColumnDef::new(Reminder::EmbedAuthorUrl).string_len(500))
|
|
||||||
.col(ColumnDef::new(Reminder::EmbedColor).integer())
|
|
||||||
.col(ColumnDef::new(Reminder::EmbedFields).json())
|
|
||||||
.col(ColumnDef::new(Reminder::SetAt).date_time().not_null().default("NOW()"))
|
|
||||||
.col(ColumnDef::new(Reminder::SetBy).big_integer().not_null())
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_reminder_channel")
|
|
||||||
.from(Reminder::Table, Reminder::ChannelId)
|
|
||||||
.to(Channel::Table, Channel::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_reminder_user")
|
|
||||||
.from(Reminder::Table, Reminder::SetBy)
|
|
||||||
.to(User::Table, User::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
Table::create()
|
|
||||||
.table(ReminderTemplate::Table)
|
|
||||||
.if_not_exists()
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(ReminderTemplate::Id)
|
|
||||||
.integer()
|
|
||||||
.not_null()
|
|
||||||
.auto_increment()
|
|
||||||
.primary_key(),
|
|
||||||
)
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::GuildId).big_integer().not_null())
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(ReminderTemplate::Name)
|
|
||||||
.string()
|
|
||||||
.char_len(24)
|
|
||||||
.default("Reminder")
|
|
||||||
.not_null(),
|
|
||||||
)
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::Username).string_len(32))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::Avatar).string_len(512))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::Content).string_len(2000))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::Tts).boolean().not_null().default(false))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::Attachment).binary_len(8 * 1024 * 1024))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::AttachmentName).string_len(260))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::EmbedTitle).string_len(256))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::EmbedDescription).string_len(4096))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::EmbedImageUrl).string_len(500))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::EmbedThumbnailUrl).string_len(500))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::EmbedFooter).string_len(2048))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::EmbedFooterUrl).string_len(500))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::EmbedAuthor).string_len(256))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::EmbedAuthorUrl).string_len(500))
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::EmbedColor).integer())
|
|
||||||
.col(ColumnDef::new(ReminderTemplate::EmbedFields).json())
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_reminder_template_guild")
|
|
||||||
.from(ReminderTemplate::Table, ReminderTemplate::GuildId)
|
|
||||||
.to(Guild::Table, Guild::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
Table::create()
|
|
||||||
.table(Timer::Table)
|
|
||||||
.if_not_exists()
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(Timer::Id)
|
|
||||||
.integer()
|
|
||||||
.not_null()
|
|
||||||
.auto_increment()
|
|
||||||
.primary_key(),
|
|
||||||
)
|
|
||||||
.col(ColumnDef::new(Timer::StartTime).date_time().not_null().default("NOW()"))
|
|
||||||
.col(ColumnDef::new(Timer::Name).string_len(32).not_null().default("Timer"))
|
|
||||||
.col(ColumnDef::new(Timer::UserId).big_integer())
|
|
||||||
.col(ColumnDef::new(Timer::GuildId).big_integer())
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_timer_user")
|
|
||||||
.from(Timer::Table, Timer::UserId)
|
|
||||||
.to(Guild::Table, Guild::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_timer_guild")
|
|
||||||
.from(Timer::Table, Timer::GuildId)
|
|
||||||
.to(Guild::Table, Guild::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
Table::create()
|
|
||||||
.table(Todo::Table)
|
|
||||||
.if_not_exists()
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(Todo::Id)
|
|
||||||
.integer()
|
|
||||||
.not_null()
|
|
||||||
.auto_increment()
|
|
||||||
.primary_key(),
|
|
||||||
)
|
|
||||||
.col(ColumnDef::new(Todo::UserId).big_integer())
|
|
||||||
.col(ColumnDef::new(Todo::GuildId).big_integer())
|
|
||||||
.col(ColumnDef::new(Todo::ChannelId).big_integer())
|
|
||||||
.col(ColumnDef::new(Todo::Value).string_len(2000).not_null())
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_todo_user")
|
|
||||||
.from(Todo::Table, Todo::UserId)
|
|
||||||
.to(User::Table, User::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_todo_guild")
|
|
||||||
.from(Todo::Table, Todo::GuildId)
|
|
||||||
.to(Guild::Table, Guild::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_todo_channel")
|
|
||||||
.from(Todo::Table, Todo::ChannelId)
|
|
||||||
.to(Channel::Table, Channel::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
Table::create()
|
|
||||||
.table(CommandMacro::Table)
|
|
||||||
.if_not_exists()
|
|
||||||
.col(
|
|
||||||
ColumnDef::new(CommandMacro::Id)
|
|
||||||
.integer()
|
|
||||||
.not_null()
|
|
||||||
.auto_increment()
|
|
||||||
.primary_key(),
|
|
||||||
)
|
|
||||||
.col(ColumnDef::new(CommandMacro::GuildId).big_integer().not_null())
|
|
||||||
.col(ColumnDef::new(CommandMacro::Name).string_len(100).not_null())
|
|
||||||
.col(ColumnDef::new(CommandMacro::Description).string_len(100))
|
|
||||||
.col(ColumnDef::new(CommandMacro::Commands).json())
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager
|
|
||||||
.create_foreign_key(
|
|
||||||
ForeignKey::create()
|
|
||||||
.name("fk_command_macro_guild")
|
|
||||||
.from(CommandMacro::Table, CommandMacro::GuildId)
|
|
||||||
.to(Guild::Table, Guild::Id)
|
|
||||||
.on_delete(ForeignKeyAction::Cascade)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(
|
|
||||||
ForeignKey::drop().table(Channel::Table).name("fk_channel_guild").to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(
|
|
||||||
ForeignKey::drop().table(User::Table).name("fk_user_channel").to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(
|
|
||||||
ForeignKey::drop().table(Reminder::Table).name("fk_reminder_channel").to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(
|
|
||||||
ForeignKey::drop().table(Reminder::Table).name("fk_reminder_user").to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(
|
|
||||||
ForeignKey::drop()
|
|
||||||
.table(ReminderTemplate::Table)
|
|
||||||
.name("fk_reminder_template_guild")
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(
|
|
||||||
ForeignKey::drop().table(Timer::Table).name("fk_timer_user").to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(
|
|
||||||
ForeignKey::drop().table(Timer::Table).name("fk_timer_guild").to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(ForeignKey::drop().table(Todo::Table).name("fk_todo_user").to_owned())
|
|
||||||
.await?;
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(
|
|
||||||
ForeignKey::drop().table(Todo::Table).name("fk_todo_guild").to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(
|
|
||||||
ForeignKey::drop().table(Todo::Table).name("fk_todo_channel").to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
manager
|
|
||||||
.drop_foreign_key(
|
|
||||||
ForeignKey::drop()
|
|
||||||
.table(CommandMacro::Table)
|
|
||||||
.name("fk_command_macro_guild")
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
manager.drop_table(Table::drop().table(Guild::Table).to_owned()).await?;
|
|
||||||
manager.drop_table(Table::drop().table(Channel::Table).to_owned()).await?;
|
|
||||||
manager.drop_table(Table::drop().table(User::Table).to_owned()).await?;
|
|
||||||
manager.drop_table(Table::drop().table(Reminder::Table).to_owned()).await?;
|
|
||||||
manager.drop_table(Table::drop().table(ReminderTemplate::Table).to_owned()).await?;
|
|
||||||
manager.drop_table(Table::drop().table(Timer::Table).to_owned()).await?;
|
|
||||||
manager.drop_table(Table::drop().table(Todo::Table).to_owned()).await?;
|
|
||||||
manager.drop_table(Table::drop().table(CommandMacro::Table).to_owned()).await?;
|
|
||||||
|
|
||||||
manager.drop_type(Type::drop().name(Timezone::Type).to_owned()).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
use sea_orm_migration::prelude::*;
|
|
||||||
|
|
||||||
#[async_std::main]
|
|
||||||
async fn main() {
|
|
||||||
cli::run_cli(migration::Migrator).await;
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
|
|
41
nginx/reminder-rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
server {
|
||||||
|
server_name www.reminder-bot.com;
|
||||||
|
|
||||||
|
return 301 $scheme://reminder-bot.com$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name reminder-bot.com;
|
||||||
|
|
||||||
|
return 301 https://reminder-bot.com$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name reminder-bot.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log;
|
||||||
|
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 4 256k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:18920;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static {
|
||||||
|
alias /var/www/reminder-rs/static;
|
||||||
|
expires 30d;
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "postman"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tokio = { version = "1", features = ["process", "full"] }
|
|
||||||
regex = "1.4"
|
|
||||||
log = "0.4"
|
|
||||||
env_logger = "0.8"
|
|
||||||
chrono = "0.4"
|
|
||||||
chrono-tz = { version = "0.5", features = ["serde"] }
|
|
||||||
lazy_static = "1.4"
|
|
||||||
num-integer = "0.1"
|
|
||||||
serde = "1.0"
|
|
||||||
serde_json = "1.0"
|
|
||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
|
|
||||||
serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
|
|
@ -1,595 +0,0 @@
|
|||||||
use chrono::Duration;
|
|
||||||
use chrono_tz::Tz;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use log::{error, info, warn};
|
|
||||||
use num_integer::Integer;
|
|
||||||
use regex::{Captures, Regex};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serenity::{
|
|
||||||
builder::CreateEmbed,
|
|
||||||
http::{CacheHttp, Http, HttpError, StatusCode},
|
|
||||||
model::{
|
|
||||||
channel::{Channel, Embed as SerenityEmbed},
|
|
||||||
id::ChannelId,
|
|
||||||
webhook::Webhook,
|
|
||||||
},
|
|
||||||
Error, Result,
|
|
||||||
};
|
|
||||||
use sqlx::{
|
|
||||||
types::{
|
|
||||||
chrono::{NaiveDateTime, Utc},
|
|
||||||
Json,
|
|
||||||
},
|
|
||||||
Executor,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::Database;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref TIMEFROM_REGEX: Regex =
|
|
||||||
Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
|
|
||||||
pub static ref TIMENOW_REGEX: Regex =
|
|
||||||
Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fmt_displacement(format: &str, seconds: u64) -> String {
|
|
||||||
let mut seconds = seconds;
|
|
||||||
let mut days: u64 = 0;
|
|
||||||
let mut hours: u64 = 0;
|
|
||||||
let mut minutes: u64 = 0;
|
|
||||||
|
|
||||||
for (rep, time_type, div) in
|
|
||||||
[("%d", &mut days, 86400), ("%h", &mut hours, 3600), ("%m", &mut minutes, 60)].iter_mut()
|
|
||||||
{
|
|
||||||
if format.contains(*rep) {
|
|
||||||
let (divided, new_seconds) = seconds.div_rem(&div);
|
|
||||||
|
|
||||||
**time_type = divided;
|
|
||||||
seconds = new_seconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format
|
|
||||||
.replace("%s", &seconds.to_string())
|
|
||||||
.replace("%m", &minutes.to_string())
|
|
||||||
.replace("%h", &hours.to_string())
|
|
||||||
.replace("%d", &days.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn substitute(string: &str) -> String {
|
|
||||||
let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
|
|
||||||
let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
|
|
||||||
let format = caps.name("format").map(|m| m.as_str());
|
|
||||||
|
|
||||||
if let (Some(final_time), Some(format)) = (final_time, format) {
|
|
||||||
let dt = NaiveDateTime::from_timestamp(final_time, 0);
|
|
||||||
let now = Utc::now().naive_utc();
|
|
||||||
|
|
||||||
let difference = {
|
|
||||||
if now < dt {
|
|
||||||
dt - Utc::now().naive_utc()
|
|
||||||
} else {
|
|
||||||
Utc::now().naive_utc() - dt
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fmt_displacement(format, difference.num_seconds() as u64)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
TIMENOW_REGEX
|
|
||||||
.replace(&new, |caps: &Captures| {
|
|
||||||
let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
|
|
||||||
let format = caps.name("format").map(|m| m.as_str());
|
|
||||||
|
|
||||||
if let (Some(timezone), Some(format)) = (timezone, format) {
|
|
||||||
let now = Utc::now().with_timezone(&timezone);
|
|
||||||
|
|
||||||
now.format(format).to_string()
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Embed {
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
image_url: Option<String>,
|
|
||||||
thumbnail_url: Option<String>,
|
|
||||||
footer: String,
|
|
||||||
footer_url: Option<String>,
|
|
||||||
author: String,
|
|
||||||
author_url: Option<String>,
|
|
||||||
color: u32,
|
|
||||||
fields: Json<Vec<EmbedField>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct EmbedField {
|
|
||||||
title: String,
|
|
||||||
value: String,
|
|
||||||
inline: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Embed {
|
|
||||||
pub async fn from_id(
|
|
||||||
pool: impl Executor<'_, Database = Database> + Copy,
|
|
||||||
id: u32,
|
|
||||||
) -> Option<Self> {
|
|
||||||
match sqlx::query_as!(
|
|
||||||
Self,
|
|
||||||
r#"
|
|
||||||
SELECT
|
|
||||||
`embed_title` AS title,
|
|
||||||
`embed_description` AS description,
|
|
||||||
`embed_image_url` AS image_url,
|
|
||||||
`embed_thumbnail_url` AS thumbnail_url,
|
|
||||||
`embed_footer` AS footer,
|
|
||||||
`embed_footer_url` AS footer_url,
|
|
||||||
`embed_author` AS author,
|
|
||||||
`embed_author_url` AS author_url,
|
|
||||||
`embed_color` AS color,
|
|
||||||
IFNULL(`embed_fields`, '[]') AS "fields:_"
|
|
||||||
FROM reminders
|
|
||||||
WHERE `id` = ?"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(mut embed) => {
|
|
||||||
embed.title = substitute(&embed.title);
|
|
||||||
embed.description = substitute(&embed.description);
|
|
||||||
embed.footer = substitute(&embed.footer);
|
|
||||||
|
|
||||||
embed.fields.iter_mut().for_each(|mut field| {
|
|
||||||
field.title = substitute(&field.title);
|
|
||||||
field.value = substitute(&field.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
if embed.has_content() {
|
|
||||||
Some(embed)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error loading embed from reminder: {:?}", e);
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_content(&self) -> bool {
|
|
||||||
if self.title.is_empty()
|
|
||||||
&& self.description.is_empty()
|
|
||||||
&& self.image_url.is_none()
|
|
||||||
&& self.thumbnail_url.is_none()
|
|
||||||
&& self.footer.is_empty()
|
|
||||||
&& self.footer_url.is_none()
|
|
||||||
&& self.author.is_empty()
|
|
||||||
&& self.author_url.is_none()
|
|
||||||
&& self.fields.0.is_empty()
|
|
||||||
{
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<CreateEmbed> for Embed {
|
|
||||||
fn into(self) -> CreateEmbed {
|
|
||||||
let mut c = CreateEmbed::default();
|
|
||||||
|
|
||||||
c.title(&self.title)
|
|
||||||
.description(&self.description)
|
|
||||||
.color(self.color)
|
|
||||||
.author(|a| {
|
|
||||||
a.name(&self.author);
|
|
||||||
|
|
||||||
if let Some(author_icon) = &self.author_url {
|
|
||||||
a.icon_url(author_icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
a
|
|
||||||
})
|
|
||||||
.footer(|f| {
|
|
||||||
f.text(&self.footer);
|
|
||||||
|
|
||||||
if let Some(footer_icon) = &self.footer_url {
|
|
||||||
f.icon_url(footer_icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
f
|
|
||||||
});
|
|
||||||
|
|
||||||
for field in &self.fields.0 {
|
|
||||||
c.field(&field.title, &field.value, field.inline);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(image_url) = &self.image_url {
|
|
||||||
c.image(image_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(thumbnail_url) = &self.thumbnail_url {
|
|
||||||
c.thumbnail(thumbnail_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Reminder {
|
|
||||||
id: u32,
|
|
||||||
|
|
||||||
channel_id: u64,
|
|
||||||
webhook_id: Option<u64>,
|
|
||||||
webhook_token: Option<String>,
|
|
||||||
|
|
||||||
channel_paused: bool,
|
|
||||||
channel_paused_until: Option<NaiveDateTime>,
|
|
||||||
enabled: bool,
|
|
||||||
|
|
||||||
tts: bool,
|
|
||||||
pin: bool,
|
|
||||||
content: String,
|
|
||||||
attachment: Option<Vec<u8>>,
|
|
||||||
attachment_name: Option<String>,
|
|
||||||
|
|
||||||
utc_time: NaiveDateTime,
|
|
||||||
timezone: String,
|
|
||||||
restartable: bool,
|
|
||||||
expires: Option<NaiveDateTime>,
|
|
||||||
interval_seconds: Option<u32>,
|
|
||||||
interval_months: Option<u32>,
|
|
||||||
|
|
||||||
avatar: Option<String>,
|
|
||||||
username: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Reminder {
|
|
||||||
pub async fn fetch_reminders(pool: impl Executor<'_, Database = Database> + Copy) -> Vec<Self> {
|
|
||||||
match sqlx::query_as_unchecked!(
|
|
||||||
Reminder,
|
|
||||||
r#"
|
|
||||||
SELECT
|
|
||||||
reminders.`id` AS id,
|
|
||||||
|
|
||||||
channels.`channel` AS channel_id,
|
|
||||||
channels.`webhook_id` AS webhook_id,
|
|
||||||
channels.`webhook_token` AS webhook_token,
|
|
||||||
|
|
||||||
channels.`paused` AS 'channel_paused',
|
|
||||||
channels.`paused_until` AS 'channel_paused_until',
|
|
||||||
reminders.`enabled` AS 'enabled',
|
|
||||||
|
|
||||||
reminders.`tts` AS tts,
|
|
||||||
reminders.`pin` AS pin,
|
|
||||||
reminders.`content` AS content,
|
|
||||||
reminders.`attachment` AS attachment,
|
|
||||||
reminders.`attachment_name` AS attachment_name,
|
|
||||||
|
|
||||||
reminders.`utc_time` AS 'utc_time',
|
|
||||||
reminders.`timezone` AS timezone,
|
|
||||||
reminders.`restartable` AS restartable,
|
|
||||||
reminders.`expires` AS 'expires',
|
|
||||||
reminders.`interval_seconds` AS 'interval_seconds',
|
|
||||||
reminders.`interval_months` AS 'interval_months',
|
|
||||||
|
|
||||||
reminders.`avatar` AS avatar,
|
|
||||||
reminders.`username` AS username
|
|
||||||
FROM
|
|
||||||
reminders
|
|
||||||
INNER JOIN
|
|
||||||
channels
|
|
||||||
ON
|
|
||||||
reminders.channel_id = channels.id
|
|
||||||
WHERE
|
|
||||||
reminders.`utc_time` < NOW()
|
|
||||||
LIMIT 25
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(reminders) => reminders
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut rem| {
|
|
||||||
rem.content = substitute(&rem.content);
|
|
||||||
|
|
||||||
rem
|
|
||||||
})
|
|
||||||
.collect::<Vec<Self>>(),
|
|
||||||
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Could not fetch reminders: {:?}", e);
|
|
||||||
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) {
|
|
||||||
let _ = sqlx::query!(
|
|
||||||
"
|
|
||||||
UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?
|
|
||||||
",
|
|
||||||
self.channel_id
|
|
||||||
)
|
|
||||||
.execute(pool)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn refresh(&self, pool: impl Executor<'_, Database = Database> + Copy) {
|
|
||||||
if self.interval_seconds.is_some() || self.interval_months.is_some() {
|
|
||||||
let now = Utc::now().naive_local();
|
|
||||||
let mut updated_reminder_time = self.utc_time;
|
|
||||||
|
|
||||||
if let Some(interval) = self.interval_months {
|
|
||||||
match sqlx::query!(
|
|
||||||
// use the second date_add to force return value to datetime
|
|
||||||
"SELECT DATE_ADD(DATE_ADD(?, INTERVAL ? MONTH), INTERVAL 0 SECOND) AS new_time",
|
|
||||||
updated_reminder_time,
|
|
||||||
interval
|
|
||||||
)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(row) => match row.new_time {
|
|
||||||
Some(datetime) => {
|
|
||||||
updated_reminder_time = datetime;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
warn!("Could not update interval by months: got NULL");
|
|
||||||
|
|
||||||
updated_reminder_time += Duration::days(30);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Could not update interval by months: {:?}", e);
|
|
||||||
|
|
||||||
// naively fallback to adding 30 days
|
|
||||||
updated_reminder_time += Duration::days(30);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(interval) = self.interval_seconds {
|
|
||||||
while updated_reminder_time < now {
|
|
||||||
updated_reminder_time += Duration::seconds(interval as i64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.expires.map_or(false, |expires| {
|
|
||||||
NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires
|
|
||||||
}) {
|
|
||||||
self.force_delete(pool).await;
|
|
||||||
} else {
|
|
||||||
sqlx::query!(
|
|
||||||
"
|
|
||||||
UPDATE reminders SET `utc_time` = ? WHERE `id` = ?
|
|
||||||
",
|
|
||||||
updated_reminder_time,
|
|
||||||
self.id
|
|
||||||
)
|
|
||||||
.execute(pool)
|
|
||||||
.await
|
|
||||||
.expect(&format!("Could not update time on Reminder {}", self.id));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.force_delete(pool).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn force_delete(&self, pool: impl Executor<'_, Database = Database> + Copy) {
|
|
||||||
sqlx::query!(
|
|
||||||
"
|
|
||||||
DELETE FROM reminders WHERE `id` = ?
|
|
||||||
",
|
|
||||||
self.id
|
|
||||||
)
|
|
||||||
.execute(pool)
|
|
||||||
.await
|
|
||||||
.expect(&format!("Could not delete Reminder {}", self.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) {
|
|
||||||
let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send(
|
|
||||||
&self,
|
|
||||||
pool: impl Executor<'_, Database = Database> + Copy,
|
|
||||||
cache_http: impl CacheHttp,
|
|
||||||
) {
|
|
||||||
async fn send_to_channel(
|
|
||||||
cache_http: impl CacheHttp,
|
|
||||||
reminder: &Reminder,
|
|
||||||
embed: Option<CreateEmbed>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await;
|
|
||||||
|
|
||||||
match channel {
|
|
||||||
Ok(Channel::Guild(channel)) => {
|
|
||||||
match channel
|
|
||||||
.send_message(&cache_http, |m| {
|
|
||||||
m.content(&reminder.content).tts(reminder.tts);
|
|
||||||
|
|
||||||
if let (Some(attachment), Some(name)) =
|
|
||||||
(&reminder.attachment, &reminder.attachment_name)
|
|
||||||
{
|
|
||||||
m.add_file((attachment as &[u8], name.as_str()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(embed) = embed {
|
|
||||||
m.set_embed(embed);
|
|
||||||
}
|
|
||||||
|
|
||||||
m
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(m) => {
|
|
||||||
if reminder.pin {
|
|
||||||
reminder.pin_message(m.id, cache_http.http()).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Channel::Private(channel)) => {
|
|
||||||
match channel
|
|
||||||
.send_message(&cache_http.http(), |m| {
|
|
||||||
m.content(&reminder.content).tts(reminder.tts);
|
|
||||||
|
|
||||||
if let (Some(attachment), Some(name)) =
|
|
||||||
(&reminder.attachment, &reminder.attachment_name)
|
|
||||||
{
|
|
||||||
m.add_file((attachment as &[u8], name.as_str()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(embed) = embed {
|
|
||||||
m.set_embed(embed);
|
|
||||||
}
|
|
||||||
|
|
||||||
m
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(m) => {
|
|
||||||
if reminder.pin {
|
|
||||||
reminder.pin_message(m.id, cache_http.http()).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => Err(e),
|
|
||||||
_ => Err(Error::Other("Channel not of valid type")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_to_webhook(
|
|
||||||
cache_http: impl CacheHttp,
|
|
||||||
reminder: &Reminder,
|
|
||||||
webhook: Webhook,
|
|
||||||
embed: Option<CreateEmbed>,
|
|
||||||
) -> Result<()> {
|
|
||||||
match webhook
|
|
||||||
.execute(&cache_http.http(), reminder.pin || reminder.restartable, |w| {
|
|
||||||
w.content(&reminder.content).tts(reminder.tts);
|
|
||||||
|
|
||||||
if let Some(username) = &reminder.username {
|
|
||||||
w.username(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(avatar) = &reminder.avatar {
|
|
||||||
w.avatar_url(avatar);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (Some(attachment), Some(name)) =
|
|
||||||
(&reminder.attachment, &reminder.attachment_name)
|
|
||||||
{
|
|
||||||
w.add_file((attachment as &[u8], name.as_str()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(embed) = embed {
|
|
||||||
w.embeds(vec![SerenityEmbed::fake(|c| {
|
|
||||||
*c = embed;
|
|
||||||
c
|
|
||||||
})]);
|
|
||||||
}
|
|
||||||
|
|
||||||
w
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(m) => {
|
|
||||||
if reminder.pin {
|
|
||||||
if let Some(message) = m {
|
|
||||||
reminder.pin_message(message.id, cache_http.http()).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.enabled
|
|
||||||
&& !(self.channel_paused
|
|
||||||
&& self
|
|
||||||
.channel_paused_until
|
|
||||||
.map_or(true, |inner| inner >= Utc::now().naive_local()))
|
|
||||||
{
|
|
||||||
let _ = sqlx::query!(
|
|
||||||
"
|
|
||||||
UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?
|
|
||||||
",
|
|
||||||
self.channel_id
|
|
||||||
)
|
|
||||||
.execute(pool)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let embed = Embed::from_id(pool, self.id).await.map(|e| e.into());
|
|
||||||
|
|
||||||
let result = if let (Some(webhook_id), Some(webhook_token)) =
|
|
||||||
(self.webhook_id, &self.webhook_token)
|
|
||||||
{
|
|
||||||
let webhook_res =
|
|
||||||
cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await;
|
|
||||||
|
|
||||||
if let Ok(webhook) = webhook_res {
|
|
||||||
send_to_webhook(cache_http, &self, webhook, embed).await
|
|
||||||
} else {
|
|
||||||
warn!("Webhook vanished: {:?}", webhook_res);
|
|
||||||
|
|
||||||
self.reset_webhook(pool).await;
|
|
||||||
send_to_channel(cache_http, &self, embed).await
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
send_to_channel(cache_http, &self, embed).await
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = result {
|
|
||||||
error!("Error sending {:?}: {:?}", self, e);
|
|
||||||
|
|
||||||
if let Error::Http(error) = e {
|
|
||||||
if error.status_code() == Some(StatusCode::NOT_FOUND) {
|
|
||||||
warn!("Seeing channel is deleted. Removing reminder");
|
|
||||||
self.force_delete(pool).await;
|
|
||||||
} else if let HttpError::UnsuccessfulRequest(error) = *error {
|
|
||||||
if error.error.code == 50007 {
|
|
||||||
warn!("User cannot receive DMs");
|
|
||||||
self.force_delete(pool).await;
|
|
||||||
} else {
|
|
||||||
self.refresh(pool).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.refresh(pool).await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.refresh(pool).await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("Reminder {} is paused", self.id);
|
|
||||||
|
|
||||||
self.refresh(pool).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
11
recordable_derive/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "recordable_derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "1.0.35"
|
||||||
|
syn = { version = "2.0.49", features = ["full"] }
|
42
recordable_derive/src/lib.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use syn::{spanned::Spanned, Data};
|
||||||
|
|
||||||
|
/// Macro to allow Recordable to be implemented on an enum by dispatching over each variant.
|
||||||
|
#[proc_macro_derive(Recordable)]
|
||||||
|
pub fn extract(input: TokenStream) -> TokenStream {
|
||||||
|
let ast = syn::parse_macro_input!(input);
|
||||||
|
|
||||||
|
impl_recordable(&ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn impl_recordable(ast: &syn::DeriveInput) -> TokenStream {
|
||||||
|
let name = &ast.ident;
|
||||||
|
|
||||||
|
match &ast.data {
|
||||||
|
Data::Enum(en) => {
|
||||||
|
let extracted = en.variants.iter().map(|var| {
|
||||||
|
let ident = &var.ident;
|
||||||
|
|
||||||
|
quote::quote_spanned! {var.span()=>
|
||||||
|
Self::#ident (opt) => opt.run(ctx).await?
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
TokenStream::from(quote::quote! {
|
||||||
|
impl Recordable for #name {
|
||||||
|
async fn run(self, ctx: crate::Context<'_>) -> Result<(), crate::Error> {
|
||||||
|
match self {
|
||||||
|
#(#extracted,)*
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
panic!("Only enums can derive Recordable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
reminder-dashboard/.prettierrc.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
printWidth = 100
|
||||||
|
tabWidth = 4
|
19
reminder-dashboard/README.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# reminder-dashboard
|
||||||
|
|
||||||
|
The re-re-rewrite of the dashboard.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The existing beta variant of the dashboard is written using vanilla JavaScript. This is fine,
|
||||||
|
but annoying to update. This would've been okay if I was more dedicated to updating the vanilla
|
||||||
|
JavaScript too, but I want to experiment with "new" things.
|
||||||
|
|
||||||
|
This also allows me to expand my frontend skills, which is relevant to part of my job.
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
1. Run both `npm run dev` and `cargo run`
|
||||||
|
2. Symlink assets: assuming cloned
|
||||||
|
into `$HOME`, `ln -s $HOME/reminder-bot/reminder-dashboard/dist/index.html $HOME/reminder-bot/web/static/index.html`
|
||||||
|
and
|
||||||
|
`ln -s $HOME/reminder-bot/reminder-dashboard/dist/static/assets $HOME/reminder-bot/web/static/assets`
|
35
reminder-dashboard/index.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="EN">
|
||||||
|
<head>
|
||||||
|
<meta name="description" content="The most powerful Discord Reminders Bot">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="yandex-verification" content="bb77b8681eb64a90"/>
|
||||||
|
<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho"/>
|
||||||
|
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; font-src fonts.gstatic.com 'self'"> -->
|
||||||
|
|
||||||
|
<!-- favicon -->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180"
|
||||||
|
href="/static/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32"
|
||||||
|
href="/static/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16"
|
||||||
|
href="/static/favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/static/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
|
<title>Reminder Bot | Dashboard</title>
|
||||||
|
|
||||||
|
<!-- styles -->
|
||||||
|
<link rel="stylesheet" href="/static/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/fa.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/font.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/dtsel.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
5467
reminder-dashboard/package-lock.json
generated
Normal file
32
reminder-dashboard/package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite build --watch --mode development",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.5.1",
|
||||||
|
"bulma": "^0.9.4",
|
||||||
|
"luxon": "^3.4.3",
|
||||||
|
"preact": "^10.13.1",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
|
"react-query": "^3.39.3",
|
||||||
|
"tributejs": "^5.1.3",
|
||||||
|
"use-debounce": "^10.0.0",
|
||||||
|
"wouter": "^3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@preact/preset-vite": "^2.5.0",
|
||||||
|
"@types/luxon": "^3.3.2",
|
||||||
|
"eslint": "^8.50.0",
|
||||||
|
"eslint-config-preact": "^1.3.0",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"react-datepicker": "^4.21.0",
|
||||||
|
"sass": "^1.71.1",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.1"
|
||||||
|
}
|
||||||
|
}
|
@ -11,10 +11,26 @@ div.reminderContent.is-collapsed .column.discord-frame {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .collapses {
|
div.reminderContent.is-collapsed .column.settings {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.reminderContent.is-collapsed .reminder-settings {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderContent.is-collapsed .button-row {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderContent.is-collapsed .button-row-edit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderContent.is-collapsed .reminder-topbar {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .invert-collapses {
|
div.reminderContent.is-collapsed .invert-collapses {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
@ -23,42 +39,42 @@ div.reminderContent .invert-collapses {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .settings {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .channel-field {
|
|
||||||
display: inline-flex;
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .reminder-topbar {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
flex-grow: 1;
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.reminderContent.is-collapsed input[name="name"] {
|
div.reminderContent.is-collapsed input[name="name"] {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
border: none;
|
border: none;
|
||||||
font-weight: 700;
|
|
||||||
background: none;
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.reminderContent.is-collapsed button.hide-box {
|
div.reminderContent.is-collapsed .hide-box {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.reminderContent.is-collapsed button.hide-box i {
|
div.reminderContent.is-collapsed .hide-box i {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
/* END */
|
/* END */
|
||||||
|
|
||||||
/* dashboard styles */
|
/* dashboard styles */
|
||||||
|
.hide-box {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-box:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
button.inline-btn {
|
button.inline-btn {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@ -85,18 +101,86 @@ div.discord-embed {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.reminderContent {
|
div.split-controls {
|
||||||
padding: 2px;
|
display: flex;
|
||||||
background-color: #f5f5f5;
|
flex-direction: column;
|
||||||
border-radius: 8px;
|
justify-content: space-between;
|
||||||
margin: 8px;
|
flex-grow: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.interval-group > button {
|
.reminder-topbar > div {
|
||||||
margin-left: auto;
|
padding-left: 6px;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-bar {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-button-bar {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.patreon-only {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tts-row {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-topbar {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-settings {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-settings > .column {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderContent {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Interval inputs */
|
/* Interval inputs */
|
||||||
|
div.interval-group {
|
||||||
|
height: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.interval-group .clear:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.interval-group .no-break {
|
||||||
|
text-wrap: avoid;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.interval-group .clear {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 1px;
|
||||||
|
margin-right: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
div.interval-group > .interval-group-left input {
|
div.interval-group > .interval-group-left input {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
border-style: none;
|
border-style: none;
|
||||||
@ -110,12 +194,13 @@ div.interval-group > .interval-group-left input.w2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.interval-group > .interval-group-left input.w3 {
|
div.interval-group > .interval-group-left input.w3 {
|
||||||
width: 6ch;
|
width: 3ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.interval-group {
|
div.interval-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
/* !Interval inputs */
|
/* !Interval inputs */
|
||||||
|
|
||||||
@ -180,6 +265,23 @@ div#pageNavbar a {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-burger {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-item.pageTitle {
|
||||||
|
flex-shrink: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-burger, .dashboard-burger:active, .dashboard-burger.is-active {
|
||||||
|
background-color: #adc99c !important;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 6px;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
div#pageNavbar a:hover {
|
div#pageNavbar a:hover {
|
||||||
background-color: #4a4a4a;
|
background-color: #4a4a4a;
|
||||||
}
|
}
|
||||||
@ -206,17 +308,24 @@ div.dashboard-sidebar {
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.dashboard-sidebar:not(.mobile-sidebar) {
|
ul.guildList {
|
||||||
display: flex;
|
flex-grow: 1;
|
||||||
flex-direction: column;
|
flex-shrink: 1;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
|
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 226px;
|
width: 226px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.dashboard-sidebar svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
div.mobile-sidebar {
|
div.mobile-sidebar {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@ -293,10 +402,7 @@ input.default-width {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-input:placeholder-shown {
|
.message-input:placeholder-shown {
|
||||||
border-top: none;
|
font-style: italic;
|
||||||
border-left: none;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom-style: dashed;
|
|
||||||
background-color: #40444b;
|
background-color: #40444b;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
@ -367,8 +473,7 @@ input.default-width {
|
|||||||
.customizable.is-400x300 img {
|
.customizable.is-400x300 img {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100px;
|
height: 100px;
|
||||||
max-height: 400px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.customizable.is-32x32 img {
|
.customizable.is-32x32 img {
|
||||||
@ -462,6 +567,7 @@ input.default-width {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.embed-body input, .embed-body textarea {
|
.embed-body input, .embed-body textarea {
|
||||||
@ -511,21 +617,84 @@ input.default-width {
|
|||||||
border-bottom: 1px solid #fff;
|
border-bottom: 1px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
.channel-selector {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.highlight {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row .button-row-reminder {
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row-template {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row .button-row-template > div {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 1023px) {
|
||||||
|
p.title.pageTitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-frame {
|
||||||
|
margin-top: 4rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.customizable.thumbnail img {
|
.customizable.thumbnail img {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.customizable.is-24x24 img {
|
@media only screen and (max-width: 768px) {
|
||||||
width: 16px;
|
.button-row {
|
||||||
height: 16px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row .button-row-reminder {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row .button-row-template > div {
|
||||||
|
flex-basis: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-settings {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tts-row {
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* loader */
|
/* loader */
|
||||||
#loader {
|
#loader {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
@ -537,6 +706,86 @@ input.default-width {
|
|||||||
|
|
||||||
/* END */
|
/* END */
|
||||||
|
|
||||||
|
div.reminderError {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorHead {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorIcon {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorIcon .fas {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="deleted"] .errorIcon {
|
||||||
|
background-color: #e7e5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="failed"] .errorIcon {
|
||||||
|
background-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="sent"] .errorIcon {
|
||||||
|
background-color: #d9f99d;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="sent"] .errorIcon .fas.fa-check {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorHead .reminderName {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgb(54, 54, 54);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorHead .reminderTime {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 1;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgb(54, 54, 54);
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-color: #e5e5e5;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .reminderMessage {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgb(54, 54, 54);
|
||||||
|
flex-grow: 1;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* other stuff */
|
/* other stuff */
|
||||||
|
|
||||||
.half-rem {
|
.half-rem {
|
||||||
@ -568,11 +817,44 @@ input.default-width {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.switch-pane {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guild-submenu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guild-submenu li {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.switch-pane.is-active ~ .guild-submenu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
background-color: #5865F2;
|
||||||
|
}
|
||||||
|
|
||||||
.is-locked {
|
.is-locked {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-locked > :not(.patreon-invert) {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-locked .patreon-invert {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.patreon-invert {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.is-locked .foreground {
|
.is-locked .foreground {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
@ -580,3 +862,27 @@ input.default-width {
|
|||||||
.is-locked .field:last-of-type {
|
.is-locked .field:last-of-type {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box {
|
||||||
|
flex-grow: 1;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #fcfcfc;
|
||||||
|
border-color: #efefef;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
margin: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.figure {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.figure-num {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 323 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
BIN
reminder-dashboard/public/static/img/logo_nobg.webp
Normal file
After Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |