Compare commits
	
		
			1 Commits
		
	
	
		
			next
			...
			jellywx/ma
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e2bf23f194 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -2,4 +2,6 @@ | |||||||
| .env | .env | ||||||
| /venv | /venv | ||||||
| .cargo | .cargo | ||||||
|  | assets | ||||||
|  | out.json | ||||||
| /.idea | /.idea | ||||||
|   | |||||||
							
								
								
									
										2303
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2303
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										47
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -1,22 +1,20 @@ | |||||||
| [package] | [package] | ||||||
| name = "reminder-rs" | name = "reminder_rs" | ||||||
| version = "1.6.40" | version = "1.6.5" | ||||||
| authors = ["Jude Southworth <judesouthworth@pm.me>"] | authors = ["jellywx <judesouthworth@pm.me>"] | ||||||
| edition = "2021" | edition = "2018" | ||||||
| license = "AGPL-3.0 only" |  | ||||||
| description = "Reminder Bot for Discord, now in Rust" |  | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| poise = "0.5" | poise = "0.3" | ||||||
| dotenv = "0.15" | dotenv = "0.15" | ||||||
| tokio = { version = "1", features = ["process", "full"] } | tokio = { version = "1", features = ["process", "full"] } | ||||||
| reqwest = "0.11" | reqwest = "0.11" | ||||||
| lazy-regex = "3.0" | lazy-regex = "2.3.0" | ||||||
| regex = "1.9" | regex = "1.6" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| env_logger = "0.10" | env_logger = "0.9" | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| chrono-tz = { version = "0.8", features = ["serde"] } | chrono-tz = { version = "0.6", features = ["serde"] } | ||||||
| lazy_static = "1.4" | lazy_static = "1.4" | ||||||
| num-integer = "0.1" | num-integer = "0.1" | ||||||
| serde = "1.0" | serde = "1.0" | ||||||
| @@ -25,34 +23,11 @@ serde_repr = "0.1" | |||||||
| rmp-serde = "1.1" | rmp-serde = "1.1" | ||||||
| rand = "0.8" | rand = "0.8" | ||||||
| levenshtein = "1.0" | levenshtein = "1.0" | ||||||
| sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]} | sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]} | ||||||
| base64 = "0.21.0" | base64 = "0.13" | ||||||
|  |  | ||||||
| [dependencies.postman] | [dependencies.postman] | ||||||
| path = "postman" | path = "postman" | ||||||
|  |  | ||||||
| [dependencies.reminder_web] | [dependencies.reminder_web] | ||||||
| path = "web" | path = "web" | ||||||
|  |  | ||||||
| [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"], |  | ||||||
|     ["conf/default.env", "etc/reminder-rs/config.env", "600"], |  | ||||||
|     ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"], |  | ||||||
|     ["web/static/**/*", "lib/reminder-rs/static", "644"], |  | ||||||
|     ["web/templates/**/*", "lib/reminder-rs/templates", "644"], |  | ||||||
|     ["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 |  | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| 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 |  | ||||||
| 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 |  | ||||||
							
								
								
									
										46
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								README.md
									
									
									
									
									
								
							| @@ -7,36 +7,25 @@ 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) | ||||||
|  |  | ||||||
| ### Build APT package | ### Compiling | ||||||
|  | Install build requirements:  | ||||||
|  | `sudo apt install gcc gcc-multilib cmake libssl-dev build-essential` | ||||||
|  |  | ||||||
| Recommended method. | Install Rust from https://rustup.rs | ||||||
|  |  | ||||||
| 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. | Reminder Bot can then be built by running `cargo build --release` in the top level directory. It is necessary to create a  | ||||||
|  | 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. | ||||||
|  |  | ||||||
| 1. Install container software: `sudo apt install podman`. | #### Compilation environment variables | ||||||
| 2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders` | These environment variables must be provided when compiling the bot | ||||||
| 3. Install SQLx CLI: `cargo install sqlx-cli` | * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`) | ||||||
| 4. From the source code directory, execute `sqlx migrate run` | * `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** | ||||||
| 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`  |  | ||||||
|  |  | ||||||
|  | ### 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 | ||||||
|  |  | ||||||
| ### Compiling for other target | ### Environment Variables | ||||||
|  |  | ||||||
| 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`) |  | ||||||
| 8. Build: `cargo build --release` |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### 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__ | ||||||
| @@ -48,5 +37,10 @@ __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 `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else | * `PYTHON_LOCATION` - default `venv/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 | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								Rocket.toml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Rocket.toml
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| [default] | [default] | ||||||
| address = "0.0.0.0" | address = "0.0.0.0" | ||||||
| port = 18920 | port = 5000 | ||||||
| template_dir = "web/templates" | template_dir = "web/templates" | ||||||
| limits = { json = "10MiB" } | limits = { json = "10MiB" } | ||||||
|  |  | ||||||
| @@ -11,18 +11,18 @@ secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY=" | |||||||
| certs = "web/private/rsa_sha256_cert.pem" | certs = "web/private/rsa_sha256_cert.pem" | ||||||
| key = "web/private/rsa_sha256_key.pem" | key = "web/private/rsa_sha256_key.pem" | ||||||
|  |  | ||||||
| [debug.rsa_sha256.tls] | [rsa_sha256.tls] | ||||||
| certs = "web/private/rsa_sha256_cert.pem" | certs = "web/private/rsa_sha256_cert.pem" | ||||||
| key = "web/private/rsa_sha256_key.pem" | key = "web/private/rsa_sha256_key.pem" | ||||||
|  |  | ||||||
| [debug.ecdsa_nistp256_sha256.tls] | [ecdsa_nistp256_sha256.tls] | ||||||
| certs = "web/private/ecdsa_nistp256_sha256_cert.pem" | certs = "web/private/ecdsa_nistp256_sha256_cert.pem" | ||||||
| key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem" | key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem" | ||||||
|  |  | ||||||
| [debug.ecdsa_nistp384_sha384.tls] | [ecdsa_nistp384_sha384.tls] | ||||||
| certs = "web/private/ecdsa_nistp384_sha384_cert.pem" | certs = "web/private/ecdsa_nistp384_sha384_cert.pem" | ||||||
| key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem" | key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem" | ||||||
|  |  | ||||||
| [debug.ed25519.tls] | [ed25519.tls] | ||||||
| certs = "web/private/ed25519_cert.pem" | certs = "web/private/ed25519_cert.pem" | ||||||
| key = "eb/private/ed25519_key.pem" | key = "eb/private/ed25519_key.pem" | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 21 KiB | 
| @@ -1,8 +0,0 @@ | |||||||
| [default] |  | ||||||
| address = "127.0.0.1" |  | ||||||
| port = 18920 |  | ||||||
| template_dir = "/lib/reminder-rs/templates" |  | ||||||
| limits = { json = "10MiB" } |  | ||||||
|  |  | ||||||
| [release] |  | ||||||
| # secret_key = "" |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| 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 +0,0 @@ | |||||||
| */10 * * * * reminder /lib/reminder-rs/healthcheck |  | ||||||
							
								
								
									
										9
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
								
							| @@ -1,9 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
|  |  | ||||||
| set -e |  | ||||||
|  |  | ||||||
| id -u reminder &>/dev/null || useradd -r -M reminder |  | ||||||
|  |  | ||||||
| chown -R reminder /etc/reminder-rs |  | ||||||
|  |  | ||||||
| #DEBHELPER# |  | ||||||
							
								
								
									
										7
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
								
							| @@ -1,7 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
|  |  | ||||||
| set -e |  | ||||||
|  |  | ||||||
| id -u reminder &>/dev/null || userdel reminder |  | ||||||
|  |  | ||||||
| #DEBHELPER# |  | ||||||
							
								
								
									
										13
									
								
								healthcheck
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								healthcheck
									
									
									
									
									
								
							| @@ -1,13 +0,0 @@ | |||||||
| #!/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 |  | ||||||
| @@ -1,6 +1,10 @@ | |||||||
|  | CREATE DATABASE IF NOT EXISTS reminders; | ||||||
|  | 
 | ||||||
| SET FOREIGN_KEY_CHECKS=0; | SET FOREIGN_KEY_CHECKS=0; | ||||||
| 
 | 
 | ||||||
| CREATE TABLE guilds ( | USE reminders; | ||||||
|  | 
 | ||||||
|  | CREATE TABLE reminders.guilds ( | ||||||
|     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, |     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, | ||||||
|     guild BIGINT UNSIGNED UNIQUE NOT NULL, |     guild BIGINT UNSIGNED UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
| @@ -14,10 +18,10 @@ CREATE TABLE guilds ( | |||||||
|     default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' 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), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (default_channel_id) REFERENCES channels(id) ON DELETE SET NULL |     FOREIGN KEY (default_channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE channels ( | CREATE TABLE reminders.channels ( | ||||||
|     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, |     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, | ||||||
|     channel BIGINT UNSIGNED UNIQUE NOT NULL, |     channel BIGINT UNSIGNED UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
| @@ -35,10 +39,10 @@ CREATE TABLE channels ( | |||||||
|     guild_id INT UNSIGNED, |     guild_id INT UNSIGNED, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE |     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE users ( | CREATE TABLE reminders.users ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
|     user BIGINT UNSIGNED UNIQUE NOT NULL, |     user BIGINT UNSIGNED UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
| @@ -55,10 +59,10 @@ CREATE TABLE users ( | |||||||
|     patreon BOOLEAN NOT NULL DEFAULT 0, |     patreon BOOLEAN NOT NULL DEFAULT 0, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (dm_channel) REFERENCES channels(id) ON DELETE RESTRICT |     FOREIGN KEY (dm_channel) REFERENCES reminders.channels(id) ON DELETE RESTRICT | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE roles ( | CREATE TABLE reminders.roles ( | ||||||
|     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, |     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, | ||||||
|     role BIGINT UNSIGNED UNIQUE NOT NULL, |     role BIGINT UNSIGNED UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
| @@ -67,10 +71,10 @@ CREATE TABLE roles ( | |||||||
|     guild_id INT UNSIGNED NOT NULL, |     guild_id INT UNSIGNED NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE |     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE embeds ( | CREATE TABLE reminders.embeds ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
|     title VARCHAR(256) NOT NULL DEFAULT '', |     title VARCHAR(256) NOT NULL DEFAULT '', | ||||||
| @@ -87,7 +91,7 @@ CREATE TABLE embeds ( | |||||||
|     PRIMARY KEY (id) |     PRIMARY KEY (id) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE embed_fields ( | CREATE TABLE reminders.embed_fields ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
|     title VARCHAR(256) NOT NULL DEFAULT '', |     title VARCHAR(256) NOT NULL DEFAULT '', | ||||||
| @@ -96,10 +100,10 @@ CREATE TABLE embed_fields ( | |||||||
|     embed_id INT UNSIGNED NOT NULL, |     embed_id INT UNSIGNED NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE |     FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE CASCADE | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE messages ( | CREATE TABLE reminders.messages ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
|     content VARCHAR(2048) NOT NULL DEFAULT '', |     content VARCHAR(2048) NOT NULL DEFAULT '', | ||||||
| @@ -110,10 +114,10 @@ CREATE TABLE messages ( | |||||||
|     attachment_name VARCHAR(260), |     attachment_name VARCHAR(260), | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL |     FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders ( | CREATE TABLE reminders.reminders ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
|     uid VARCHAR(64) UNIQUE NOT NULL, |     uid VARCHAR(64) UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
| @@ -136,20 +140,20 @@ CREATE TABLE reminders ( | |||||||
|     set_by INT UNSIGNED, |     set_by INT UNSIGNED, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT, |     FOREIGN KEY (message_id) REFERENCES reminders.messages(id) ON DELETE RESTRICT, | ||||||
|     FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, |     FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL |     FOREIGN KEY (set_by) REFERENCES reminders.users(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TRIGGER message_cleanup AFTER DELETE ON reminders | CREATE TRIGGER message_cleanup AFTER DELETE ON reminders.reminders | ||||||
| FOR EACH ROW | FOR EACH ROW | ||||||
|     DELETE FROM messages WHERE id = OLD.message_id; |     DELETE FROM reminders.messages WHERE id = OLD.message_id; | ||||||
| 
 | 
 | ||||||
| CREATE TRIGGER embed_cleanup AFTER DELETE ON messages | CREATE TRIGGER embed_cleanup AFTER DELETE ON reminders.messages | ||||||
| FOR EACH ROW | FOR EACH ROW | ||||||
|     DELETE FROM embeds WHERE id = OLD.embed_id; |     DELETE FROM reminders.embeds WHERE id = OLD.embed_id; | ||||||
| 
 | 
 | ||||||
| CREATE TABLE todos ( | CREATE TABLE reminders.todos ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
|     user_id INT UNSIGNED, |     user_id INT UNSIGNED, | ||||||
|     guild_id INT UNSIGNED, |     guild_id INT UNSIGNED, | ||||||
| @@ -157,23 +161,23 @@ CREATE TABLE todos ( | |||||||
|     value VARCHAR(2000) NOT NULL, |     value VARCHAR(2000) NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, |     FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL, | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL |     FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE command_restrictions ( | CREATE TABLE reminders.command_restrictions ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
|     role_id INT UNSIGNED NOT NULL, |     role_id INT UNSIGNED NOT NULL, | ||||||
|     command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL, |     command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, |     FOREIGN KEY (role_id) REFERENCES reminders.roles(id) ON DELETE CASCADE, | ||||||
|     UNIQUE KEY (`role_id`, `command`) |     UNIQUE KEY (`role_id`, `command`) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE timers ( | CREATE TABLE reminders.timers ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
|     start_time TIMESTAMP NOT NULL DEFAULT NOW(), |     start_time TIMESTAMP NOT NULL DEFAULT NOW(), | ||||||
|     name VARCHAR(32) NOT NULL, |     name VARCHAR(32) NOT NULL, | ||||||
| @@ -182,7 +186,7 @@ CREATE TABLE timers ( | |||||||
|     PRIMARY KEY (id) |     PRIMARY KEY (id) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE events ( | CREATE TABLE reminders.events ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
|     `time` TIMESTAMP NOT NULL DEFAULT NOW(), |     `time` TIMESTAMP NOT NULL DEFAULT NOW(), | ||||||
| 
 | 
 | ||||||
| @@ -194,12 +198,12 @@ CREATE TABLE events ( | |||||||
|     reminder_id INT UNSIGNED, |     reminder_id INT UNSIGNED, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, |     FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL, | ||||||
|     FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL |     FOREIGN KEY (reminder_id) REFERENCES reminders.reminders(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE command_aliases ( | CREATE TABLE reminders.command_aliases ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
|     guild_id INT UNSIGNED NOT NULL, |     guild_id INT UNSIGNED NOT NULL, | ||||||
| @@ -208,22 +212,22 @@ CREATE TABLE command_aliases ( | |||||||
|     command VARCHAR(2048) NOT NULL, |     command VARCHAR(2048) NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, | ||||||
|     UNIQUE KEY (`guild_id`, `name`) |     UNIQUE KEY (`guild_id`, `name`) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE guild_users ( | CREATE TABLE reminders.guild_users ( | ||||||
|     guild INT UNSIGNED NOT NULL, |     guild INT UNSIGNED NOT NULL, | ||||||
|     user INT UNSIGNED NOT NULL, |     user INT UNSIGNED NOT NULL, | ||||||
| 
 | 
 | ||||||
|     can_access BOOL NOT NULL DEFAULT 0, |     can_access BOOL NOT NULL DEFAULT 0, | ||||||
| 
 | 
 | ||||||
|     FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild) REFERENCES reminders.guilds(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE, |     FOREIGN KEY (user) REFERENCES reminders.users(id) ON DELETE CASCADE, | ||||||
|     UNIQUE KEY (guild, user) |     UNIQUE KEY (guild, user) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE EVENT event_cleanup | CREATE EVENT reminders.event_cleanup | ||||||
| ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY | ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY | ||||||
| ON COMPLETION PRESERVE | ON COMPLETION PRESERVE | ||||||
| DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY); | DO DELETE FROM reminders.events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY); | ||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | USE reminders; | ||||||
|  | 
 | ||||||
| SET FOREIGN_KEY_CHECKS = 0; | SET FOREIGN_KEY_CHECKS = 0; | ||||||
| 
 | 
 | ||||||
| DROP TABLE IF EXISTS reminders_new; | DROP TABLE IF EXISTS reminders_new; | ||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | USE reminders; | ||||||
|  | 
 | ||||||
| CREATE TABLE macro ( | CREATE TABLE macro ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT, |     id INT UNSIGNED AUTO_INCREMENT, | ||||||
|     guild_id INT UNSIGNED NOT NULL, |     guild_id INT UNSIGNED NOT NULL, | ||||||
							
								
								
									
										4
									
								
								migration/03-reminder_variable_intervals.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								migration/03-reminder_variable_intervals.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | USE reminders; | ||||||
|  |  | ||||||
|  | ALTER TABLE reminders.reminders RENAME COLUMN `interval` TO `interval_seconds`; | ||||||
|  | ALTER TABLE reminders.reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL; | ||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | USE reminders; | ||||||
|  | 
 | ||||||
| CREATE TABLE reminder_template ( | CREATE TABLE reminder_template ( | ||||||
|     `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, |     `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, | ||||||
| 
 | 
 | ||||||
| @@ -1,2 +0,0 @@ | |||||||
| ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`; |  | ||||||
| ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL; |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| ALTER TABLE reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL; |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| ALTER TABLE reminders ADD COLUMN `thread_id` BIGINT DEFAULT NULL; |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| ALTER TABLE guilds ADD COLUMN ephemeral_confirmations BOOL NOT NULL DEFAULT 0; |  | ||||||
| @@ -1,2 +0,0 @@ | |||||||
| ALTER TABLE reminders MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder'; |  | ||||||
| ALTER TABLE reminder_template MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder'; |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| 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`) |  | ||||||
| ); |  | ||||||
| @@ -1,2 +0,0 @@ | |||||||
| ALTER TABLE reminders ADD COLUMN `status` ENUM ('pending', 'sent', 'failed', 'deleted') NOT NULL DEFAULT 'pending'; |  | ||||||
| ALTER TABLE reminders ADD COLUMN `status_message` TEXT; |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| -- Drop existing constraint |  | ||||||
| ALTER TABLE `reminders` DROP CONSTRAINT `reminders_ibfk_1`; |  | ||||||
|  |  | ||||||
| ALTER TABLE `reminders` MODIFY COLUMN `channel_id` INT UNSIGNED; |  | ||||||
| ALTER TABLE `reminders` ADD COLUMN `guild_id` INT UNSIGNED; |  | ||||||
|  |  | ||||||
| ALTER TABLE `reminders` |  | ||||||
|     ADD CONSTRAINT `guild_id_fk` |  | ||||||
|         FOREIGN KEY (`guild_id`) |  | ||||||
|         REFERENCES `guilds`(`id`) |  | ||||||
|         ON DELETE CASCADE; |  | ||||||
|  |  | ||||||
| ALTER TABLE `reminders` |  | ||||||
|     ADD CONSTRAINT `channel_id_fk` |  | ||||||
|         FOREIGN KEY (`channel_id`) |  | ||||||
|         REFERENCES `channels`(`id`) |  | ||||||
|         ON DELETE SET NULL; |  | ||||||
|  |  | ||||||
| UPDATE `reminders` SET `guild_id` = (SELECT guilds.`id` FROM `channels` INNER JOIN `guilds` ON channels.guild_id = guilds.id WHERE reminders.channel_id = channels.id); |  | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| ALTER TABLE reminders ADD COLUMN `status_change_time` DATETIME; |  | ||||||
|  |  | ||||||
| -- This is a best-guess as to the status change time. |  | ||||||
| UPDATE reminders SET `status_change_time` = `utc_time` WHERE `status` != 'pending'; |  | ||||||
| @@ -1,41 +0,0 @@ | |||||||
| 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; |  | ||||||
|         } |  | ||||||
| } |  | ||||||
| @@ -5,12 +5,12 @@ edition = "2021" | |||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| tokio = { version = "1", features = ["process", "full"] } | tokio = { version = "1", features = ["process", "full"] } | ||||||
| regex = "1.9" | regex = "1.4" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| chrono-tz = { version = "0.8", features = ["serde"] } | chrono-tz = { version = "0.5", features = ["serde"] } | ||||||
| lazy_static = "1.4" | lazy_static = "1.4" | ||||||
| num-integer = "0.1" | num-integer = "0.1" | ||||||
| serde = "1.0" | serde = "1.0" | ||||||
| sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]} | sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]} | ||||||
| serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } | serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } | ||||||
|   | |||||||
| @@ -1,6 +1,4 @@ | |||||||
| use std::env; | use chrono::Duration; | ||||||
|  |  | ||||||
| use chrono::{DateTime, Days, Duration, Months}; |  | ||||||
| use chrono_tz::Tz; | use chrono_tz::Tz; | ||||||
| use lazy_static::lazy_static; | use lazy_static::lazy_static; | ||||||
| use log::{error, info, warn}; | use log::{error, info, warn}; | ||||||
| @@ -9,7 +7,7 @@ use regex::{Captures, Regex}; | |||||||
| use serde::Deserialize; | use serde::Deserialize; | ||||||
| use serenity::{ | use serenity::{ | ||||||
|     builder::CreateEmbed, |     builder::CreateEmbed, | ||||||
|     http::{CacheHttp, Http, HttpError}, |     http::{CacheHttp, Http, HttpError, StatusCode}, | ||||||
|     model::{ |     model::{ | ||||||
|         channel::{Channel, Embed as SerenityEmbed}, |         channel::{Channel, Embed as SerenityEmbed}, | ||||||
|         id::ChannelId, |         id::ChannelId, | ||||||
| @@ -32,7 +30,6 @@ lazy_static! { | |||||||
|         Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap(); |         Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap(); | ||||||
|     pub static ref TIMENOW_REGEX: Regex = |     pub static ref TIMENOW_REGEX: Regex = | ||||||
|         Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap(); |         Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap(); | ||||||
|     pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1"); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| fn fmt_displacement(format: &str, seconds: u64) -> String { | fn fmt_displacement(format: &str, seconds: u64) -> String { | ||||||
| @@ -65,8 +62,7 @@ pub fn substitute(string: &str) -> String { | |||||||
|         let format = caps.name("format").map(|m| m.as_str()); |         let format = caps.name("format").map(|m| m.as_str()); | ||||||
|  |  | ||||||
|         if let (Some(final_time), Some(format)) = (final_time, format) { |         if let (Some(final_time), Some(format)) = (final_time, format) { | ||||||
|             match NaiveDateTime::from_timestamp_opt(final_time, 0) { |             let dt = NaiveDateTime::from_timestamp(final_time, 0); | ||||||
|                 Some(dt) => { |  | ||||||
|             let now = Utc::now().naive_utc(); |             let now = Utc::now().naive_utc(); | ||||||
|  |  | ||||||
|             let difference = { |             let difference = { | ||||||
| @@ -78,10 +74,6 @@ pub fn substitute(string: &str) -> String { | |||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             fmt_displacement(format, difference.num_seconds() as u64) |             fmt_displacement(format, difference.num_seconds() as u64) | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 None => String::new(), |  | ||||||
|             } |  | ||||||
|         } else { |         } else { | ||||||
|             String::new() |             String::new() | ||||||
|         } |         } | ||||||
| @@ -154,7 +146,7 @@ impl Embed { | |||||||
|                 embed.description = substitute(&embed.description); |                 embed.description = substitute(&embed.description); | ||||||
|                 embed.footer = substitute(&embed.footer); |                 embed.footer = substitute(&embed.footer); | ||||||
|  |  | ||||||
|                 embed.fields.iter_mut().for_each(|field| { |                 embed.fields.iter_mut().for_each(|mut field| { | ||||||
|                     field.title = substitute(&field.title); |                     field.title = substitute(&field.title); | ||||||
|                     field.value = substitute(&field.value); |                     field.value = substitute(&field.value); | ||||||
|                 }); |                 }); | ||||||
| @@ -237,11 +229,11 @@ impl Into<CreateEmbed> for Embed { | |||||||
| pub struct Reminder { | pub struct Reminder { | ||||||
|     id: u32, |     id: u32, | ||||||
|  |  | ||||||
|     channel_id: Option<u64>, |     channel_id: u64, | ||||||
|     webhook_id: Option<u64>, |     webhook_id: Option<u64>, | ||||||
|     webhook_token: Option<String>, |     webhook_token: Option<String>, | ||||||
|  |  | ||||||
|     channel_paused: Option<bool>, |     channel_paused: bool, | ||||||
|     channel_paused_until: Option<NaiveDateTime>, |     channel_paused_until: Option<NaiveDateTime>, | ||||||
|     enabled: bool, |     enabled: bool, | ||||||
|  |  | ||||||
| @@ -251,12 +243,11 @@ pub struct Reminder { | |||||||
|     attachment: Option<Vec<u8>>, |     attachment: Option<Vec<u8>>, | ||||||
|     attachment_name: Option<String>, |     attachment_name: Option<String>, | ||||||
|  |  | ||||||
|     utc_time: DateTime<Utc>, |     utc_time: NaiveDateTime, | ||||||
|     timezone: String, |     timezone: String, | ||||||
|     restartable: bool, |     restartable: bool, | ||||||
|     expires: Option<DateTime<Utc>>, |     expires: Option<NaiveDateTime>, | ||||||
|     interval_seconds: Option<u32>, |     interval_seconds: Option<u32>, | ||||||
|     interval_days: Option<u32>, |  | ||||||
|     interval_months: Option<u32>, |     interval_months: Option<u32>, | ||||||
|  |  | ||||||
|     avatar: Option<String>, |     avatar: Option<String>, | ||||||
| @@ -290,31 +281,27 @@ SELECT | |||||||
|     reminders.`restartable` AS restartable, |     reminders.`restartable` AS restartable, | ||||||
|     reminders.`expires` AS 'expires', |     reminders.`expires` AS 'expires', | ||||||
|     reminders.`interval_seconds` AS 'interval_seconds', |     reminders.`interval_seconds` AS 'interval_seconds', | ||||||
|     reminders.`interval_days` AS 'interval_days', |  | ||||||
|     reminders.`interval_months` AS 'interval_months', |     reminders.`interval_months` AS 'interval_months', | ||||||
|  |  | ||||||
|     reminders.`avatar` AS avatar, |     reminders.`avatar` AS avatar, | ||||||
|     reminders.`username` AS username |     reminders.`username` AS username | ||||||
| FROM | FROM | ||||||
|     reminders |     reminders | ||||||
| LEFT JOIN | INNER JOIN | ||||||
|     channels |     channels | ||||||
| ON | ON | ||||||
|     reminders.channel_id = channels.id |     reminders.channel_id = channels.id | ||||||
| WHERE | WHERE | ||||||
|     reminders.`status` = 'pending' AND |  | ||||||
|     reminders.`id` IN ( |     reminders.`id` IN ( | ||||||
|         SELECT |         SELECT | ||||||
|             MIN(id) |             MIN(id) | ||||||
|         FROM |         FROM | ||||||
|             reminders |             reminders | ||||||
|         WHERE |         WHERE | ||||||
|             reminders.`utc_time` <= NOW() AND |             reminders.`utc_time` <= NOW() | ||||||
|             `status` = 'pending' AND |             AND ( | ||||||
|             ( |  | ||||||
|                 reminders.`interval_seconds` IS NOT NULL |                 reminders.`interval_seconds` IS NOT NULL | ||||||
|                 OR reminders.`interval_months` IS NOT NULL |                 OR reminders.`interval_months` IS NOT NULL | ||||||
|                 OR reminders.`interval_days` IS NOT NULL |  | ||||||
|                 OR reminders.enabled |                 OR reminders.enabled | ||||||
|             ) |             ) | ||||||
|         GROUP BY channel_id |         GROUP BY channel_id | ||||||
| @@ -344,8 +331,7 @@ WHERE | |||||||
|     async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) { |     async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||||
|         let _ = sqlx::query!( |         let _ = sqlx::query!( | ||||||
|             " |             " | ||||||
|             UPDATE channels SET webhook_id = NULL, webhook_token = NULL | UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ? | ||||||
|             WHERE channel = ? |  | ||||||
|             ", |             ", | ||||||
|             self.channel_id |             self.channel_id | ||||||
|         ) |         ) | ||||||
| @@ -354,74 +340,56 @@ WHERE | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn refresh(&self, pool: impl Executor<'_, Database = Database> + Copy) { |     async fn refresh(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||||
|         if self.interval_seconds.is_some() |         if self.interval_seconds.is_some() || self.interval_months.is_some() { | ||||||
|             || self.interval_months.is_some() |             let now = Utc::now().naive_local(); | ||||||
|             || self.interval_days.is_some() |             let mut updated_reminder_time = self.utc_time; | ||||||
|         { |  | ||||||
|             // If all intervals are zero then dont care |  | ||||||
|             if self.interval_seconds == Some(0) |  | ||||||
|                 && self.interval_days == Some(0) |  | ||||||
|                 && self.interval_months == Some(0) |  | ||||||
|             { |  | ||||||
|                 self.set_sent(pool).await; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             let now = Utc::now(); |  | ||||||
|             let mut updated_reminder_time = |  | ||||||
|                 self.utc_time.with_timezone(&self.timezone.parse().unwrap_or(Tz::UTC)); |  | ||||||
|             let mut fail_count = 0; |  | ||||||
|  |  | ||||||
|             while updated_reminder_time < now && fail_count < 4 { |  | ||||||
|             if let Some(interval) = self.interval_months { |             if let Some(interval) = self.interval_months { | ||||||
|                     if interval != 0 { |                 match sqlx::query!( | ||||||
|                         updated_reminder_time = updated_reminder_time |                     // use the second date_add to force return value to datetime | ||||||
|                             .checked_add_months(Months::new(interval)) |                     "SELECT DATE_ADD(DATE_ADD(?, INTERVAL ? MONTH), INTERVAL 0 SECOND) AS new_time", | ||||||
|                             .unwrap_or_else(|| { |                     updated_reminder_time, | ||||||
|                                 warn!( |                     interval | ||||||
|                                     "{}: Could not add {} months to a reminder", |                 ) | ||||||
|                                     interval, self.id |                 .fetch_one(pool) | ||||||
|                                 ); |                 .await | ||||||
|                                 fail_count += 1; |                 { | ||||||
|  |                     Ok(row) => match row.new_time { | ||||||
|                                 updated_reminder_time |                         Some(datetime) => { | ||||||
|                             }); |                             updated_reminder_time = datetime; | ||||||
|                         } |                         } | ||||||
|  |                         None => { | ||||||
|  |                             warn!("Could not update interval by months: got NULL"); | ||||||
|  |  | ||||||
|  |                             updated_reminder_time += Duration::days(30); | ||||||
|                         } |                         } | ||||||
|  |                     }, | ||||||
|  |  | ||||||
|                 if let Some(interval) = self.interval_days { |                     Err(e) => { | ||||||
|                     if interval != 0 { |                         warn!("Could not update interval by months: {:?}", e); | ||||||
|                         updated_reminder_time = updated_reminder_time |  | ||||||
|                             .checked_add_days(Days::new(interval as u64)) |  | ||||||
|                             .unwrap_or_else(|| { |  | ||||||
|                                 warn!("{}: Could not add {} days to a reminder", self.id, interval); |  | ||||||
|                                 fail_count += 1; |  | ||||||
|  |  | ||||||
|                                 updated_reminder_time |                         // naively fallback to adding 30 days | ||||||
|                             }) |                         updated_reminder_time += Duration::days(30); | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if let Some(interval) = self.interval_seconds { |             if let Some(interval) = self.interval_seconds { | ||||||
|  |                 while updated_reminder_time < now { | ||||||
|                     updated_reminder_time += Duration::seconds(interval as i64); |                     updated_reminder_time += Duration::seconds(interval as i64); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if fail_count >= 4 { |             if self.expires.map_or(false, |expires| { | ||||||
|                 self.log_error( |                 NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires | ||||||
|                     pool, |             }) { | ||||||
|                     "Failed to update 4 times and so is being deleted", |                 self.force_delete(pool).await; | ||||||
|                     None::<&'static str>, |  | ||||||
|                 ) |  | ||||||
|                 .await; |  | ||||||
|                 self.set_failed(pool, "Failed to update 4 times and so is being deleted").await; |  | ||||||
|             } else if self.expires.map_or(false, |expires| updated_reminder_time > expires) { |  | ||||||
|                 self.set_sent(pool).await; |  | ||||||
|             } else { |             } else { | ||||||
|                 sqlx::query!( |                 sqlx::query!( | ||||||
|                     " |                     " | ||||||
|                     UPDATE reminders SET `utc_time` = ? WHERE `id` = ? | UPDATE reminders SET `utc_time` = ? WHERE `id` = ? | ||||||
|                     ", |                     ", | ||||||
|                     updated_reminder_time.with_timezone(&Utc), |                     updated_reminder_time, | ||||||
|                     self.id |                     self.id | ||||||
|                 ) |                 ) | ||||||
|                 .execute(pool) |                 .execute(pool) | ||||||
| @@ -429,84 +397,15 @@ WHERE | |||||||
|                 .expect(&format!("Could not update time on Reminder {}", self.id)); |                 .expect(&format!("Could not update time on Reminder {}", self.id)); | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             self.set_sent(pool).await; |             self.force_delete(pool).await; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn log_error( |     async fn force_delete(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||||
|         &self, |  | ||||||
|         pool: impl Executor<'_, Database = Database> + Copy, |  | ||||||
|         error: &'static str, |  | ||||||
|         debug_info: Option<impl std::fmt::Debug>, |  | ||||||
|     ) { |  | ||||||
|         let message = match debug_info { |  | ||||||
|             Some(info) => format!( |  | ||||||
|                 "{} |  | ||||||
| {:?}", |  | ||||||
|                 error, info |  | ||||||
|             ), |  | ||||||
|  |  | ||||||
|             None => error.to_string(), |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         error!("[Reminder {}] {}", self.id, message); |  | ||||||
|  |  | ||||||
|         if *LOG_TO_DATABASE { |  | ||||||
|         sqlx::query!( |         sqlx::query!( | ||||||
|             " |             " | ||||||
|                 INSERT INTO stat (type, reminder_id, message) | DELETE FROM reminders WHERE `id` = ? | ||||||
|                 VALUES ('reminder_failed', ?, ?) |  | ||||||
|             ", |             ", | ||||||
|                 self.id, |  | ||||||
|                 message, |  | ||||||
|             ) |  | ||||||
|             .execute(pool) |  | ||||||
|             .await |  | ||||||
|             .expect("Could not log error to database"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn log_success(&self, pool: impl Executor<'_, Database = Database> + Copy) { |  | ||||||
|         if *LOG_TO_DATABASE { |  | ||||||
|             sqlx::query!( |  | ||||||
|                 " |  | ||||||
|                 INSERT INTO stat (type, reminder_id) |  | ||||||
|                 VALUES ('reminder_sent', ?) |  | ||||||
|                 ", |  | ||||||
|                 self.id, |  | ||||||
|             ) |  | ||||||
|             .execute(pool) |  | ||||||
|             .await |  | ||||||
|             .expect("Could not log success to database"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) { |  | ||||||
|         sqlx::query!( |  | ||||||
|             " |  | ||||||
|             UPDATE reminders |  | ||||||
|             SET `status` = 'sent', `status_change_time` = NOW() |  | ||||||
|             WHERE `id` = ? |  | ||||||
|             ", |  | ||||||
|             self.id |  | ||||||
|         ) |  | ||||||
|         .execute(pool) |  | ||||||
|         .await |  | ||||||
|         .expect(&format!("Could not delete Reminder {}", self.id)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn set_failed( |  | ||||||
|         &self, |  | ||||||
|         pool: impl Executor<'_, Database = Database> + Copy, |  | ||||||
|         message: &'static str, |  | ||||||
|     ) { |  | ||||||
|         sqlx::query!( |  | ||||||
|             " |  | ||||||
|             UPDATE reminders |  | ||||||
|             SET `status` = 'failed', `status_message` = ?, `status_change_time` = NOW() |  | ||||||
|             WHERE `id` = ? |  | ||||||
|             ", |  | ||||||
|             message, |  | ||||||
|             self.id |             self.id | ||||||
|         ) |         ) | ||||||
|         .execute(pool) |         .execute(pool) | ||||||
| @@ -515,9 +414,7 @@ WHERE | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) { |     async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) { | ||||||
|         if let Some(channel_id) = self.channel_id { |         let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await; | ||||||
|             let _ = http.as_ref().pin_message(channel_id, message_id.into(), None).await; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub async fn send( |     pub async fn send( | ||||||
| @@ -527,11 +424,10 @@ WHERE | |||||||
|     ) { |     ) { | ||||||
|         async fn send_to_channel( |         async fn send_to_channel( | ||||||
|             cache_http: impl CacheHttp, |             cache_http: impl CacheHttp, | ||||||
|             channel_id: u64, |  | ||||||
|             reminder: &Reminder, |             reminder: &Reminder, | ||||||
|             embed: Option<CreateEmbed>, |             embed: Option<CreateEmbed>, | ||||||
|         ) -> Result<()> { |         ) -> Result<()> { | ||||||
|             let channel = ChannelId(channel_id).to_channel(&cache_http).await; |             let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await; | ||||||
|  |  | ||||||
|             match channel { |             match channel { | ||||||
|                 Ok(Channel::Guild(channel)) => { |                 Ok(Channel::Guild(channel)) => { | ||||||
| @@ -563,7 +459,6 @@ WHERE | |||||||
|                         Err(e) => Err(e), |                         Err(e) => Err(e), | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 Ok(Channel::Private(channel)) => { |                 Ok(Channel::Private(channel)) => { | ||||||
|                     match channel |                     match channel | ||||||
|                         .send_message(&cache_http.http(), |m| { |                         .send_message(&cache_http.http(), |m| { | ||||||
| @@ -593,9 +488,7 @@ WHERE | |||||||
|                         Err(e) => Err(e), |                         Err(e) => Err(e), | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 Err(e) => Err(e), |                 Err(e) => Err(e), | ||||||
|  |  | ||||||
|                 _ => Err(Error::Other("Channel not of valid type")), |                 _ => Err(Error::Other("Channel not of valid type")), | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -611,10 +504,8 @@ WHERE | |||||||
|                     w.content(&reminder.content).tts(reminder.tts); |                     w.content(&reminder.content).tts(reminder.tts); | ||||||
|  |  | ||||||
|                     if let Some(username) = &reminder.username { |                     if let Some(username) = &reminder.username { | ||||||
|                         if !username.is_empty() { |  | ||||||
|                         w.username(username); |                         w.username(username); | ||||||
|                     } |                     } | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     if let Some(avatar) = &reminder.avatar { |                     if let Some(avatar) = &reminder.avatar { | ||||||
|                         w.avatar_url(avatar); |                         w.avatar_url(avatar); | ||||||
| @@ -650,19 +541,15 @@ WHERE | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         match self.channel_id { |  | ||||||
|             Some(channel_id) => { |  | ||||||
|         if self.enabled |         if self.enabled | ||||||
|                     && !(self.channel_paused.unwrap_or(false) |             && !(self.channel_paused | ||||||
|                 && self |                 && self | ||||||
|                     .channel_paused_until |                     .channel_paused_until | ||||||
|                     .map_or(true, |inner| inner >= Utc::now().naive_local())) |                     .map_or(true, |inner| inner >= Utc::now().naive_local())) | ||||||
|         { |         { | ||||||
|             let _ = sqlx::query!( |             let _ = sqlx::query!( | ||||||
|                 " |                 " | ||||||
|                         UPDATE `channels` | UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ? | ||||||
|                         SET paused = 0, paused_until = NULL |  | ||||||
|                         WHERE `channel` = ? |  | ||||||
|                 ", |                 ", | ||||||
|                 self.channel_id |                 self.channel_id | ||||||
|             ) |             ) | ||||||
| @@ -674,112 +561,40 @@ WHERE | |||||||
|             let result = if let (Some(webhook_id), Some(webhook_token)) = |             let result = if let (Some(webhook_id), Some(webhook_token)) = | ||||||
|                 (self.webhook_id, &self.webhook_token) |                 (self.webhook_id, &self.webhook_token) | ||||||
|             { |             { | ||||||
|                         let webhook_res = cache_http |                 let webhook_res = | ||||||
|                             .http() |                     cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await; | ||||||
|                             .get_webhook_with_token(webhook_id, webhook_token) |  | ||||||
|                             .await; |  | ||||||
|  |  | ||||||
|                 if let Ok(webhook) = webhook_res { |                 if let Ok(webhook) = webhook_res { | ||||||
|                     send_to_webhook(cache_http, &self, webhook, embed).await |                     send_to_webhook(cache_http, &self, webhook, embed).await | ||||||
|                 } else { |                 } else { | ||||||
|                             warn!("Webhook vanished for reminder {}: {:?}", self.id, webhook_res); |                     warn!("Webhook vanished: {:?}", webhook_res); | ||||||
|  |  | ||||||
|                     self.reset_webhook(pool).await; |                     self.reset_webhook(pool).await; | ||||||
|                             send_to_channel(cache_http, channel_id, &self, embed).await |                     send_to_channel(cache_http, &self, embed).await | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                         send_to_channel(cache_http, channel_id, &self, embed).await |                 send_to_channel(cache_http, &self, embed).await | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             if let Err(e) = result { |             if let Err(e) = result { | ||||||
|  |                 error!("Error sending reminder {}: {:?}", self.id, e); | ||||||
|  |  | ||||||
|                 if let Error::Http(error) = e { |                 if let Error::Http(error) = e { | ||||||
|                             if let HttpError::UnsuccessfulRequest(http_error) = *error { |                     if error.status_code() == Some(StatusCode::NOT_FOUND) { | ||||||
|                                 match http_error.error.code { |                         warn!("Seeing channel is deleted. Removing reminder"); | ||||||
|                                     10003 => { |                         self.force_delete(pool).await; | ||||||
|                                         self.log_error( |                     } else if let HttpError::UnsuccessfulRequest(error) = *error { | ||||||
|                                             pool, |                         if error.error.code == 50007 { | ||||||
|                                             "Could not be sent as channel does not exist", |                             warn!("User cannot receive DMs"); | ||||||
|                                             None::<&'static str>, |                             self.force_delete(pool).await; | ||||||
|                                         ) |                         } else { | ||||||
|                                         .await; |  | ||||||
|                                         self.set_failed( |  | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as channel does not exist", |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                                     } |  | ||||||
|                                     10004 => { |  | ||||||
|                                         self.log_error( |  | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as guild does not exist", |  | ||||||
|                                             None::<&'static str>, |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                                         self.set_failed( |  | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as guild does not exist", |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                                     } |  | ||||||
|                                     50001 => { |  | ||||||
|                                         self.log_error( |  | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as missing access", |  | ||||||
|                                             None::<&'static str>, |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                                         self.set_failed( |  | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as missing access", |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                                     } |  | ||||||
|                                     50007 => { |  | ||||||
|                                         self.log_error( |  | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as user has DMs disabled", |  | ||||||
|                                             None::<&'static str>, |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                                         self.set_failed( |  | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as user has DMs disabled", |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                                     } |  | ||||||
|                                     50013 => { |  | ||||||
|                                         self.log_error( |  | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as permissions are invalid", |  | ||||||
|                                             None::<&'static str>, |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                                         self.set_failed( |  | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as permissions are invalid", |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                                     } |  | ||||||
|                                     _ => { |  | ||||||
|                                         self.log_error( |  | ||||||
|                                             pool, |  | ||||||
|                                             "HTTP error sending reminder", |  | ||||||
|                                             Some(http_error), |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                             self.refresh(pool).await; |                             self.refresh(pool).await; | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } else { |                 } else { | ||||||
|                                 self.log_error(pool, "(Likely) a parsing error", Some(error)).await; |  | ||||||
|                     self.refresh(pool).await; |                     self.refresh(pool).await; | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                             self.log_error(pool, "Non-HTTP error", Some(e)).await; |  | ||||||
|                             self.refresh(pool).await; |  | ||||||
|                         } |  | ||||||
|                     } else { |  | ||||||
|                         self.log_success(pool).await; |  | ||||||
|                 self.refresh(pool).await; |                 self.refresh(pool).await; | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
| @@ -788,13 +603,4 @@ WHERE | |||||||
|             self.refresh(pool).await; |             self.refresh(pool).await; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|             None => { |  | ||||||
|                 info!("Reminder {} is orphaned", self.id); |  | ||||||
|  |  | ||||||
|                 self.log_error(pool, "Orphaned", Option::<u8>::None).await; |  | ||||||
|                 self.set_failed(pool, "Could not be sent as channel was deleted").await; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,9 +1,6 @@ | |||||||
| use std::time::{SystemTime, UNIX_EPOCH}; |  | ||||||
|  |  | ||||||
| use chrono_tz::TZ_VARIANTS; | use chrono_tz::TZ_VARIANTS; | ||||||
| use poise::AutocompleteChoice; |  | ||||||
|  |  | ||||||
| use crate::{models::CtxData, time_parser::natural_parser, Context}; | use crate::Context; | ||||||
|  |  | ||||||
| pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> { | pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> { | ||||||
|     if partial.is_empty() { |     if partial.is_empty() { | ||||||
| @@ -36,82 +33,3 @@ WHERE | |||||||
|     .map(|s| s.name.clone()) |     .map(|s| s.name.clone()) | ||||||
|     .collect() |     .collect() | ||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn time_hint_autocomplete( |  | ||||||
|     ctx: Context<'_>, |  | ||||||
|     partial: &str, |  | ||||||
| ) -> Vec<AutocompleteChoice<String>> { |  | ||||||
|     if partial.is_empty() { |  | ||||||
|         vec![AutocompleteChoice { |  | ||||||
|             name: "Start typing a time...".to_string(), |  | ||||||
|             value: "now".to_string(), |  | ||||||
|         }] |  | ||||||
|     } else { |  | ||||||
|         match natural_parser(partial, &ctx.timezone().await.to_string()).await { |  | ||||||
|             Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) { |  | ||||||
|                 Ok(now) => { |  | ||||||
|                     let diff = timestamp - now.as_secs() as i64; |  | ||||||
|  |  | ||||||
|                     if diff < 0 { |  | ||||||
|                         vec![AutocompleteChoice { |  | ||||||
|                             name: "Time is in the past".to_string(), |  | ||||||
|                             value: "1 year ago".to_string(), |  | ||||||
|                         }] |  | ||||||
|                     } else { |  | ||||||
|                         if diff > 86400 { |  | ||||||
|                             vec![ |  | ||||||
|                                 AutocompleteChoice { |  | ||||||
|                                     name: partial.to_string(), |  | ||||||
|                                     value: partial.to_string(), |  | ||||||
|                                 }, |  | ||||||
|                                 AutocompleteChoice { |  | ||||||
|                                     name: format!( |  | ||||||
|                                         "In approximately {} days, {} hours", |  | ||||||
|                                         diff / 86400, |  | ||||||
|                                         (diff % 86400) / 3600 |  | ||||||
|                                     ), |  | ||||||
|                                     value: partial.to_string(), |  | ||||||
|                                 }, |  | ||||||
|                             ] |  | ||||||
|                         } else if diff > 3600 { |  | ||||||
|                             vec![ |  | ||||||
|                                 AutocompleteChoice { |  | ||||||
|                                     name: partial.to_string(), |  | ||||||
|                                     value: partial.to_string(), |  | ||||||
|                                 }, |  | ||||||
|                                 AutocompleteChoice { |  | ||||||
|                                     name: format!("In approximately {} hours", diff / 3600), |  | ||||||
|                                     value: partial.to_string(), |  | ||||||
|                                 }, |  | ||||||
|                             ] |  | ||||||
|                         } else { |  | ||||||
|                             vec![ |  | ||||||
|                                 AutocompleteChoice { |  | ||||||
|                                     name: partial.to_string(), |  | ||||||
|                                     value: partial.to_string(), |  | ||||||
|                                 }, |  | ||||||
|                                 AutocompleteChoice { |  | ||||||
|                                     name: format!("In approximately {} minutes", diff / 60), |  | ||||||
|                                     value: partial.to_string(), |  | ||||||
|                                 }, |  | ||||||
|                             ] |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 Err(_) => { |  | ||||||
|                     vec![AutocompleteChoice { |  | ||||||
|                         name: partial.to_string(), |  | ||||||
|                         value: partial.to_string(), |  | ||||||
|                     }] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|  |  | ||||||
|             None => { |  | ||||||
|                 vec![AutocompleteChoice { |  | ||||||
|                     name: "Time not recognised".to_string(), |  | ||||||
|                     value: "now".to_string(), |  | ||||||
|                 }] |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								src/commands/command_macro/install.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/commands/command_macro/install.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | use poise::serenity_prelude::CommandType; | ||||||
|  |  | ||||||
|  | use crate::{ | ||||||
|  |     commands::autocomplete::macro_name_autocomplete, models::command_macro::guild_command_macro, | ||||||
|  |     Context, Error, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /// Add a macro as a slash-command to this server. Enables controlling permissions per-macro. | ||||||
|  | #[poise::command( | ||||||
|  |     slash_command, | ||||||
|  |     rename = "install", | ||||||
|  |     guild_only = true, | ||||||
|  |     default_member_permissions = "MANAGE_GUILD", | ||||||
|  |     identifying_name = "install_macro" | ||||||
|  | )] | ||||||
|  | pub async fn install_macro( | ||||||
|  |     ctx: Context<'_>, | ||||||
|  |     #[description = "Name of macro to install"] | ||||||
|  |     #[autocomplete = "macro_name_autocomplete"] | ||||||
|  |     name: String, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  |     let guild_id = ctx.guild_id().unwrap(); | ||||||
|  |  | ||||||
|  |     if let Some(command_macro) = guild_command_macro(&ctx, &name).await { | ||||||
|  |         guild_id | ||||||
|  |             .create_application_command(&ctx.discord(), |a| { | ||||||
|  |                 a.kind(CommandType::ChatInput) | ||||||
|  |                     .name(command_macro.name) | ||||||
|  |                     .description(command_macro.description.unwrap_or_else(|| "".to_string())) | ||||||
|  |             }) | ||||||
|  |             .await?; | ||||||
|  |         ctx.send(|r| r.ephemeral(true).content("Macro installed. Go to Server Settings 🠚 Integrations 🠚 Reminder Bot to configure permissions.")).await?; | ||||||
|  |     } else { | ||||||
|  |         ctx.send(|r| r.ephemeral(true).content("No macro found with that name")).await?; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
| @@ -2,7 +2,7 @@ use poise::CreateReply; | |||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     component_models::pager::{MacroPager, Pager}, |     component_models::pager::{MacroPager, Pager}, | ||||||
|     consts::THEME_COLOR, |     consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, | ||||||
|     models::{command_macro::CommandMacro, CtxData}, |     models::{command_macro::CommandMacro, CtxData}, | ||||||
|     Context, Error, |     Context, Error, | ||||||
| }; | }; | ||||||
| @@ -30,7 +30,27 @@ pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
| } | } | ||||||
|  |  | ||||||
| pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize { | pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize { | ||||||
|     ((macros.len() as f64) / 25.0).ceil() as usize |     let mut skipped_char_count = 0; | ||||||
|  |  | ||||||
|  |     macros | ||||||
|  |         .iter() | ||||||
|  |         .map(|m| { | ||||||
|  |             if let Some(description) = &m.description { | ||||||
|  |                 format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len()) | ||||||
|  |             } else { | ||||||
|  |                 format!("**{}**\n- Has {} commands", m.name, m.commands.len()) | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         .fold(1, |mut pages, p| { | ||||||
|  |             skipped_char_count += p.len(); | ||||||
|  |  | ||||||
|  |             if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH { | ||||||
|  |                 skipped_char_count = p.len(); | ||||||
|  |                 pages += 1; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             pages | ||||||
|  |         }) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply { | pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply { | ||||||
| @@ -55,27 +75,45 @@ pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> Crea | |||||||
|         page = pages - 1; |         page = pages - 1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let lower = (page * 25).min(macros.len()); |     let mut char_count = 0; | ||||||
|     let upper = ((page + 1) * 25).min(macros.len()); |     let mut skipped_char_count = 0; | ||||||
|  |  | ||||||
|     let fields = macros[lower..upper].iter().map(|m| { |     let mut skipped_pages = 0; | ||||||
|  |  | ||||||
|  |     let display_vec: Vec<String> = macros | ||||||
|  |         .iter() | ||||||
|  |         .map(|m| { | ||||||
|             if let Some(description) = &m.description { |             if let Some(description) = &m.description { | ||||||
|             ( |                 format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len()) | ||||||
|                 m.name.clone(), |  | ||||||
|                 format!("*{}*\n- Has {} commands", description, m.commands.len()), |  | ||||||
|                 true, |  | ||||||
|             ) |  | ||||||
|             } else { |             } else { | ||||||
|             (m.name.clone(), format!("- Has {} commands", m.commands.len()), true) |                 format!("**{}**\n- Has {} commands", m.name, m.commands.len()) | ||||||
|             } |             } | ||||||
|     }); |         }) | ||||||
|  |         .skip_while(|p| { | ||||||
|  |             skipped_char_count += p.len(); | ||||||
|  |  | ||||||
|  |             if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH { | ||||||
|  |                 skipped_char_count = p.len(); | ||||||
|  |                 skipped_pages += 1; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             skipped_pages < page | ||||||
|  |         }) | ||||||
|  |         .take_while(|p| { | ||||||
|  |             char_count += p.len(); | ||||||
|  |  | ||||||
|  |             char_count < EMBED_DESCRIPTION_MAX_LENGTH | ||||||
|  |         }) | ||||||
|  |         .collect::<Vec<String>>(); | ||||||
|  |  | ||||||
|  |     let display = display_vec.join("\n"); | ||||||
|  |  | ||||||
|     let mut reply = CreateReply::default(); |     let mut reply = CreateReply::default(); | ||||||
|  |  | ||||||
|     reply |     reply | ||||||
|         .embed(|e| { |         .embed(|e| { | ||||||
|             e.title("Macros") |             e.title("Macros") | ||||||
|                 .fields(fields) |                 .description(display) | ||||||
|                 .footer(|f| f.text(format!("Page {} of {}", page + 1, pages))) |                 .footer(|f| f.text(format!("Page {} of {}", page + 1, pages))) | ||||||
|                 .color(*THEME_COLOR) |                 .color(*THEME_COLOR) | ||||||
|         }) |         }) | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|         "SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", |         "SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||||
|         guild_id.0 |         guild_id.0 | ||||||
|     ) |     ) | ||||||
|     .fetch_all(&mut *transaction) |     .fetch_all(&mut transaction) | ||||||
|     .await?; |     .await?; | ||||||
|  |  | ||||||
|     let mut added_aliases = 0; |     let mut added_aliases = 0; | ||||||
| @@ -42,7 +42,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|                     cmd_macro.description, |                     cmd_macro.description, | ||||||
|                     cmd_macro.commands |                     cmd_macro.commands | ||||||
|                 ) |                 ) | ||||||
|                 .execute(&mut *transaction) |                 .execute(&mut transaction) | ||||||
|                 .await?; |                 .await?; | ||||||
|  |  | ||||||
|                 added_aliases += 1; |                 added_aliases += 1; | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| use crate::{Context, Error}; | use crate::{Context, Error}; | ||||||
|  |  | ||||||
| pub mod delete; | pub mod delete; | ||||||
|  | pub mod install; | ||||||
| pub mod list; | pub mod list; | ||||||
| pub mod migrate; | pub mod migrate; | ||||||
| pub mod record; | pub mod record; | ||||||
|   | |||||||
| @@ -15,18 +15,6 @@ pub async fn record_macro( | |||||||
|     #[description = "Name for the new macro"] name: String, |     #[description = "Name for the new macro"] name: String, | ||||||
|     #[description = "Description for the new macro"] description: Option<String>, |     #[description = "Description for the new macro"] description: Option<String>, | ||||||
| ) -> Result<(), Error> { | ) -> Result<(), Error> { | ||||||
|     if name.len() > 100 { |  | ||||||
|         ctx.say("Name must be less than 100 characters").await?; |  | ||||||
|  |  | ||||||
|         return Ok(()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if description.as_ref().map_or(0, |d| d.len()) > 100 { |  | ||||||
|         ctx.say("Description must be less than 100 characters").await?; |  | ||||||
|  |  | ||||||
|         return Ok(()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let guild_id = ctx.guild_id().unwrap(); |     let guild_id = ctx.guild_id().unwrap(); | ||||||
|  |  | ||||||
|     let row = sqlx::query!( |     let row = sqlx::query!( | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| use super::super::autocomplete::macro_name_autocomplete; | use super::super::autocomplete::macro_name_autocomplete; | ||||||
| use crate::{models::command_macro::guild_command_macro, Context, Data, Error, THEME_COLOR}; | use crate::{models::command_macro::guild_command_macro, Context, Data, Error}; | ||||||
|  |  | ||||||
| /// Run a recorded macro | /// Run a recorded macro | ||||||
| #[poise::command( | #[poise::command( | ||||||
| @@ -17,17 +17,7 @@ pub async fn run_macro( | |||||||
| ) -> Result<(), Error> { | ) -> Result<(), Error> { | ||||||
|     match guild_command_macro(&Context::Application(ctx), &name).await { |     match guild_command_macro(&Context::Application(ctx), &name).await { | ||||||
|         Some(command_macro) => { |         Some(command_macro) => { | ||||||
|             Context::Application(ctx) |             ctx.defer_response(false).await?; | ||||||
|                 .send(|b| { |  | ||||||
|                     b.embed(|e| { |  | ||||||
|                         e.title("Running Macro").color(*THEME_COLOR).description(format!( |  | ||||||
|                             "Running macro {} ({} commands)", |  | ||||||
|                             command_macro.name, |  | ||||||
|                             command_macro.commands.len() |  | ||||||
|                         )) |  | ||||||
|                     }) |  | ||||||
|                 }) |  | ||||||
|                 .await?; |  | ||||||
|  |  | ||||||
|             for command in command_macro.commands { |             for command in command_macro.commands { | ||||||
|                 if let Some(action) = command.action { |                 if let Some(action) = command.action { | ||||||
|   | |||||||
| @@ -6,8 +6,8 @@ use crate::{models::CtxData, Context, Error, THEME_COLOR}; | |||||||
| fn footer( | fn footer( | ||||||
|     ctx: Context<'_>, |     ctx: Context<'_>, | ||||||
| ) -> impl FnOnce(&mut serenity::CreateEmbedFooter) -> &mut serenity::CreateEmbedFooter { | ) -> impl FnOnce(&mut serenity::CreateEmbedFooter) -> &mut serenity::CreateEmbedFooter { | ||||||
|     let shard_count = ctx.serenity_context().cache.shard_count(); |     let shard_count = ctx.discord().cache.shard_count(); | ||||||
|     let shard = ctx.serenity_context().shard_id; |     let shard = ctx.discord().shard_id; | ||||||
|  |  | ||||||
|     move |f| { |     move |f| { | ||||||
|         f.text(format!( |         f.text(format!( | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| mod autocomplete; | pub mod autocomplete; | ||||||
| pub mod command_macro; | pub mod command_macro; | ||||||
| pub mod info_cmds; | pub mod info_cmds; | ||||||
| pub mod moderation_cmds; | pub mod moderation_cmds; | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| use chrono::offset::Utc; | use chrono::offset::Utc; | ||||||
| use chrono_tz::{Tz, TZ_VARIANTS}; | use chrono_tz::{Tz, TZ_VARIANTS}; | ||||||
| use levenshtein::levenshtein; | use levenshtein::levenshtein; | ||||||
| use log::warn; |  | ||||||
|  |  | ||||||
| use super::autocomplete::timezone_autocomplete; | use super::autocomplete::timezone_autocomplete; | ||||||
| use crate::{consts::THEME_COLOR, models::CtxData, Context, Error}; | use crate::{consts::THEME_COLOR, models::CtxData, Context, Error}; | ||||||
| @@ -102,78 +101,6 @@ You may want to use one of the popular timezones below, otherwise click [here](h | |||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Configure server settings |  | ||||||
| #[poise::command( |  | ||||||
|     slash_command, |  | ||||||
|     rename = "settings", |  | ||||||
|     identifying_name = "settings", |  | ||||||
|     guild_only = true |  | ||||||
| )] |  | ||||||
| pub async fn settings(_ctx: Context<'_>) -> Result<(), Error> { |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Configure ephemeral setup |  | ||||||
| #[poise::command( |  | ||||||
|     slash_command, |  | ||||||
|     rename = "ephemeral", |  | ||||||
|     identifying_name = "ephemeral_confirmations", |  | ||||||
|     guild_only = true |  | ||||||
| )] |  | ||||||
| pub async fn ephemeral_confirmations(_ctx: Context<'_>) -> Result<(), Error> { |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set reminder confirmations to be sent "ephemerally" (private and cleared automatically) |  | ||||||
| #[poise::command( |  | ||||||
|     slash_command, |  | ||||||
|     rename = "on", |  | ||||||
|     identifying_name = "set_ephemeral_confirmations", |  | ||||||
|     guild_only = true |  | ||||||
| )] |  | ||||||
| pub async fn set_ephemeral_confirmations(ctx: Context<'_>) -> Result<(), Error> { |  | ||||||
|     let mut guild_data = ctx.guild_data().await.unwrap()?; |  | ||||||
|     guild_data.ephemeral_confirmations = true; |  | ||||||
|     guild_data.commit_changes(&ctx.data().database).await; |  | ||||||
|  |  | ||||||
|     ctx.send(|r| { |  | ||||||
|         r.ephemeral(true).embed(|e| { |  | ||||||
|             e.title("Confirmations ephemeral") |  | ||||||
|                 .description("Reminder confirmations will be sent privately, and removed when your client restarts.") |  | ||||||
|                 .color(*THEME_COLOR) |  | ||||||
|         }) |  | ||||||
|     }) |  | ||||||
|     .await?; |  | ||||||
|  |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set reminder confirmations to persist indefinitely |  | ||||||
| #[poise::command( |  | ||||||
|     slash_command, |  | ||||||
|     rename = "off", |  | ||||||
|     identifying_name = "unset_ephemeral_confirmations", |  | ||||||
|     guild_only = true |  | ||||||
| )] |  | ||||||
| pub async fn unset_ephemeral_confirmations(ctx: Context<'_>) -> Result<(), Error> { |  | ||||||
|     let mut guild_data = ctx.guild_data().await.unwrap()?; |  | ||||||
|     guild_data.ephemeral_confirmations = false; |  | ||||||
|     guild_data.commit_changes(&ctx.data().database).await; |  | ||||||
|  |  | ||||||
|     ctx.send(|r| { |  | ||||||
|         r.ephemeral(true).embed(|e| { |  | ||||||
|             e.title("Confirmations public") |  | ||||||
|                 .description( |  | ||||||
|                     "Reminder confirmations will be sent as regular messages, and won't be removed automatically.", |  | ||||||
|                 ) |  | ||||||
|                 .color(*THEME_COLOR) |  | ||||||
|         }) |  | ||||||
|     }) |  | ||||||
|     .await?; |  | ||||||
|  |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Configure whether other users can set reminders to your direct messages | /// Configure whether other users can set reminders to your direct messages | ||||||
| #[poise::command(slash_command, rename = "dm", identifying_name = "allowed_dm")] | #[poise::command(slash_command, rename = "dm", identifying_name = "allowed_dm")] | ||||||
| pub async fn allowed_dm(_ctx: Context<'_>) -> Result<(), Error> { | pub async fn allowed_dm(_ctx: Context<'_>) -> Result<(), Error> { | ||||||
| @@ -181,7 +108,7 @@ pub async fn allowed_dm(_ctx: Context<'_>) -> Result<(), Error> { | |||||||
| } | } | ||||||
|  |  | ||||||
| /// Allow other users to set reminders in your direct messages | /// Allow other users to set reminders in your direct messages | ||||||
| #[poise::command(slash_command, rename = "allow", identifying_name = "set_allowed_dm")] | #[poise::command(slash_command, rename = "allow", identifying_name = "allowed_dm")] | ||||||
| pub async fn set_allowed_dm(ctx: Context<'_>) -> Result<(), Error> { | pub async fn set_allowed_dm(ctx: Context<'_>) -> Result<(), Error> { | ||||||
|     let mut user_data = ctx.author_data().await?; |     let mut user_data = ctx.author_data().await?; | ||||||
|     user_data.allowed_dm = true; |     user_data.allowed_dm = true; | ||||||
| @@ -200,7 +127,7 @@ pub async fn set_allowed_dm(ctx: Context<'_>) -> Result<(), Error> { | |||||||
| } | } | ||||||
|  |  | ||||||
| /// Block other users from setting reminders in your direct messages | /// Block other users from setting reminders in your direct messages | ||||||
| #[poise::command(slash_command, rename = "block", identifying_name = "unset_allowed_dm")] | #[poise::command(slash_command, rename = "block", identifying_name = "allowed_dm")] | ||||||
| pub async fn unset_allowed_dm(ctx: Context<'_>) -> Result<(), Error> { | pub async fn unset_allowed_dm(ctx: Context<'_>) -> Result<(), Error> { | ||||||
|     let mut user_data = ctx.author_data().await?; |     let mut user_data = ctx.author_data().await?; | ||||||
|     user_data.allowed_dm = false; |     user_data.allowed_dm = false; | ||||||
| @@ -230,7 +157,8 @@ pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|     match ctx.channel_data().await { |     match ctx.channel_data().await { | ||||||
|         Ok(data) => { |         Ok(data) => { | ||||||
|             if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) { |             if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) { | ||||||
|                 ctx.send(|b| { |                 let _ = ctx | ||||||
|  |                     .send(|b| { | ||||||
|                         b.ephemeral(true).content(format!( |                         b.ephemeral(true).content(format!( | ||||||
|                             "**Warning!** |                             "**Warning!** | ||||||
| This link can be used by users to anonymously send messages, with or without permissions. | This link can be used by users to anonymously send messages, with or without permissions. | ||||||
| @@ -239,15 +167,13 @@ Do not share it! | |||||||
|                             id, token, |                             id, token, | ||||||
|                         )) |                         )) | ||||||
|                     }) |                     }) | ||||||
|                 .await?; |                     .await; | ||||||
|             } else { |             } else { | ||||||
|                 ctx.say("No webhook configured on this channel.").await?; |                 let _ = ctx.say("No webhook configured on this channel.").await; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         Err(e) => { |         Err(_) => { | ||||||
|             warn!("Error fetching channel data: {:?}", e); |             let _ = ctx.say("No webhook configured on this channel.").await; | ||||||
|  |  | ||||||
|             ctx.say("No webhook configured on this channel.").await?; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,18 +1,21 @@ | |||||||
| use std::{collections::HashSet, string::ToString}; | use std::{ | ||||||
|  |     collections::HashSet, | ||||||
|  |     string::ToString, | ||||||
|  |     time::{SystemTime, UNIX_EPOCH}, | ||||||
|  | }; | ||||||
|  |  | ||||||
| use chrono::{DateTime, NaiveDateTime, Utc}; | use chrono::NaiveDateTime; | ||||||
| use chrono_tz::Tz; | use chrono_tz::Tz; | ||||||
| use log::warn; |  | ||||||
| use num_integer::Integer; | use num_integer::Integer; | ||||||
| use poise::{ | use poise::{ | ||||||
|     serenity_prelude::{ |     serenity_prelude::{ | ||||||
|         builder::CreateEmbed, component::ButtonStyle, model::channel::Channel, ReactionType, |         builder::CreateEmbed, component::ButtonStyle, model::channel::Channel, ReactionType, | ||||||
|     }, |     }, | ||||||
|     CreateReply, Modal, |     AutocompleteChoice, CreateReply, Modal, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | use super::autocomplete::timezone_autocomplete; | ||||||
| use crate::{ | use crate::{ | ||||||
|     commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete}, |  | ||||||
|     component_models::{ |     component_models::{ | ||||||
|         pager::{DelPager, LookPager, Pager}, |         pager::{DelPager, LookPager, Pager}, | ||||||
|         ComponentDataModel, DelSelector, UndoReminder, |         ComponentDataModel, DelSelector, UndoReminder, | ||||||
| @@ -57,8 +60,8 @@ pub async fn pause( | |||||||
|             let parsed = natural_parser(&until, &timezone.to_string()).await; |             let parsed = natural_parser(&until, &timezone.to_string()).await; | ||||||
|  |  | ||||||
|             if let Some(timestamp) = parsed { |             if let Some(timestamp) = parsed { | ||||||
|                 match NaiveDateTime::from_timestamp_opt(timestamp, 0) { |                 let dt = NaiveDateTime::from_timestamp(timestamp, 0); | ||||||
|                     Some(dt) => { |  | ||||||
|                 channel.paused = true; |                 channel.paused = true; | ||||||
|                 channel.paused_until = Some(dt); |                 channel.paused_until = Some(dt); | ||||||
|  |  | ||||||
| @@ -69,15 +72,6 @@ pub async fn pause( | |||||||
|                     timestamp |                     timestamp | ||||||
|                 )) |                 )) | ||||||
|                 .await?; |                 .await?; | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     None => { |  | ||||||
|                         ctx.say(format!( |  | ||||||
|                             "Time processed could not be interpreted as `DateTime`. Please write the time as clearly as possible", |  | ||||||
|                         )) |  | ||||||
|                         .await?; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } else { |             } else { | ||||||
|                 ctx.say( |                 ctx.say( | ||||||
|                     "Time could not be processed. Please write the time as clearly as possible", |                     "Time could not be processed. Please write the time as clearly as possible", | ||||||
| @@ -216,7 +210,7 @@ pub async fn look( | |||||||
|         }), |         }), | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     let channel_opt = ctx.channel_id().to_channel_cached(&ctx); |     let channel_opt = ctx.channel_id().to_channel_cached(&ctx.discord()); | ||||||
|  |  | ||||||
|     let channel_id = if let Some(Channel::Guild(channel)) = channel_opt { |     let channel_id = if let Some(Channel::Guild(channel)) = channel_opt { | ||||||
|         if Some(channel.guild_id) == ctx.guild_id() { |         if Some(channel.guild_id) == ctx.guild_id() { | ||||||
| @@ -228,7 +222,8 @@ pub async fn look( | |||||||
|         ctx.channel_id() |         ctx.channel_id() | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     let channel_name = if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) { |     let channel_name = | ||||||
|  |         if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx.discord()) { | ||||||
|             Some(channel.name) |             Some(channel.name) | ||||||
|         } else { |         } else { | ||||||
|             None |             None | ||||||
| @@ -250,7 +245,7 @@ pub async fn look( | |||||||
|                 char_count < EMBED_DESCRIPTION_MAX_LENGTH |                 char_count < EMBED_DESCRIPTION_MAX_LENGTH | ||||||
|             }) |             }) | ||||||
|             .collect::<Vec<String>>() |             .collect::<Vec<String>>() | ||||||
|             .join(""); |             .join("\n"); | ||||||
|  |  | ||||||
|         let pages = reminders |         let pages = reminders | ||||||
|             .iter() |             .iter() | ||||||
| @@ -294,7 +289,8 @@ pub async fn delete(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|     let timezone = ctx.timezone().await; |     let timezone = ctx.timezone().await; | ||||||
|  |  | ||||||
|     let reminders = |     let reminders = | ||||||
|         Reminder::from_guild(&ctx, &ctx.data().database, ctx.guild_id(), ctx.author().id).await; |         Reminder::from_guild(&ctx.discord(), &ctx.data().database, ctx.guild_id(), ctx.author().id) | ||||||
|  |             .await; | ||||||
|  |  | ||||||
|     let resp = show_delete_page(&reminders, 0, timezone); |     let resp = show_delete_page(&reminders, 0, timezone); | ||||||
|  |  | ||||||
| @@ -436,8 +432,11 @@ pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> Cr | |||||||
|     reply |     reply | ||||||
| } | } | ||||||
|  |  | ||||||
| fn time_difference(start_time: DateTime<Utc>) -> String { | fn time_difference(start_time: NaiveDateTime) -> String { | ||||||
|     let delta = (Utc::now() - start_time).num_seconds(); |     let unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; | ||||||
|  |     let now = NaiveDateTime::from_timestamp(unix_time, 0); | ||||||
|  |  | ||||||
|  |     let delta = (now - start_time).num_seconds(); | ||||||
|  |  | ||||||
|     let (minutes, seconds) = delta.div_rem(&60); |     let (minutes, seconds) = delta.div_rem(&60); | ||||||
|     let (hours, minutes) = minutes.div_rem(&60); |     let (hours, minutes) = minutes.div_rem(&60); | ||||||
| @@ -551,6 +550,20 @@ pub async fn delete_timer( | |||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async fn multiline_autocomplete( | ||||||
|  |     _ctx: Context<'_>, | ||||||
|  |     partial: &str, | ||||||
|  | ) -> Vec<AutocompleteChoice<String>> { | ||||||
|  |     if partial.is_empty() { | ||||||
|  |         vec![AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() }] | ||||||
|  |     } else { | ||||||
|  |         vec![ | ||||||
|  |             AutocompleteChoice { name: partial.to_string(), value: partial.to_string() }, | ||||||
|  |             AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() }, | ||||||
|  |         ] | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(poise::Modal)] | #[derive(poise::Modal)] | ||||||
| #[name = "Reminder"] | #[name = "Reminder"] | ||||||
| struct ContentModal { | struct ContentModal { | ||||||
| @@ -561,17 +574,18 @@ struct ContentModal { | |||||||
|     content: String, |     content: String, | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Create a reminder with multi-line content. Press "+4 more" for other options. | /// Create a reminder. Press "+5 more" for other options. A modal will open if "content" is not provided | ||||||
| #[poise::command( | #[poise::command( | ||||||
|     slash_command, |     slash_command, | ||||||
|     identifying_name = "multiline", |     identifying_name = "remind", | ||||||
|     default_member_permissions = "MANAGE_GUILD" |     default_member_permissions = "MANAGE_GUILD" | ||||||
| )] | )] | ||||||
| pub async fn multiline( | pub async fn remind( | ||||||
|     ctx: ApplicationContext<'_>, |     ctx: ApplicationContext<'_>, | ||||||
|     #[description = "A description of the time to set the reminder for"] |     #[description = "A description of the time to set the reminder for"] time: String, | ||||||
|     #[autocomplete = "time_hint_autocomplete"] |     #[description = "The message content to send"] | ||||||
|     time: String, |     #[autocomplete = "multiline_autocomplete"] | ||||||
|  |     content: String, | ||||||
|     #[description = "Channel or user mentions to set the reminder for"] channels: Option<String>, |     #[description = "Channel or user mentions to set the reminder for"] channels: Option<String>, | ||||||
|     #[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"] |     #[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"] | ||||||
|     interval: Option<String>, |     interval: Option<String>, | ||||||
| @@ -584,10 +598,10 @@ pub async fn multiline( | |||||||
|     timezone: Option<String>, |     timezone: Option<String>, | ||||||
| ) -> Result<(), Error> { | ) -> Result<(), Error> { | ||||||
|     let tz = timezone.map(|t| t.parse::<Tz>().ok()).flatten(); |     let tz = timezone.map(|t| t.parse::<Tz>().ok()).flatten(); | ||||||
|     let data_opt = ContentModal::execute(ctx).await?; |  | ||||||
|  |  | ||||||
|     match data_opt { |     if content.is_empty() { | ||||||
|         Some(data) => { |         let data = ContentModal::execute(ctx).await?; | ||||||
|  |  | ||||||
|         create_reminder( |         create_reminder( | ||||||
|             Context::Application(ctx), |             Context::Application(ctx), | ||||||
|             time, |             time, | ||||||
| @@ -599,45 +613,19 @@ pub async fn multiline( | |||||||
|             tz, |             tz, | ||||||
|         ) |         ) | ||||||
|         .await |         .await | ||||||
|         } |     } else { | ||||||
|  |         create_reminder( | ||||||
|         None => { |             Context::Application(ctx), | ||||||
|             warn!("Unexpected None encountered in /multiline"); |             time, | ||||||
|             Ok(Context::Application(ctx) |             content, | ||||||
|                 .send(|m| m.content("Unexpected error.").ephemeral(true)) |             channels, | ||||||
|  |             interval, | ||||||
|  |             expires, | ||||||
|  |             tts, | ||||||
|  |             tz, | ||||||
|  |         ) | ||||||
|         .await |         .await | ||||||
|                 .map(|_| ())?) |  | ||||||
|     } |     } | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Create a reminder. Press "+4 more" for other options. Use "/multiline" for multiline content. |  | ||||||
| #[poise::command( |  | ||||||
|     slash_command, |  | ||||||
|     identifying_name = "remind", |  | ||||||
|     default_member_permissions = "MANAGE_GUILD" |  | ||||||
| )] |  | ||||||
| pub async fn remind( |  | ||||||
|     ctx: ApplicationContext<'_>, |  | ||||||
|     #[description = "A description of the time to set the reminder for"] |  | ||||||
|     #[autocomplete = "time_hint_autocomplete"] |  | ||||||
|     time: String, |  | ||||||
|     #[description = "The message content to send"] content: String, |  | ||||||
|     #[description = "Channel or user mentions to set the reminder for"] channels: Option<String>, |  | ||||||
|     #[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"] |  | ||||||
|     interval: Option<String>, |  | ||||||
|     #[description = "(Patreon only) For repeating reminders, the time at which the reminder will stop repeating"] |  | ||||||
|     expires: Option<String>, |  | ||||||
|     #[description = "Set the TTS flag on the reminder message, similar to the /tts command"] |  | ||||||
|     tts: Option<bool>, |  | ||||||
|     #[description = "Set a timezone override for this reminder only"] |  | ||||||
|     #[autocomplete = "timezone_autocomplete"] |  | ||||||
|     timezone: Option<String>, |  | ||||||
| ) -> Result<(), Error> { |  | ||||||
|     let tz = timezone.map(|t| t.parse::<Tz>().ok()).flatten(); |  | ||||||
|  |  | ||||||
|     create_reminder(Context::Application(ctx), time, content, channels, interval, expires, tts, tz) |  | ||||||
|         .await |  | ||||||
| } | } | ||||||
|  |  | ||||||
| async fn create_reminder( | async fn create_reminder( | ||||||
| @@ -656,13 +644,7 @@ async fn create_reminder( | |||||||
|         return Ok(()); |         return Ok(()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let ephemeral = |  | ||||||
|         ctx.guild_data().await.map_or(false, |gr| gr.map_or(false, |g| g.ephemeral_confirmations)); |  | ||||||
|     if ephemeral { |  | ||||||
|         ctx.defer_ephemeral().await?; |  | ||||||
|     } else { |  | ||||||
|     ctx.defer().await?; |     ctx.defer().await?; | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let user_data = ctx.author_data().await.unwrap(); |     let user_data = ctx.author_data().await.unwrap(); | ||||||
|     let timezone = timezone.unwrap_or(ctx.timezone().await); |     let timezone = timezone.unwrap_or(ctx.timezone().await); | ||||||
| @@ -692,9 +674,9 @@ async fn create_reminder( | |||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             let (processed_interval, processed_expires) = if let Some(repeat) = &interval { |             let (processed_interval, processed_expires) = if let Some(repeat) = &interval { | ||||||
|                 if check_subscription(&ctx, ctx.author().id).await |                 if check_subscription(&ctx.discord(), ctx.author().id).await | ||||||
|                     || (ctx.guild_id().is_some() |                     || (ctx.guild_id().is_some() | ||||||
|                         && check_guild_subscription(&ctx, ctx.guild_id().unwrap()).await) |                         && check_guild_subscription(&ctx.discord(), ctx.guild_id().unwrap()).await) | ||||||
|                 { |                 { | ||||||
|                     ( |                     ( | ||||||
|                         parse_duration(repeat) |                         parse_duration(repeat) | ||||||
| @@ -709,10 +691,9 @@ async fn create_reminder( | |||||||
|                         }, |                         }, | ||||||
|                     ) |                     ) | ||||||
|                 } else { |                 } else { | ||||||
|                     ctx.send(|b| { |                     ctx.say( | ||||||
|                         b.content( |                         "`repeat` is only available to Patreon subscribers or self-hosted users", | ||||||
|                         "`repeat` is only available to Patreon subscribers or self-hosted users") |                     ) | ||||||
|                     }) |  | ||||||
|                     .await?; |                     .await?; | ||||||
|  |  | ||||||
|                     return Ok(()); |                     return Ok(()); | ||||||
| @@ -722,17 +703,12 @@ async fn create_reminder( | |||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             if processed_interval.is_none() && interval.is_some() { |             if processed_interval.is_none() && interval.is_some() { | ||||||
|                 ctx.send(|b| { |                 ctx.say( | ||||||
|                     b.content( |                     "Repeat interval could not be processed. Try similar to `1 hour` or `4 days`", | ||||||
|                     "Repeat interval could not be processed. Try similar to `1 hour` or `4 days`") |                 ) | ||||||
|                 }) |  | ||||||
|                 .await?; |                 .await?; | ||||||
|             } else if processed_expires.is_none() && expires.is_some() { |             } else if processed_expires.is_none() && expires.is_some() { | ||||||
|                 ctx.send(|b| { |                 ctx.say("Expiry time failed to process. Please make it as clear as possible") | ||||||
|                     b.ephemeral(true).content( |  | ||||||
|                         "Expiry time failed to process. Please make it as clear as possible", |  | ||||||
|                     ) |  | ||||||
|                 }) |  | ||||||
|                     .await?; |                     .await?; | ||||||
|             } else { |             } else { | ||||||
|                 let mut builder = MultiReminderBuilder::new(&ctx, ctx.guild_id()) |                 let mut builder = MultiReminderBuilder::new(&ctx, ctx.guild_id()) | ||||||
|   | |||||||
| @@ -340,18 +340,7 @@ pub fn show_todo_page( | |||||||
|                                 opt.create_option(|o| { |                                 opt.create_option(|o| { | ||||||
|                                     o.label(format!("Mark {} complete", count + first_num)) |                                     o.label(format!("Mark {} complete", count + first_num)) | ||||||
|                                         .value(id) |                                         .value(id) | ||||||
|                                         .description({ |                                         .description(disp.split_once(' ').unwrap_or(("", "")).1) | ||||||
|                                             let c = disp.split_once(' ').unwrap_or(("", "")).1; |  | ||||||
|  |  | ||||||
|                                             if c.len() > 100 { |  | ||||||
|                                                 format!( |  | ||||||
|                                                     "{}...", |  | ||||||
|                                                     c.chars().take(97).collect::<String>() |  | ||||||
|                                                 ) |  | ||||||
|                                             } else { |  | ||||||
|                                                 c.to_string() |  | ||||||
|                                             } |  | ||||||
|                                         }) |  | ||||||
|                                 }); |                                 }); | ||||||
|                             } |                             } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ pub(crate) mod pager; | |||||||
|  |  | ||||||
| use std::io::Cursor; | use std::io::Cursor; | ||||||
|  |  | ||||||
| use base64::{engine::general_purpose, Engine}; |  | ||||||
| use chrono_tz::Tz; | use chrono_tz::Tz; | ||||||
| use log::warn; | use log::warn; | ||||||
| use poise::{ | use poise::{ | ||||||
| @@ -52,12 +51,11 @@ impl ComponentDataModel { | |||||||
|     pub fn to_custom_id(&self) -> String { |     pub fn to_custom_id(&self) -> String { | ||||||
|         let mut buf = Vec::new(); |         let mut buf = Vec::new(); | ||||||
|         self.serialize(&mut Serializer::new(&mut buf)).unwrap(); |         self.serialize(&mut Serializer::new(&mut buf)).unwrap(); | ||||||
|         general_purpose::STANDARD.encode(buf) |         base64::encode(buf) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn from_custom_id(data: &String) -> Self { |     pub fn from_custom_id(data: &String) -> Self { | ||||||
|         let buf = general_purpose::STANDARD |         let buf = base64::decode(data) | ||||||
|             .decode(data) |  | ||||||
|             .map_err(|e| format!("Could not decode `custom_id' {}: {:?}", data, e)) |             .map_err(|e| format!("Could not decode `custom_id' {}: {:?}", data, e)) | ||||||
|             .unwrap(); |             .unwrap(); | ||||||
|         let cur = Cursor::new(buf); |         let cur = Cursor::new(buf); | ||||||
| @@ -115,7 +113,7 @@ impl ComponentDataModel { | |||||||
|                         char_count < EMBED_DESCRIPTION_MAX_LENGTH |                         char_count < EMBED_DESCRIPTION_MAX_LENGTH | ||||||
|                     }) |                     }) | ||||||
|                     .collect::<Vec<String>>() |                     .collect::<Vec<String>>() | ||||||
|                     .join(""); |                     .join("\n"); | ||||||
|  |  | ||||||
|                 let mut embed = CreateEmbed::default(); |                 let mut embed = CreateEmbed::default(); | ||||||
|                 embed |                 embed | ||||||
| @@ -166,21 +164,12 @@ impl ComponentDataModel { | |||||||
|                     .await; |                     .await; | ||||||
|             } |             } | ||||||
|             ComponentDataModel::DelSelector(selector) => { |             ComponentDataModel::DelSelector(selector) => { | ||||||
|                 for id in &component.data.values { |                 let selected_id = component.data.values.join(","); | ||||||
|                     match id.parse::<u32>() { |  | ||||||
|                         Ok(id) => { |  | ||||||
|                             if let Some(reminder) = Reminder::from_id(&data.database, id).await { |  | ||||||
|                                 reminder.delete(&data.database).await.unwrap(); |  | ||||||
|                             } else { |  | ||||||
|                                 warn!("Attempt to delete non-existent reminder"); |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         Err(e) => { |                 sqlx::query!("DELETE FROM reminders WHERE FIND_IN_SET(id, ?)", selected_id) | ||||||
|                             warn!("Error casting ID to integer: {:?}.", e); |                     .execute(&data.database) | ||||||
|                         } |                     .await | ||||||
|                     } |                     .unwrap(); | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 let reminders = Reminder::from_guild( |                 let reminders = Reminder::from_guild( | ||||||
|                     &ctx, |                     &ctx, | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ pub const DAY: u64 = 86_400; | |||||||
| pub const HOUR: u64 = 3_600; | pub const HOUR: u64 = 3_600; | ||||||
| pub const MINUTE: u64 = 60; | pub const MINUTE: u64 = 60; | ||||||
|  |  | ||||||
| pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4096; | pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000; | ||||||
| pub const SELECT_MAX_ENTRIES: usize = 25; | pub const SELECT_MAX_ENTRIES: usize = 25; | ||||||
|  |  | ||||||
| pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; | pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; | ||||||
| @@ -17,13 +17,17 @@ use regex::Regex; | |||||||
|  |  | ||||||
| lazy_static! { | lazy_static! { | ||||||
|     pub static ref DEFAULT_AVATAR: AttachmentType<'static> = ( |     pub static ref DEFAULT_AVATAR: AttachmentType<'static> = ( | ||||||
|         include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/webhook.jpg")) as &[u8], |         include_bytes!(concat!( | ||||||
|         "webhook.jpg", |             env!("CARGO_MANIFEST_DIR"), | ||||||
|  |             "/assets/", | ||||||
|  |             env!("WEBHOOK_AVATAR", "WEBHOOK_AVATAR not provided for compilation") | ||||||
|  |         )) as &[u8], | ||||||
|  |         env!("WEBHOOK_AVATAR"), | ||||||
|     ) |     ) | ||||||
|         .into(); |         .into(); | ||||||
|     pub static ref REGEX_CHANNEL_USER: Regex = Regex::new(r#"\s*<(#|@)(?:!)?(\d+)>\s*"#).unwrap(); |     pub static ref REGEX_CHANNEL_USER: Regex = Regex::new(r#"\s*<(#|@)(?:!)?(\d+)>\s*"#).unwrap(); | ||||||
|     pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter( |     pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter( | ||||||
|         env::var("PATREON_ROLE_ID") |         env::var("SUBSCRIPTION_ROLES") | ||||||
|             .map(|var| var |             .map(|var| var | ||||||
|                 .split(',') |                 .split(',') | ||||||
|                 .filter_map(|item| { item.parse::<u64>().ok() }) |                 .filter_map(|item| { item.parse::<u64>().ok() }) | ||||||
| @@ -31,7 +35,7 @@ lazy_static! { | |||||||
|             .unwrap_or_else(|_| Vec::new()) |             .unwrap_or_else(|_| Vec::new()) | ||||||
|     ); |     ); | ||||||
|     pub static ref CNC_GUILD: Option<u64> = |     pub static ref CNC_GUILD: Option<u64> = | ||||||
|         env::var("PATREON_GUILD_ID").map(|var| var.parse::<u64>().ok()).ok().flatten(); |         env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten(); | ||||||
|     pub static ref MIN_INTERVAL: i64 = |     pub static ref MIN_INTERVAL: i64 = | ||||||
|         env::var("MIN_INTERVAL").ok().and_then(|inner| inner.parse::<i64>().ok()).unwrap_or(600); |         env::var("MIN_INTERVAL").ok().and_then(|inner| inner.parse::<i64>().ok()).unwrap_or(600); | ||||||
|     pub static ref MAX_TIME: i64 = env::var("MAX_TIME") |     pub static ref MAX_TIME: i64 = env::var("MAX_TIME") | ||||||
| @@ -44,5 +48,5 @@ lazy_static! { | |||||||
|         .map_or(THEME_COLOR_FALLBACK, |inner| u32::from_str_radix(&inner, 16) |         .map_or(THEME_COLOR_FALLBACK, |inner| u32::from_str_radix(&inner, 16) | ||||||
|             .unwrap_or(THEME_COLOR_FALLBACK)); |             .unwrap_or(THEME_COLOR_FALLBACK)); | ||||||
|     pub static ref PYTHON_LOCATION: String = |     pub static ref PYTHON_LOCATION: String = | ||||||
|         env::var("PYTHON_LOCATION").unwrap_or_else(|_| "/usr/bin/python3".to_string()); |         env::var("PYTHON_LOCATION").unwrap_or_else(|_| "venv/bin/python3".to_string()); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								src/hooks.rs
									
									
									
									
									
								
							| @@ -4,7 +4,7 @@ use poise::{ | |||||||
|  |  | ||||||
| use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error}; | use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error}; | ||||||
|  |  | ||||||
| async fn macro_check(ctx: Context<'_>) -> bool { | async fn recording_macro_check(ctx: Context<'_>) -> bool { | ||||||
|     if let Context::Application(app_ctx) = ctx { |     if let Context::Application(app_ctx) = ctx { | ||||||
|         if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(_) = |         if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(_) = | ||||||
|             app_ctx.interaction |             app_ctx.interaction | ||||||
| @@ -47,26 +47,25 @@ async fn macro_check(ctx: Context<'_>) -> bool { | |||||||
|  |  | ||||||
| async fn check_self_permissions(ctx: Context<'_>) -> bool { | async fn check_self_permissions(ctx: Context<'_>) -> bool { | ||||||
|     if let Some(guild) = ctx.guild() { |     if let Some(guild) = ctx.guild() { | ||||||
|         let user_id = ctx.serenity_context().cache.current_user_id(); |         let user_id = ctx.discord().cache.current_user_id(); | ||||||
|  |  | ||||||
|         let manage_webhooks = |  | ||||||
|             guild.member_permissions(&ctx, user_id).await.map_or(false, |p| p.manage_webhooks()); |  | ||||||
|  |  | ||||||
|  |         let manage_webhooks = guild | ||||||
|  |             .member_permissions(&ctx.discord(), user_id) | ||||||
|  |             .await | ||||||
|  |             .map_or(false, |p| p.manage_webhooks()); | ||||||
|         let (view_channel, send_messages, embed_links) = ctx |         let (view_channel, send_messages, embed_links) = ctx | ||||||
|             .channel_id() |             .channel_id() | ||||||
|             .to_channel(&ctx) |             .to_channel_cached(&ctx.discord()) | ||||||
|             .await |  | ||||||
|             .ok() |  | ||||||
|             .and_then(|c| { |             .and_then(|c| { | ||||||
|                 if let Channel::Guild(channel) = c { |                 if let Channel::Guild(channel) = c { | ||||||
|                     let perms = channel.permissions_for_user(&ctx, user_id).ok()?; |                     channel.permissions_for_user(&ctx.discord(), user_id).ok() | ||||||
|  |  | ||||||
|                     Some((perms.view_channel(), perms.send_messages(), perms.embed_links())) |  | ||||||
|                 } else { |                 } else { | ||||||
|                     None |                     None | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
|             .unwrap_or((false, false, false)); |             .map_or((false, false, false), |p| { | ||||||
|  |                 (p.view_channel(), p.send_messages(), p.embed_links()) | ||||||
|  |             }); | ||||||
|  |  | ||||||
|         if manage_webhooks && send_messages && embed_links { |         if manage_webhooks && send_messages && embed_links { | ||||||
|             true |             true | ||||||
| @@ -82,8 +81,8 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool { | |||||||
| {}     **Manage Webhooks**", | {}     **Manage Webhooks**", | ||||||
|                         if view_channel { "✅" } else { "❌" }, |                         if view_channel { "✅" } else { "❌" }, | ||||||
|                         if send_messages { "✅" } else { "❌" }, |                         if send_messages { "✅" } else { "❌" }, | ||||||
|                         if embed_links { "✅" } else { "❌" }, |  | ||||||
|                         if manage_webhooks { "✅" } else { "❌" }, |                         if manage_webhooks { "✅" } else { "❌" }, | ||||||
|  |                         if embed_links { "✅" } else { "❌" }, | ||||||
|                     )) |                     )) | ||||||
|                 }) |                 }) | ||||||
|                 .await; |                 .await; | ||||||
| @@ -96,5 +95,5 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool { | |||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn all_checks(ctx: Context<'_>) -> Result<bool, Error> { | pub async fn all_checks(ctx: Context<'_>) -> Result<bool, Error> { | ||||||
|     Ok(macro_check(ctx).await && check_self_permissions(ctx).await) |     Ok(recording_macro_check(ctx).await && check_self_permissions(ctx).await) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -110,14 +110,13 @@ impl OverflowOp for u64 { | |||||||
| #[derive(Copy, Clone)] | #[derive(Copy, Clone)] | ||||||
| pub struct Interval { | pub struct Interval { | ||||||
|     pub month: u64, |     pub month: u64, | ||||||
|     pub day: u64, |  | ||||||
|     pub sec: u64, |     pub sec: u64, | ||||||
| } | } | ||||||
|  |  | ||||||
| struct Parser<'a> { | struct Parser<'a> { | ||||||
|     iter: Chars<'a>, |     iter: Chars<'a>, | ||||||
|     src: &'a str, |     src: &'a str, | ||||||
|     current: (u64, u64, u64, u64), |     current: (u64, u64, u64), | ||||||
| } | } | ||||||
|  |  | ||||||
| impl<'a> Parser<'a> { | impl<'a> Parser<'a> { | ||||||
| @@ -141,17 +140,17 @@ impl<'a> Parser<'a> { | |||||||
|         Ok(None) |         Ok(None) | ||||||
|     } |     } | ||||||
|     fn parse_unit(&mut self, n: u64, start: usize, end: usize) -> Result<(), Error> { |     fn parse_unit(&mut self, n: u64, start: usize, end: usize) -> Result<(), Error> { | ||||||
|         let (mut month, mut day, mut sec, nsec) = match &self.src[start..end] { |         let (mut month, mut sec, nsec) = match &self.src[start..end] { | ||||||
|             "nanos" | "nsec" | "ns" => (0, 0u64, 0u64, n), |             "nanos" | "nsec" | "ns" => (0u64, 0u64, n), | ||||||
|             "usec" | "us" => (0, 0, 0u64, n.mul(1000)?), |             "usec" | "us" => (0, 0u64, n.mul(1000)?), | ||||||
|             "millis" | "msec" | "ms" => (0, 0, 0u64, n.mul(1_000_000)?), |             "millis" | "msec" | "ms" => (0, 0u64, n.mul(1_000_000)?), | ||||||
|             "seconds" | "second" | "secs" | "sec" | "s" => (0, 0, n, 0), |             "seconds" | "second" | "secs" | "sec" | "s" => (0, n, 0), | ||||||
|             "minutes" | "minute" | "min" | "mins" | "m" => (0, 0, n.mul(60)?, 0), |             "minutes" | "minute" | "min" | "mins" | "m" => (0, n.mul(60)?, 0), | ||||||
|             "hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0), |             "hours" | "hour" | "hr" | "hrs" | "h" => (0, n.mul(3600)?, 0), | ||||||
|             "days" | "day" | "d" => (0, n, 0, 0), |             "days" | "day" | "d" => (0, n.mul(86400)?, 0), | ||||||
|             "weeks" | "week" | "w" => (0, n.mul(7)?, 0, 0), |             "weeks" | "week" | "w" => (0, n.mul(86400 * 7)?, 0), | ||||||
|             "months" | "month" | "M" => (n, 0, 0, 0), |             "months" | "month" | "M" => (n, 0, 0), | ||||||
|             "years" | "year" | "y" => (n.mul(12)?, 0, 0, 0), |             "years" | "year" | "y" => (12, 0, 0), | ||||||
|             _ => { |             _ => { | ||||||
|                 return Err(Error::UnknownUnit { |                 return Err(Error::UnknownUnit { | ||||||
|                     start, |                     start, | ||||||
| @@ -161,16 +160,15 @@ impl<'a> Parser<'a> { | |||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|         let mut nsec = self.current.3 + nsec; |         let mut nsec = self.current.2 + nsec; | ||||||
|         if nsec > 1_000_000_000 { |         if nsec > 1_000_000_000 { | ||||||
|             sec += nsec / 1_000_000_000; |             sec += nsec / 1_000_000_000; | ||||||
|             nsec %= 1_000_000_000; |             nsec %= 1_000_000_000; | ||||||
|         } |         } | ||||||
|         sec += self.current.2; |         sec += self.current.1; | ||||||
|         day += self.current.1; |  | ||||||
|         month += self.current.0; |         month += self.current.0; | ||||||
|  |  | ||||||
|         self.current = (month, day, sec, nsec); |         self.current = (month, sec, nsec); | ||||||
|  |  | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| @@ -217,13 +215,7 @@ impl<'a> Parser<'a> { | |||||||
|             self.parse_unit(n, start, off)?; |             self.parse_unit(n, start, off)?; | ||||||
|             n = match self.parse_first_char()? { |             n = match self.parse_first_char()? { | ||||||
|                 Some(n) => n, |                 Some(n) => n, | ||||||
|                 None => { |                 None => return Ok(Interval { month: self.current.0, sec: self.current.1 }), | ||||||
|                     return Ok(Interval { |  | ||||||
|                         month: self.current.0, |  | ||||||
|                         day: self.current.1, |  | ||||||
|                         sec: self.current.2, |  | ||||||
|                     }) |  | ||||||
|                 } |  | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -255,73 +247,5 @@ impl<'a> Parser<'a> { | |||||||
| /// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000))); | /// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000))); | ||||||
| /// ``` | /// ``` | ||||||
| pub fn parse_duration(s: &str) -> Result<Interval, Error> { | pub fn parse_duration(s: &str) -> Result<Interval, Error> { | ||||||
|     Parser { iter: s.chars(), src: s, current: (0, 0, 0, 0) }.parse() |     Parser { iter: s.chars(), src: s, current: (0, 0, 0) }.parse() | ||||||
| } |  | ||||||
|  |  | ||||||
| #[cfg(test)] |  | ||||||
| mod tests { |  | ||||||
|     use super::*; |  | ||||||
|  |  | ||||||
|     #[test] |  | ||||||
|     fn parse_seconds() { |  | ||||||
|         let interval = parse_duration("10 seconds").unwrap(); |  | ||||||
|  |  | ||||||
|         assert_eq!(interval.sec, 10); |  | ||||||
|         assert_eq!(interval.day, 0); |  | ||||||
|         assert_eq!(interval.month, 0); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     #[test] |  | ||||||
|     fn parse_minutes() { |  | ||||||
|         let interval = parse_duration("10 minutes").unwrap(); |  | ||||||
|  |  | ||||||
|         assert_eq!(interval.sec, 600); |  | ||||||
|         assert_eq!(interval.day, 0); |  | ||||||
|         assert_eq!(interval.month, 0); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     #[test] |  | ||||||
|     fn parse_hours() { |  | ||||||
|         let interval = parse_duration("10 hours").unwrap(); |  | ||||||
|  |  | ||||||
|         assert_eq!(interval.sec, 36_000); |  | ||||||
|         assert_eq!(interval.day, 0); |  | ||||||
|         assert_eq!(interval.month, 0); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     #[test] |  | ||||||
|     fn parse_days() { |  | ||||||
|         let interval = parse_duration("10 days").unwrap(); |  | ||||||
|  |  | ||||||
|         assert_eq!(interval.sec, 0); |  | ||||||
|         assert_eq!(interval.day, 10); |  | ||||||
|         assert_eq!(interval.month, 0); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     #[test] |  | ||||||
|     fn parse_weeks() { |  | ||||||
|         let interval = parse_duration("10 weeks").unwrap(); |  | ||||||
|  |  | ||||||
|         assert_eq!(interval.sec, 0); |  | ||||||
|         assert_eq!(interval.day, 70); |  | ||||||
|         assert_eq!(interval.month, 0); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     #[test] |  | ||||||
|     fn parse_months() { |  | ||||||
|         let interval = parse_duration("10 months").unwrap(); |  | ||||||
|  |  | ||||||
|         assert_eq!(interval.sec, 0); |  | ||||||
|         assert_eq!(interval.day, 0); |  | ||||||
|         assert_eq!(interval.month, 10); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     #[test] |  | ||||||
|     fn parse_years() { |  | ||||||
|         let interval = parse_duration("10 years").unwrap(); |  | ||||||
|  |  | ||||||
|         assert_eq!(interval.sec, 0); |  | ||||||
|         assert_eq!(interval.day, 0); |  | ||||||
|         assert_eq!(interval.month, 120); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										49
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -18,10 +18,10 @@ use std::{ | |||||||
|     env, |     env, | ||||||
|     error::Error as StdError, |     error::Error as StdError, | ||||||
|     fmt::{Debug, Display, Formatter}, |     fmt::{Debug, Display, Formatter}, | ||||||
|     path::Path, |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| use chrono_tz::Tz; | use chrono_tz::Tz; | ||||||
|  | use dotenv::dotenv; | ||||||
| use log::{error, warn}; | use log::{error, warn}; | ||||||
| use poise::serenity_prelude::model::{ | use poise::serenity_prelude::model::{ | ||||||
|     gateway::GatewayIntents, |     gateway::GatewayIntents, | ||||||
| @@ -75,7 +75,7 @@ impl Display for Ended { | |||||||
|  |  | ||||||
| impl StdError for Ended {} | impl StdError for Ended {} | ||||||
|  |  | ||||||
| #[tokio::main(flavor = "multi_thread")] | #[tokio::main] | ||||||
| async fn main() -> Result<(), Box<dyn StdError + Send + Sync>> { | async fn main() -> Result<(), Box<dyn StdError + Send + Sync>> { | ||||||
|     let (tx, mut rx) = broadcast::channel(16); |     let (tx, mut rx) = broadcast::channel(16); | ||||||
|  |  | ||||||
| @@ -88,11 +88,7 @@ async fn main() -> Result<(), Box<dyn StdError + Send + Sync>> { | |||||||
| async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | ||||||
|     env_logger::init(); |     env_logger::init(); | ||||||
|  |  | ||||||
|     if Path::new("/etc/reminder-rs/config.env").exists() { |     dotenv()?; | ||||||
|         dotenv::from_path("/etc/reminder-rs/config.env")?; |  | ||||||
|     } else { |  | ||||||
|         let _ = dotenv::dotenv(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let discord_token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment"); |     let discord_token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment"); | ||||||
|  |  | ||||||
| @@ -112,16 +108,6 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | |||||||
|                 ], |                 ], | ||||||
|                 ..moderation_cmds::allowed_dm() |                 ..moderation_cmds::allowed_dm() | ||||||
|             }, |             }, | ||||||
|             poise::Command { |  | ||||||
|                 subcommands: vec![poise::Command { |  | ||||||
|                     subcommands: vec![ |  | ||||||
|                         moderation_cmds::set_ephemeral_confirmations(), |  | ||||||
|                         moderation_cmds::unset_ephemeral_confirmations(), |  | ||||||
|                     ], |  | ||||||
|                     ..moderation_cmds::ephemeral_confirmations() |  | ||||||
|                 }], |  | ||||||
|                 ..moderation_cmds::settings() |  | ||||||
|             }, |  | ||||||
|             moderation_cmds::webhook(), |             moderation_cmds::webhook(), | ||||||
|             poise::Command { |             poise::Command { | ||||||
|                 subcommands: vec![ |                 subcommands: vec![ | ||||||
| @@ -131,6 +117,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | |||||||
|                     command_macro::record::record_macro(), |                     command_macro::record::record_macro(), | ||||||
|                     command_macro::run::run_macro(), |                     command_macro::run::run_macro(), | ||||||
|                     command_macro::migrate::migrate_macro(), |                     command_macro::migrate::migrate_macro(), | ||||||
|  |                     command_macro::install::install_macro(), | ||||||
|                 ], |                 ], | ||||||
|                 ..command_macro::macro_base() |                 ..command_macro::macro_base() | ||||||
|             }, |             }, | ||||||
| @@ -147,7 +134,6 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | |||||||
|                 ], |                 ], | ||||||
|                 ..reminder_cmds::timer_base() |                 ..reminder_cmds::timer_base() | ||||||
|             }, |             }, | ||||||
|             reminder_cmds::multiline(), |  | ||||||
|             reminder_cmds::remind(), |             reminder_cmds::remind(), | ||||||
|             poise::Command { |             poise::Command { | ||||||
|                 subcommands: vec![ |                 subcommands: vec![ | ||||||
| @@ -175,36 +161,15 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | |||||||
|         ], |         ], | ||||||
|         allowed_mentions: None, |         allowed_mentions: None, | ||||||
|         command_check: Some(|ctx| Box::pin(all_checks(ctx))), |         command_check: Some(|ctx| Box::pin(all_checks(ctx))), | ||||||
|         event_handler: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)), |         listener: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)), | ||||||
|         on_error: |error| { |  | ||||||
|             Box::pin(async move { |  | ||||||
|                 match error { |  | ||||||
|                     poise::FrameworkError::CommandCheckFailed { .. } => { |  | ||||||
|                         // suppress error |  | ||||||
|                     } |  | ||||||
|                     error => { |  | ||||||
|                         if let Err(e) = poise::builtins::on_error(error).await { |  | ||||||
|                             log::error!("Error while handling error: {}", e); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         }, |  | ||||||
|         ..Default::default() |         ..Default::default() | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     let database = |     let database = | ||||||
|         Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap(); |         Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap(); | ||||||
|  |  | ||||||
|     sqlx::migrate!().run(&database).await?; |  | ||||||
|  |  | ||||||
|     let popular_timezones = sqlx::query!( |     let popular_timezones = sqlx::query!( | ||||||
|         "SELECT IFNULL(timezone, 'UTC') AS timezone |         "SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21" | ||||||
|         FROM users |  | ||||||
|         WHERE timezone IS NOT NULL |  | ||||||
|         GROUP BY timezone |  | ||||||
|         ORDER BY COUNT(timezone) DESC |  | ||||||
|         LIMIT 21" |  | ||||||
|     ) |     ) | ||||||
|     .fetch_all(&database) |     .fetch_all(&database) | ||||||
|     .await |     .await | ||||||
| @@ -215,7 +180,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | |||||||
|  |  | ||||||
|     poise::Framework::builder() |     poise::Framework::builder() | ||||||
|         .token(discord_token) |         .token(discord_token) | ||||||
|         .setup(move |ctx, _bot, framework| { |         .user_data_setup(move |ctx, _bot, framework| { | ||||||
|             Box::pin(async move { |             Box::pin(async move { | ||||||
|                 register_application_commands(ctx, framework, None).await.unwrap(); |                 register_application_commands(ctx, framework, None).await.unwrap(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ pub struct ChannelData { | |||||||
|     pub webhook_id: Option<u64>, |     pub webhook_id: Option<u64>, | ||||||
|     pub webhook_token: Option<String>, |     pub webhook_token: Option<String>, | ||||||
|     pub paused: bool, |     pub paused: bool, | ||||||
|     pub db_guild_id: Option<u32>, |  | ||||||
|     pub paused_until: Option<NaiveDateTime>, |     pub paused_until: Option<NaiveDateTime>, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -24,9 +23,7 @@ impl ChannelData { | |||||||
|         if let Ok(c) = sqlx::query_as_unchecked!( |         if let Ok(c) = sqlx::query_as_unchecked!( | ||||||
|             Self, |             Self, | ||||||
|             " |             " | ||||||
|             SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until, | SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ? | ||||||
|                 guild_id AS db_guild_id |  | ||||||
|             FROM channels WHERE channel = ? |  | ||||||
|             ", |             ", | ||||||
|             channel_id |             channel_id | ||||||
|         ) |         ) | ||||||
| @@ -35,17 +32,13 @@ impl ChannelData { | |||||||
|         { |         { | ||||||
|             Ok(c) |             Ok(c) | ||||||
|         } else { |         } else { | ||||||
|             let props = |             let props = channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name)); | ||||||
|                 channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name)); |  | ||||||
|  |  | ||||||
|             let (guild_id, channel_name) = |             let (guild_id, channel_name) = if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) }; | ||||||
|                 if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) }; |  | ||||||
|  |  | ||||||
|             sqlx::query!( |             sqlx::query!( | ||||||
|                 " |                 " | ||||||
|                 INSERT IGNORE INTO channels | INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?)) | ||||||
|                 (channel, name, guild_id) |  | ||||||
|                 VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?)) |  | ||||||
|                 ", |                 ", | ||||||
|                 channel_id, |                 channel_id, | ||||||
|                 channel_name, |                 channel_name, | ||||||
| @@ -57,10 +50,7 @@ impl ChannelData { | |||||||
|             Ok(sqlx::query_as_unchecked!( |             Ok(sqlx::query_as_unchecked!( | ||||||
|                 Self, |                 Self, | ||||||
|                 " |                 " | ||||||
|                 SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, | SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ? | ||||||
|                     paused_until, guild_id AS db_guild_id |  | ||||||
|                 FROM channels |  | ||||||
|                 WHERE channel = ? |  | ||||||
|                 ", |                 ", | ||||||
|                 channel_id |                 channel_id | ||||||
|             ) |             ) | ||||||
| @@ -72,10 +62,8 @@ impl ChannelData { | |||||||
|     pub async fn commit_changes(&self, pool: &MySqlPool) { |     pub async fn commit_changes(&self, pool: &MySqlPool) { | ||||||
|         sqlx::query!( |         sqlx::query!( | ||||||
|             " |             " | ||||||
|             UPDATE channels | UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until \ | ||||||
|             SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, |              = ? WHERE id = ? | ||||||
|                 paused = ?, paused_until = ? |  | ||||||
|             WHERE id = ? |  | ||||||
|             ", |             ", | ||||||
|             self.name, |             self.name, | ||||||
|             self.nudge, |             self.nudge, | ||||||
|   | |||||||
| @@ -37,6 +37,7 @@ pub struct RawCommandMacro { | |||||||
|     pub commands: Value, |     pub commands: Value, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// Get a macro by name form a guild. | ||||||
| pub async fn guild_command_macro( | pub async fn guild_command_macro( | ||||||
|     ctx: &Context<'_>, |     ctx: &Context<'_>, | ||||||
|     name: &str, |     name: &str, | ||||||
|   | |||||||
| @@ -1,48 +0,0 @@ | |||||||
| use poise::serenity_prelude::GuildId; |  | ||||||
| use sqlx::MySqlPool; |  | ||||||
|  |  | ||||||
| pub struct GuildData { |  | ||||||
|     pub ephemeral_confirmations: bool, |  | ||||||
|     pub id: u32, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl GuildData { |  | ||||||
|     pub async fn from_guild( |  | ||||||
|         guild_id: GuildId, |  | ||||||
|         pool: &MySqlPool, |  | ||||||
|     ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> { |  | ||||||
|         if let Ok(c) = sqlx::query_as_unchecked!( |  | ||||||
|             Self, |  | ||||||
|             "SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?", |  | ||||||
|             guild_id.0 |  | ||||||
|         ) |  | ||||||
|         .fetch_one(pool) |  | ||||||
|         .await |  | ||||||
|         { |  | ||||||
|             Ok(c) |  | ||||||
|         } else { |  | ||||||
|             sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id.0) |  | ||||||
|                 .execute(&pool.clone()) |  | ||||||
|                 .await?; |  | ||||||
|  |  | ||||||
|             Ok(sqlx::query_as_unchecked!( |  | ||||||
|                 Self, |  | ||||||
|                 "SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?", |  | ||||||
|                 guild_id.0 |  | ||||||
|             ) |  | ||||||
|             .fetch_one(pool) |  | ||||||
|             .await?) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub async fn commit_changes(&self, pool: &MySqlPool) { |  | ||||||
|         sqlx::query!( |  | ||||||
|             "UPDATE guilds SET ephemeral_confirmations = ? WHERE id = ?", |  | ||||||
|             self.ephemeral_confirmations, |  | ||||||
|             self.id |  | ||||||
|         ) |  | ||||||
|         .execute(pool) |  | ||||||
|         .await |  | ||||||
|         .unwrap(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,15 +1,14 @@ | |||||||
| pub mod channel_data; | pub mod channel_data; | ||||||
| pub mod command_macro; | pub mod command_macro; | ||||||
| pub mod guild_data; |  | ||||||
| pub mod reminder; | pub mod reminder; | ||||||
| pub mod timer; | pub mod timer; | ||||||
| pub mod user_data; | pub mod user_data; | ||||||
|  |  | ||||||
| use chrono_tz::Tz; | use chrono_tz::Tz; | ||||||
| use poise::serenity_prelude::{async_trait, model::id::UserId, ChannelType}; | use poise::serenity_prelude::{async_trait, model::id::UserId}; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData}, |     models::{channel_data::ChannelData, user_data::UserData}, | ||||||
|     CommandMacro, Context, Data, Error, GuildId, |     CommandMacro, Context, Data, Error, GuildId, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -19,8 +18,6 @@ pub trait CtxData { | |||||||
|  |  | ||||||
|     async fn author_data(&self) -> Result<UserData, Error>; |     async fn author_data(&self) -> Result<UserData, Error>; | ||||||
|  |  | ||||||
|     async fn guild_data(&self) -> Option<Result<GuildData, Error>>; |  | ||||||
|  |  | ||||||
|     async fn timezone(&self) -> Tz; |     async fn timezone(&self) -> Tz; | ||||||
|  |  | ||||||
|     async fn channel_data(&self) -> Result<ChannelData, Error>; |     async fn channel_data(&self) -> Result<ChannelData, Error>; | ||||||
| @@ -30,21 +27,15 @@ pub trait CtxData { | |||||||
|  |  | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl CtxData for Context<'_> { | impl CtxData for Context<'_> { | ||||||
|     async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error> { |     async fn user_data<U: Into<UserId> + Send>( | ||||||
|         UserData::from_user(user_id, &self.serenity_context(), &self.data().database).await |         &self, | ||||||
|  |         user_id: U, | ||||||
|  |     ) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>> { | ||||||
|  |         UserData::from_user(user_id, &self.discord(), &self.data().database).await | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn author_data(&self) -> Result<UserData, Error> { |     async fn author_data(&self) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>> { | ||||||
|         UserData::from_user(&self.author().id, &self.serenity_context(), &self.data().database) |         UserData::from_user(&self.author().id, &self.discord(), &self.data().database).await | ||||||
|             .await |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn guild_data(&self) -> Option<Result<GuildData, Error>> { |  | ||||||
|         if let Some(guild_id) = self.guild_id() { |  | ||||||
|             Some(GuildData::from_guild(guild_id, &self.data().database).await) |  | ||||||
|         } else { |  | ||||||
|             None |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn timezone(&self) -> Tz { |     async fn timezone(&self) -> Tz { | ||||||
| @@ -52,20 +43,7 @@ impl CtxData for Context<'_> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn channel_data(&self) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>> { |     async fn channel_data(&self) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>> { | ||||||
|         // If we're in a thread, get the parent channel. |         let channel = self.channel_id().to_channel_cached(&self.discord()).unwrap(); | ||||||
|         let recv_channel = self.channel_id().to_channel(&self).await?; |  | ||||||
|  |  | ||||||
|         let channel = match recv_channel.guild() { |  | ||||||
|             Some(guild_channel) => { |  | ||||||
|                 if guild_channel.kind == ChannelType::PublicThread { |  | ||||||
|                     guild_channel.parent_id.unwrap().to_channel_cached(&self).unwrap() |  | ||||||
|                 } else { |  | ||||||
|                     self.channel_id().to_channel_cached(&self).unwrap() |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             None => self.channel_id().to_channel_cached(&self).unwrap(), |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         ChannelData::from_channel(&channel, &self.data().database).await |         ChannelData::from_channel(&channel, &self.data().database).await | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ use poise::serenity_prelude::{ | |||||||
|         id::{ChannelId, GuildId, UserId}, |         id::{ChannelId, GuildId, UserId}, | ||||||
|         webhook::Webhook, |         webhook::Webhook, | ||||||
|     }, |     }, | ||||||
|     ChannelType, Result as SerenityResult, |     Result as SerenityResult, | ||||||
| }; | }; | ||||||
| use sqlx::MySqlPool; | use sqlx::MySqlPool; | ||||||
|  |  | ||||||
| @@ -51,12 +51,9 @@ pub struct ReminderBuilder { | |||||||
|     pool: MySqlPool, |     pool: MySqlPool, | ||||||
|     uid: String, |     uid: String, | ||||||
|     channel: u32, |     channel: u32, | ||||||
|     guild: Option<u32>, |  | ||||||
|     thread_id: Option<u64>, |  | ||||||
|     utc_time: NaiveDateTime, |     utc_time: NaiveDateTime, | ||||||
|     timezone: String, |     timezone: String, | ||||||
|     interval_seconds: Option<i64>, |     interval_secs: Option<i64>, | ||||||
|     interval_days: Option<i64>, |  | ||||||
|     interval_months: Option<i64>, |     interval_months: Option<i64>, | ||||||
|     expires: Option<NaiveDateTime>, |     expires: Option<NaiveDateTime>, | ||||||
|     content: String, |     content: String, | ||||||
| @@ -87,11 +84,9 @@ impl ReminderBuilder { | |||||||
| INSERT INTO reminders ( | INSERT INTO reminders ( | ||||||
|     `uid`, |     `uid`, | ||||||
|     `channel_id`, |     `channel_id`, | ||||||
|     `guild_id`, |  | ||||||
|     `utc_time`, |     `utc_time`, | ||||||
|     `timezone`, |     `timezone`, | ||||||
|     `interval_seconds`, |     `interval_seconds`, | ||||||
|     `interval_days`, |  | ||||||
|     `interval_months`, |     `interval_months`, | ||||||
|     `expires`, |     `expires`, | ||||||
|     `content`, |     `content`, | ||||||
| @@ -111,18 +106,14 @@ INSERT INTO reminders ( | |||||||
|     ?, |     ?, | ||||||
|     ?, |     ?, | ||||||
|     ?, |     ?, | ||||||
|     ?, |  | ||||||
|     ?, |  | ||||||
|     ? |     ? | ||||||
| ) | ) | ||||||
|             ", |             ", | ||||||
|                         self.uid, |                         self.uid, | ||||||
|                         self.channel, |                         self.channel, | ||||||
|                         self.guild, |  | ||||||
|                         utc_time, |                         utc_time, | ||||||
|                         self.timezone, |                         self.timezone, | ||||||
|                         self.interval_seconds, |                         self.interval_secs, | ||||||
|                         self.interval_days, |  | ||||||
|                         self.interval_months, |                         self.interval_months, | ||||||
|                         self.expires, |                         self.expires, | ||||||
|                         self.content, |                         self.content, | ||||||
| @@ -184,15 +175,17 @@ impl<'a> MultiReminderBuilder<'a> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn time<T: Into<i64>>(mut self, time: T) -> Self { |     pub fn time<T: Into<i64>>(mut self, time: T) -> Self { | ||||||
|         if let Some(utc_time) = NaiveDateTime::from_timestamp_opt(time.into(), 0) { |         self.utc_time = NaiveDateTime::from_timestamp(time.into(), 0); | ||||||
|             self.utc_time = utc_time; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         self |         self | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn expires<T: Into<i64>>(mut self, time: Option<T>) -> Self { |     pub fn expires<T: Into<i64>>(mut self, time: Option<T>) -> Self { | ||||||
|         self.expires = time.map(|t| NaiveDateTime::from_timestamp_opt(t.into(), 0)).flatten(); |         if let Some(t) = time { | ||||||
|  |             self.expires = Some(NaiveDateTime::from_timestamp(t.into(), 0)); | ||||||
|  |         } else { | ||||||
|  |             self.expires = None; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         self |         self | ||||||
|     } |     } | ||||||
| @@ -219,78 +212,63 @@ impl<'a> MultiReminderBuilder<'a> { | |||||||
|  |  | ||||||
|         let mut ok_locs = HashSet::new(); |         let mut ok_locs = HashSet::new(); | ||||||
|  |  | ||||||
|         if self |         if self.interval.map_or(false, |i| ((i.sec + i.month * 30 * DAY) as i64) < *MIN_INTERVAL) { | ||||||
|             .interval |  | ||||||
|             .map_or(false, |i| ((i.sec + i.day * DAY + i.month * 30 * DAY) as i64) < *MIN_INTERVAL) |  | ||||||
|         { |  | ||||||
|             errors.insert(ReminderError::ShortInterval); |             errors.insert(ReminderError::ShortInterval); | ||||||
|         } else if self |         } else if self.interval.map_or(false, |i| ((i.sec + i.month * 30 * DAY) as i64) > *MAX_TIME) | ||||||
|             .interval |  | ||||||
|             .map_or(false, |i| ((i.sec + i.day * DAY + i.month * 30 * DAY) as i64) > *MAX_TIME) |  | ||||||
|         { |         { | ||||||
|             errors.insert(ReminderError::LongInterval); |             errors.insert(ReminderError::LongInterval); | ||||||
|         } else { |         } else { | ||||||
|             for scope in self.scopes { |             for scope in self.scopes { | ||||||
|                 let thread_id = None; |  | ||||||
|                 let db_channel_id = match scope { |                 let db_channel_id = match scope { | ||||||
|                     ReminderScope::User(user_id) => { |                     ReminderScope::User(user_id) => { | ||||||
|                         if let Ok(user) = UserId(user_id).to_user(&self.ctx).await { |                         if let Ok(user) = UserId(user_id).to_user(&self.ctx.discord()).await { | ||||||
|                             let user_data = UserData::from_user( |                             let user_data = UserData::from_user( | ||||||
|                                 &user, |                                 &user, | ||||||
|                                 &self.ctx.serenity_context(), |                                 &self.ctx.discord(), | ||||||
|                                 &self.ctx.data().database, |                                 &self.ctx.data().database, | ||||||
|                             ) |                             ) | ||||||
|                             .await |                             .await | ||||||
|                             .unwrap(); |                             .unwrap(); | ||||||
|  |  | ||||||
|                             if let Some(guild_id) = self.guild_id { |                             if let Some(guild_id) = self.guild_id { | ||||||
|                                 if guild_id.member(&self.ctx, user).await.is_err() { |                                 if guild_id.member(&self.ctx.discord(), user).await.is_err() { | ||||||
|                                     Err(ReminderError::InvalidTag) |                                     Err(ReminderError::InvalidTag) | ||||||
|                                 } else if self.set_by.map_or(true, |i| i != user_data.id) |                                 } else if self.set_by.map_or(true, |i| i != user_data.id) | ||||||
|                                     && !user_data.allowed_dm |                                     && !user_data.allowed_dm | ||||||
|                                 { |                                 { | ||||||
|                                     Err(ReminderError::UserBlockedDm) |                                     Err(ReminderError::UserBlockedDm) | ||||||
|                                 } else { |                                 } else { | ||||||
|                                     Ok((user_data.dm_channel, None)) |                                     Ok(user_data.dm_channel) | ||||||
|                                 } |                                 } | ||||||
|                             } else { |                             } else { | ||||||
|                                 Ok((user_data.dm_channel, None)) |                                 Ok(user_data.dm_channel) | ||||||
|                             } |                             } | ||||||
|                         } else { |                         } else { | ||||||
|                             Err(ReminderError::InvalidTag) |                             Err(ReminderError::InvalidTag) | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                     ReminderScope::Channel(channel_id) => { |                     ReminderScope::Channel(channel_id) => { | ||||||
|                         let channel = ChannelId(channel_id).to_channel(&self.ctx).await.unwrap(); |                         let channel = | ||||||
|  |                             ChannelId(channel_id).to_channel(&self.ctx.discord()).await.unwrap(); | ||||||
|  |  | ||||||
|                         if let Some(mut guild_channel) = channel.clone().guild() { |                         if let Some(guild_channel) = channel.clone().guild() { | ||||||
|                             if Some(guild_channel.guild_id) != self.guild_id { |                             if Some(guild_channel.guild_id) != self.guild_id { | ||||||
|                                 Err(ReminderError::InvalidTag) |                                 Err(ReminderError::InvalidTag) | ||||||
|                             } else { |                             } else { | ||||||
|                                 let mut channel_data = if guild_channel.kind |                                 let mut channel_data = | ||||||
|                                     == ChannelType::PublicThread |  | ||||||
|                                 { |  | ||||||
|                                     // fixme jesus christ |  | ||||||
|                                     let parent = guild_channel |  | ||||||
|                                         .parent_id |  | ||||||
|                                         .unwrap() |  | ||||||
|                                         .to_channel(&self.ctx) |  | ||||||
|                                         .await |  | ||||||
|                                         .unwrap(); |  | ||||||
|                                     guild_channel = parent.clone().guild().unwrap(); |  | ||||||
|                                     ChannelData::from_channel(&parent, &self.ctx.data().database) |  | ||||||
|                                         .await |  | ||||||
|                                         .unwrap() |  | ||||||
|                                 } else { |  | ||||||
|                                     ChannelData::from_channel(&channel, &self.ctx.data().database) |                                     ChannelData::from_channel(&channel, &self.ctx.data().database) | ||||||
|                                         .await |                                         .await | ||||||
|                                         .unwrap() |                                         .unwrap(); | ||||||
|                                 }; |  | ||||||
|  |  | ||||||
|                                 if channel_data.webhook_id.is_none() |                                 if channel_data.webhook_id.is_none() | ||||||
|                                     || channel_data.webhook_token.is_none() |                                     || channel_data.webhook_token.is_none() | ||||||
|                                 { |                                 { | ||||||
|                                     match create_webhook(&self.ctx, guild_channel, "Reminder").await |                                     match create_webhook( | ||||||
|  |                                         &self.ctx.discord(), | ||||||
|  |                                         guild_channel, | ||||||
|  |                                         "Reminder", | ||||||
|  |                                     ) | ||||||
|  |                                     .await | ||||||
|                                     { |                                     { | ||||||
|                                         Ok(webhook) => { |                                         Ok(webhook) => { | ||||||
|                                             channel_data.webhook_id = |                                             channel_data.webhook_id = | ||||||
| @@ -301,13 +279,13 @@ impl<'a> MultiReminderBuilder<'a> { | |||||||
|                                                 .commit_changes(&self.ctx.data().database) |                                                 .commit_changes(&self.ctx.data().database) | ||||||
|                                                 .await; |                                                 .await; | ||||||
|  |  | ||||||
|                                             Ok((channel_data.id, channel_data.db_guild_id)) |                                             Ok(channel_data.id) | ||||||
|                                         } |                                         } | ||||||
|  |  | ||||||
|                                         Err(e) => Err(ReminderError::DiscordError(e.to_string())), |                                         Err(e) => Err(ReminderError::DiscordError(e.to_string())), | ||||||
|                                     } |                                     } | ||||||
|                                 } else { |                                 } else { | ||||||
|                                     Ok((channel_data.id, channel_data.db_guild_id)) |                                     Ok(channel_data.id) | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                         } else { |                         } else { | ||||||
| @@ -321,13 +299,10 @@ impl<'a> MultiReminderBuilder<'a> { | |||||||
|                         let builder = ReminderBuilder { |                         let builder = ReminderBuilder { | ||||||
|                             pool: self.ctx.data().database.clone(), |                             pool: self.ctx.data().database.clone(), | ||||||
|                             uid: generate_uid(), |                             uid: generate_uid(), | ||||||
|                             channel: c.0, |                             channel: c, | ||||||
|                             guild: c.1, |  | ||||||
|                             thread_id, |  | ||||||
|                             utc_time: self.utc_time, |                             utc_time: self.utc_time, | ||||||
|                             timezone: self.timezone.to_string(), |                             timezone: self.timezone.to_string(), | ||||||
|                             interval_seconds: self.interval.map(|i| i.sec as i64), |                             interval_secs: self.interval.map(|i| i.sec as i64), | ||||||
|                             interval_days: self.interval.map(|i| i.day as i64), |  | ||||||
|                             interval_months: self.interval.map(|i| i.month as i64), |                             interval_months: self.interval.map(|i| i.month as i64), | ||||||
|                             expires: self.expires, |                             expires: self.expires, | ||||||
|                             content: self.content.content.clone(), |                             content: self.content.content.clone(), | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ pub mod look_flags; | |||||||
|  |  | ||||||
| use std::hash::{Hash, Hasher}; | use std::hash::{Hash, Hasher}; | ||||||
|  |  | ||||||
| use chrono::{DateTime, NaiveDateTime, Utc}; | use chrono::{NaiveDateTime, TimeZone}; | ||||||
| use chrono_tz::Tz; | use chrono_tz::Tz; | ||||||
| use poise::serenity_prelude::{ | use poise::serenity_prelude::{ | ||||||
|     model::id::{ChannelId, GuildId, UserId}, |     model::id::{ChannelId, GuildId, UserId}, | ||||||
| @@ -24,9 +24,8 @@ pub struct Reminder { | |||||||
|     pub id: u32, |     pub id: u32, | ||||||
|     pub uid: String, |     pub uid: String, | ||||||
|     pub channel: u64, |     pub channel: u64, | ||||||
|     pub utc_time: DateTime<Utc>, |     pub utc_time: NaiveDateTime, | ||||||
|     pub interval_seconds: Option<u32>, |     pub interval_seconds: Option<u32>, | ||||||
|     pub interval_days: Option<u32>, |  | ||||||
|     pub interval_months: Option<u32>, |     pub interval_months: Option<u32>, | ||||||
|     pub expires: Option<NaiveDateTime>, |     pub expires: Option<NaiveDateTime>, | ||||||
|     pub enabled: bool, |     pub enabled: bool, | ||||||
| @@ -60,7 +59,6 @@ SELECT | |||||||
|     channels.channel, |     channels.channel, | ||||||
|     reminders.utc_time, |     reminders.utc_time, | ||||||
|     reminders.interval_seconds, |     reminders.interval_seconds, | ||||||
|     reminders.interval_days, |  | ||||||
|     reminders.interval_months, |     reminders.interval_months, | ||||||
|     reminders.expires, |     reminders.expires, | ||||||
|     reminders.enabled, |     reminders.enabled, | ||||||
| @@ -97,7 +95,6 @@ SELECT | |||||||
|     channels.channel, |     channels.channel, | ||||||
|     reminders.utc_time, |     reminders.utc_time, | ||||||
|     reminders.interval_seconds, |     reminders.interval_seconds, | ||||||
|     reminders.interval_days, |  | ||||||
|     reminders.interval_months, |     reminders.interval_months, | ||||||
|     reminders.expires, |     reminders.expires, | ||||||
|     reminders.enabled, |     reminders.enabled, | ||||||
| @@ -141,7 +138,6 @@ SELECT | |||||||
|     channels.channel, |     channels.channel, | ||||||
|     reminders.utc_time, |     reminders.utc_time, | ||||||
|     reminders.interval_seconds, |     reminders.interval_seconds, | ||||||
|     reminders.interval_days, |  | ||||||
|     reminders.interval_months, |     reminders.interval_months, | ||||||
|     reminders.expires, |     reminders.expires, | ||||||
|     reminders.enabled, |     reminders.enabled, | ||||||
| @@ -159,7 +155,6 @@ LEFT JOIN | |||||||
| ON | ON | ||||||
|     reminders.set_by = users.id |     reminders.set_by = users.id | ||||||
| WHERE | WHERE | ||||||
|     `status` = 'pending' AND |  | ||||||
|     channels.channel = ? AND |     channels.channel = ? AND | ||||||
|     FIND_IN_SET(reminders.enabled, ?) |     FIND_IN_SET(reminders.enabled, ?) | ||||||
| ORDER BY | ORDER BY | ||||||
| @@ -200,7 +195,6 @@ SELECT | |||||||
|     channels.channel, |     channels.channel, | ||||||
|     reminders.utc_time, |     reminders.utc_time, | ||||||
|     reminders.interval_seconds, |     reminders.interval_seconds, | ||||||
|     reminders.interval_days, |  | ||||||
|     reminders.interval_months, |     reminders.interval_months, | ||||||
|     reminders.expires, |     reminders.expires, | ||||||
|     reminders.enabled, |     reminders.enabled, | ||||||
| @@ -218,7 +212,6 @@ LEFT JOIN | |||||||
| ON | ON | ||||||
|     reminders.set_by = users.id |     reminders.set_by = users.id | ||||||
| WHERE | WHERE | ||||||
|     `status` = 'pending' AND |  | ||||||
|     FIND_IN_SET(channels.channel, ?) |     FIND_IN_SET(channels.channel, ?) | ||||||
|                 ", |                 ", | ||||||
|                     channels |                     channels | ||||||
| @@ -235,7 +228,6 @@ SELECT | |||||||
|     channels.channel, |     channels.channel, | ||||||
|     reminders.utc_time, |     reminders.utc_time, | ||||||
|     reminders.interval_seconds, |     reminders.interval_seconds, | ||||||
|     reminders.interval_days, |  | ||||||
|     reminders.interval_months, |     reminders.interval_months, | ||||||
|     reminders.expires, |     reminders.expires, | ||||||
|     reminders.enabled, |     reminders.enabled, | ||||||
| @@ -253,7 +245,6 @@ LEFT JOIN | |||||||
| ON | ON | ||||||
|     reminders.set_by = users.id |     reminders.set_by = users.id | ||||||
| WHERE | WHERE | ||||||
|     `status` = 'pending' AND |  | ||||||
|     channels.guild_id = (SELECT id FROM guilds WHERE guild = ?) |     channels.guild_id = (SELECT id FROM guilds WHERE guild = ?) | ||||||
|                 ", |                 ", | ||||||
|                     guild_id.as_u64() |                     guild_id.as_u64() | ||||||
| @@ -271,7 +262,6 @@ SELECT | |||||||
|     channels.channel, |     channels.channel, | ||||||
|     reminders.utc_time, |     reminders.utc_time, | ||||||
|     reminders.interval_seconds, |     reminders.interval_seconds, | ||||||
|     reminders.interval_days, |  | ||||||
|     reminders.interval_months, |     reminders.interval_months, | ||||||
|     reminders.expires, |     reminders.expires, | ||||||
|     reminders.enabled, |     reminders.enabled, | ||||||
| @@ -289,7 +279,6 @@ LEFT JOIN | |||||||
| ON | ON | ||||||
|     reminders.set_by = users.id |     reminders.set_by = users.id | ||||||
| WHERE | WHERE | ||||||
|     `status` = 'pending' AND |  | ||||||
|     channels.id = (SELECT dm_channel FROM users WHERE user = ?) |     channels.id = (SELECT dm_channel FROM users WHERE user = ?) | ||||||
|             ", |             ", | ||||||
|                 user.as_u64() |                 user.as_u64() | ||||||
| @@ -304,13 +293,7 @@ WHERE | |||||||
|         &self, |         &self, | ||||||
|         db: impl Executor<'_, Database = Database>, |         db: impl Executor<'_, Database = Database>, | ||||||
|     ) -> Result<(), sqlx::Error> { |     ) -> Result<(), sqlx::Error> { | ||||||
|         sqlx::query!( |         sqlx::query!("DELETE FROM reminders WHERE uid = ?", self.uid).execute(db).await.map(|_| ()) | ||||||
|             "UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?", |  | ||||||
|             self.uid |  | ||||||
|         ) |  | ||||||
|         .execute(db) |  | ||||||
|         .await |  | ||||||
|         .map(|_| ()) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn display_content(&self) -> &str { |     pub fn display_content(&self) -> &str { | ||||||
| @@ -327,32 +310,30 @@ WHERE | |||||||
|             count + 1, |             count + 1, | ||||||
|             self.display_content(), |             self.display_content(), | ||||||
|             self.channel, |             self.channel, | ||||||
|             self.utc_time.with_timezone(timezone).format("%Y-%m-%d %H:%M:%S") |             timezone.timestamp(self.utc_time.timestamp(), 0).format("%Y-%m-%d %H:%M:%S") | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn display(&self, flags: &LookFlags, timezone: &Tz) -> String { |     pub fn display(&self, flags: &LookFlags, timezone: &Tz) -> String { | ||||||
|         let time_display = match flags.time_display { |         let time_display = match flags.time_display { | ||||||
|             TimeDisplayType::Absolute => { |             TimeDisplayType::Absolute => timezone | ||||||
|                 self.utc_time.with_timezone(timezone).format("%Y-%m-%d %H:%M:%S").to_string() |                 .timestamp(self.utc_time.timestamp(), 0) | ||||||
|             } |                 .format("%Y-%m-%d %H:%M:%S") | ||||||
|  |                 .to_string(), | ||||||
|  |  | ||||||
|             TimeDisplayType::Relative => format!("<t:{}:R>", self.utc_time.timestamp()), |             TimeDisplayType::Relative => format!("<t:{}:R>", self.utc_time.timestamp()), | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         if self.interval_seconds.is_some() |         if self.interval_seconds.is_some() || self.interval_months.is_some() { | ||||||
|             || self.interval_days.is_some() |  | ||||||
|             || self.interval_months.is_some() |  | ||||||
|         { |  | ||||||
|             format!( |             format!( | ||||||
|                 "'{}' *occurs next at* **{}**, repeating (set by {})\n", |                 "'{}' *occurs next at* **{}**, repeating (set by {})", | ||||||
|                 self.display_content(), |                 self.display_content(), | ||||||
|                 time_display, |                 time_display, | ||||||
|                 self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string()) |                 self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string()) | ||||||
|             ) |             ) | ||||||
|         } else { |         } else { | ||||||
|             format!( |             format!( | ||||||
|                 "'{}' *occurs next at* **{}** (set by {})\n", |                 "'{}' *occurs next at* **{}** (set by {})", | ||||||
|                 self.display_content(), |                 self.display_content(), | ||||||
|                 time_display, |                 time_display, | ||||||
|                 self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string()) |                 self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string()) | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| use chrono::{DateTime, Utc}; | use chrono::NaiveDateTime; | ||||||
| use sqlx::MySqlPool; | use sqlx::MySqlPool; | ||||||
|  |  | ||||||
| pub struct Timer { | pub struct Timer { | ||||||
|     pub name: String, |     pub name: String, | ||||||
|     pub start_time: DateTime<Utc>, |     pub start_time: NaiveDateTime, | ||||||
|     pub owner: u64, |     pub owner: u64, | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ impl UserData { | |||||||
|  |  | ||||||
|         match sqlx::query!( |         match sqlx::query!( | ||||||
|             " |             " | ||||||
| SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ? | SELECT timezone FROM users WHERE user = ? | ||||||
|             ", |             ", | ||||||
|             user_id |             user_id | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -83,7 +83,7 @@ pub fn send_as_initial_response( | |||||||
|         components, |         components, | ||||||
|         ephemeral, |         ephemeral, | ||||||
|         allowed_mentions, |         allowed_mentions, | ||||||
|         reply: _, |         reference_message: _, // can't reply to a message in interactions | ||||||
|     } = data; |     } = data; | ||||||
|  |  | ||||||
|     if let Some(content) = content { |     if let Some(content) = content { | ||||||
|   | |||||||
| @@ -1,14 +0,0 @@ | |||||||
| [Unit] |  | ||||||
| Description=Reminder Bot |  | ||||||
|  |  | ||||||
| [Service] |  | ||||||
| User=reminder |  | ||||||
| Type=simple |  | ||||||
| ExecStart=/usr/bin/reminder-rs |  | ||||||
| WorkingDirectory=/etc/reminder-rs |  | ||||||
| Restart=always |  | ||||||
| RestartSec=4 |  | ||||||
| Environment="reminder_rs=warn,postman=warn" |  | ||||||
|  |  | ||||||
| [Install] |  | ||||||
| WantedBy=multi-user.target |  | ||||||
| @@ -7,14 +7,14 @@ edition = "2018" | |||||||
| [dependencies] | [dependencies] | ||||||
| rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] } | rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] } | ||||||
| rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] } | rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] } | ||||||
| serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } | serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } | ||||||
| oauth2 = "4" | oauth2 = "4" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| reqwest = "0.11" | reqwest = "0.11" | ||||||
| serde = { version = "1.0", features = ["derive"] } | serde = { version = "1.0", features = ["derive"] } | ||||||
| sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] } | sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] } | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| chrono-tz = "0.8" | chrono-tz = "0.5" | ||||||
| lazy_static = "1.4.0" | lazy_static = "1.4.0" | ||||||
| rand = "0.7" | rand = "0.7" | ||||||
| base64 = "0.13" | base64 = "0.13" | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ pub const DISCORD_OAUTH_TOKEN: &'static str = "https://discord.com/api/oauth2/to | |||||||
| pub const DISCORD_OAUTH_AUTHORIZE: &'static str = "https://discord.com/api/oauth2/authorize"; | pub const DISCORD_OAUTH_AUTHORIZE: &'static str = "https://discord.com/api/oauth2/authorize"; | ||||||
| pub const DISCORD_API: &'static str = "https://discord.com/api"; | pub const DISCORD_API: &'static str = "https://discord.com/api"; | ||||||
|  |  | ||||||
| pub const MAX_NAME_LENGTH: usize = 100; |  | ||||||
| pub const MAX_CONTENT_LENGTH: usize = 2000; | pub const MAX_CONTENT_LENGTH: usize = 2000; | ||||||
| pub const MAX_EMBED_DESCRIPTION_LENGTH: usize = 4096; | pub const MAX_EMBED_DESCRIPTION_LENGTH: usize = 4096; | ||||||
| pub const MAX_EMBED_TITLE_LENGTH: usize = 256; | pub const MAX_EMBED_TITLE_LENGTH: usize = 256; | ||||||
| @@ -32,7 +31,7 @@ lazy_static! { | |||||||
|     ) |     ) | ||||||
|         .into(); |         .into(); | ||||||
|     pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter( |     pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter( | ||||||
|         env::var("PATREON_ROLE_ID") |         env::var("SUBSCRIPTION_ROLES") | ||||||
|             .map(|var| var |             .map(|var| var | ||||||
|                 .split(',') |                 .split(',') | ||||||
|                 .filter_map(|item| { item.parse::<u64>().ok() }) |                 .filter_map(|item| { item.parse::<u64>().ok() }) | ||||||
| @@ -40,7 +39,7 @@ lazy_static! { | |||||||
|             .unwrap_or_else(|_| Vec::new()) |             .unwrap_or_else(|_| Vec::new()) | ||||||
|     ); |     ); | ||||||
|     pub static ref CNC_GUILD: Option<u64> = |     pub static ref CNC_GUILD: Option<u64> = | ||||||
|         env::var("PATREON_GUILD_ID").map(|var| var.parse::<u64>().ok()).ok().flatten(); |         env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten(); | ||||||
|     pub static ref MIN_INTERVAL: u32 = env::var("MIN_INTERVAL") |     pub static ref MIN_INTERVAL: u32 = env::var("MIN_INTERVAL") | ||||||
|         .ok() |         .ok() | ||||||
|         .map(|inner| inner.parse::<u32>().ok()) |         .map(|inner| inner.parse::<u32>().ok()) | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ mod consts; | |||||||
| mod macros; | mod macros; | ||||||
| mod routes; | mod routes; | ||||||
|  |  | ||||||
| use std::{collections::HashMap, env, path::Path}; | use std::{collections::HashMap, env}; | ||||||
|  |  | ||||||
| use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; | use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; | ||||||
| use rocket::{ | use rocket::{ | ||||||
| @@ -72,14 +72,10 @@ pub async fn initialize( | |||||||
|     db_pool: Pool<Database>, |     db_pool: Pool<Database>, | ||||||
| ) -> Result<(), Box<dyn std::error::Error>> { | ) -> Result<(), Box<dyn std::error::Error>> { | ||||||
|     info!("Checking environment variables..."); |     info!("Checking environment variables..."); | ||||||
|  |  | ||||||
|     if env::var("OFFLINE").map_or(true, |v| v != "1") { |  | ||||||
|     env::var("OAUTH2_CLIENT_ID").expect("`OAUTH2_CLIENT_ID' not supplied"); |     env::var("OAUTH2_CLIENT_ID").expect("`OAUTH2_CLIENT_ID' not supplied"); | ||||||
|     env::var("OAUTH2_CLIENT_SECRET").expect("`OAUTH2_CLIENT_SECRET' not supplied"); |     env::var("OAUTH2_CLIENT_SECRET").expect("`OAUTH2_CLIENT_SECRET' not supplied"); | ||||||
|     env::var("OAUTH2_DISCORD_CALLBACK").expect("`OAUTH2_DISCORD_CALLBACK' not supplied"); |     env::var("OAUTH2_DISCORD_CALLBACK").expect("`OAUTH2_DISCORD_CALLBACK' not supplied"); | ||||||
|         env::var("PATREON_GUILD_ID").expect("`PATREON_GUILD_ID' not supplied"); |     env::var("PATREON_GUILD_ID").expect("`PATREON_GUILD' not supplied"); | ||||||
|     } |  | ||||||
|  |  | ||||||
|     info!("Done!"); |     info!("Done!"); | ||||||
|  |  | ||||||
|     let oauth2_client = BasicClient::new( |     let oauth2_client = BasicClient::new( | ||||||
| @@ -92,9 +88,6 @@ pub async fn initialize( | |||||||
|  |  | ||||||
|     let reqwest_client = reqwest::Client::new(); |     let reqwest_client = reqwest::Client::new(); | ||||||
|  |  | ||||||
|     let static_path = |  | ||||||
|         if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" }; |  | ||||||
|  |  | ||||||
|     rocket::build() |     rocket::build() | ||||||
|         .attach(Template::fairing()) |         .attach(Template::fairing()) | ||||||
|         .register( |         .register( | ||||||
| @@ -112,7 +105,7 @@ pub async fn initialize( | |||||||
|         .manage(reqwest_client) |         .manage(reqwest_client) | ||||||
|         .manage(serenity_context) |         .manage(serenity_context) | ||||||
|         .manage(db_pool) |         .manage(db_pool) | ||||||
|         .mount("/static", FileServer::from(static_path)) |         .mount("/static", FileServer::from(concat!(env!("CARGO_MANIFEST_DIR"), "/static"))) | ||||||
|         .mount( |         .mount( | ||||||
|             "/", |             "/", | ||||||
|             routes![ |             routes![ | ||||||
| @@ -120,8 +113,7 @@ pub async fn initialize( | |||||||
|                 routes::cookies, |                 routes::cookies, | ||||||
|                 routes::privacy, |                 routes::privacy, | ||||||
|                 routes::terms, |                 routes::terms, | ||||||
|                 routes::return_to_same_site, |                 routes::return_to_same_site | ||||||
|                 routes::report::report_error, |  | ||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|         .mount( |         .mount( | ||||||
| @@ -139,19 +131,11 @@ pub async fn initialize( | |||||||
|                 routes::help_iemanager, |                 routes::help_iemanager, | ||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|         .mount( |         .mount("/login", routes![routes::login::discord_login, routes::login::discord_callback]) | ||||||
|             "/login", |  | ||||||
|             routes![ |  | ||||||
|                 routes::login::discord_login, |  | ||||||
|                 routes::login::discord_logout, |  | ||||||
|                 routes::login::discord_callback |  | ||||||
|             ], |  | ||||||
|         ) |  | ||||||
|         .mount( |         .mount( | ||||||
|             "/dashboard", |             "/dashboard", | ||||||
|             routes![ |             routes![ | ||||||
|                 routes::dashboard::dashboard_1, |                 routes::dashboard::dashboard, | ||||||
|                 routes::dashboard::dashboard_2, |  | ||||||
|                 routes::dashboard::dashboard_home, |                 routes::dashboard::dashboard_home, | ||||||
|                 routes::dashboard::user::get_user_info, |                 routes::dashboard::user::get_user_info, | ||||||
|                 routes::dashboard::user::update_user_info, |                 routes::dashboard::user::update_user_info, | ||||||
| @@ -173,7 +157,6 @@ pub async fn initialize( | |||||||
|                 routes::dashboard::export::import_todos, |                 routes::dashboard::export::import_todos, | ||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|         .mount("/admin", routes![routes::admin::admin_dashboard_home, routes::admin::bot_data]) |  | ||||||
|         .launch() |         .launch() | ||||||
|         .await?; |         .await?; | ||||||
|  |  | ||||||
| @@ -190,8 +173,6 @@ pub async fn initialize( | |||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool { | pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool { | ||||||
|     offline!(true); |  | ||||||
|  |  | ||||||
|     if let Some(subscription_guild) = *CNC_GUILD { |     if let Some(subscription_guild) = *CNC_GUILD { | ||||||
|         let guild_member = GuildId(subscription_guild).member(cache_http, user_id).await; |         let guild_member = GuildId(subscription_guild).member(cache_http, user_id).await; | ||||||
|  |  | ||||||
| @@ -213,8 +194,6 @@ pub async fn check_guild_subscription( | |||||||
|     cache_http: impl CacheHttp, |     cache_http: impl CacheHttp, | ||||||
|     guild_id: impl Into<GuildId>, |     guild_id: impl Into<GuildId>, | ||||||
| ) -> bool { | ) -> bool { | ||||||
|     offline!(true); |  | ||||||
|  |  | ||||||
|     if let Some(guild) = cache_http.cache().unwrap().guild(guild_id) { |     if let Some(guild) = cache_http.cache().unwrap().guild(guild_id) { | ||||||
|         let owner = guild.owner_id; |         let owner = guild.owner_id; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,11 +1,3 @@ | |||||||
| macro_rules! offline { |  | ||||||
|     ($field:expr) => { |  | ||||||
|         if std::env::var("OFFLINE").map_or(false, |v| v == "1") { |  | ||||||
|             return $field; |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| macro_rules! check_length { | macro_rules! check_length { | ||||||
|     ($max:ident, $field:expr) => { |     ($max:ident, $field:expr) => { | ||||||
|         if $field.len() > $max { |         if $field.len() > $max { | ||||||
| @@ -60,33 +52,18 @@ macro_rules! check_authorization { | |||||||
|  |  | ||||||
|         let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten(); |         let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten(); | ||||||
|  |  | ||||||
|         if std::env::var("OFFLINE").map_or(true, |v| v != "1") { |  | ||||||
|         match user_id { |         match user_id { | ||||||
|             Some(user_id) => { |             Some(user_id) => { | ||||||
|                 match GuildId($guild).to_guild_cached($ctx) { |                 match GuildId($guild).to_guild_cached($ctx) { | ||||||
|                     Some(guild) => { |                     Some(guild) => { | ||||||
|                             let member_res = guild.member($ctx, UserId(user_id)).await; |                         let member = guild.member($ctx, UserId(user_id)).await; | ||||||
|  |  | ||||||
|                             match member_res { |                         match member { | ||||||
|                             Err(_) => { |                             Err(_) => { | ||||||
|                                 return Err(json!({"error": "User not in guild"})); |                                 return Err(json!({"error": "User not in guild"})); | ||||||
|                             } |                             } | ||||||
|  |  | ||||||
|                                 Ok(member) => { |                             Ok(_) => {} | ||||||
|                                     let permissions_res = member.permissions($ctx); |  | ||||||
|  |  | ||||||
|                                     match permissions_res { |  | ||||||
|                                         Err(_) => { |  | ||||||
|                                             return Err(json!({"error": "Couldn't fetch permissions"})); |  | ||||||
|                                         } |  | ||||||
|  |  | ||||||
|                                         Ok(permissions) => { |  | ||||||
|                                             if !(permissions.manage_messages() || permissions.manage_guild() || permissions.administrator()) { |  | ||||||
|                                                 return Err(json!({"error": "Incorrect permissions"})); |  | ||||||
|                                             } |  | ||||||
|                                         } |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
| @@ -101,7 +78,6 @@ macro_rules! check_authorization { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| macro_rules! update_field { | macro_rules! update_field { | ||||||
|   | |||||||
| @@ -1,218 +0,0 @@ | |||||||
| use std::{collections::HashMap, env}; |  | ||||||
|  |  | ||||||
| use chrono::{DateTime, Utc}; |  | ||||||
| use rocket::{ |  | ||||||
|     http::{CookieJar, Status}, |  | ||||||
|     serde::json::json, |  | ||||||
|     State, |  | ||||||
| }; |  | ||||||
| use rocket_dyn_templates::Template; |  | ||||||
| use serde::Serialize; |  | ||||||
| use sqlx::{MySql, Pool}; |  | ||||||
|  |  | ||||||
| use crate::routes::JsonResult; |  | ||||||
|  |  | ||||||
| fn is_admin(cookies: &CookieJar<'_>) -> bool { |  | ||||||
|     cookies |  | ||||||
|         .get_private("userid") |  | ||||||
|         .map_or(false, |cookie| Some(cookie.value().to_string()) == env::var("ADMIN_ID").ok()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/")] |  | ||||||
| pub async fn admin_dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Status> { |  | ||||||
|     if let Some(cookie) = cookies.get_private("userid") { |  | ||||||
|         let map: HashMap<&str, String> = HashMap::new(); |  | ||||||
|         if Some(cookie.value().to_string()) == env::var("ADMIN_ID").ok() { |  | ||||||
|             Ok(Template::render("admin_dashboard", &map)) |  | ||||||
|         } else { |  | ||||||
|             Err(Status::Forbidden) |  | ||||||
|         } |  | ||||||
|     } else { |  | ||||||
|         Err(Status::Unauthorized) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Serialize)] |  | ||||||
| struct TimeFrame { |  | ||||||
|     time_key: DateTime<Utc>, |  | ||||||
|     count: i64, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/data")] |  | ||||||
| pub async fn bot_data(cookies: &CookieJar<'_>, pool: &State<Pool<MySql>>) -> JsonResult { |  | ||||||
|     if !is_admin(cookies) { |  | ||||||
|         return json_err!("Not authorized"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let backlog = sqlx::query!( |  | ||||||
|         "SELECT COUNT(1) AS backlog FROM reminders WHERE `utc_time` < NOW() AND enabled = 1 AND `status` = 'pending'" |  | ||||||
|     ) |  | ||||||
|     .fetch_one(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
|  |  | ||||||
|     let schedule_once = sqlx::query_as_unchecked!( |  | ||||||
|         TimeFrame, |  | ||||||
|         "SELECT |  | ||||||
|             FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 300) * 300) AS `time_key`, |  | ||||||
|             COUNT(1) AS `count` |  | ||||||
|         FROM reminders |  | ||||||
|         WHERE |  | ||||||
|             `utc_time` < DATE_ADD(NOW(), INTERVAL 1 DAY) AND |  | ||||||
|             `utc_time` >= NOW() AND |  | ||||||
|             `enabled` = 1 AND |  | ||||||
|             `status` = 'pending' AND |  | ||||||
|             `interval_seconds` IS NULL AND |  | ||||||
|             `interval_months` IS NULL AND |  | ||||||
|             `interval_days` IS NULL |  | ||||||
|         GROUP BY `time_key` |  | ||||||
|         ORDER BY `time_key`" |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
|  |  | ||||||
|     let schedule_interval = sqlx::query_as_unchecked!( |  | ||||||
|         TimeFrame, |  | ||||||
|         "SELECT |  | ||||||
|             FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 300) * 300) AS `time_key`, |  | ||||||
|             COUNT(1) AS `count` |  | ||||||
|         FROM reminders |  | ||||||
|         WHERE |  | ||||||
|             `utc_time` < DATE_ADD(NOW(), INTERVAL 1 DAY) AND |  | ||||||
|             `utc_time` >= NOW() AND |  | ||||||
|             `status` = 'pending' AND |  | ||||||
|             `enabled` = 1 AND ( |  | ||||||
|                 `interval_seconds` IS NOT NULL OR |  | ||||||
|                 `interval_months` IS NOT NULL OR |  | ||||||
|                 `interval_days` IS NOT NULL |  | ||||||
|             ) |  | ||||||
|         GROUP BY `time_key` |  | ||||||
|         ORDER BY `time_key`" |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
|  |  | ||||||
|     let schedule_once_long = sqlx::query_as_unchecked!( |  | ||||||
|         TimeFrame, |  | ||||||
|         "SELECT |  | ||||||
|             FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 86400) * 86400) AS `time_key`, |  | ||||||
|             COUNT(1) AS `count` |  | ||||||
|         FROM reminders |  | ||||||
|         WHERE |  | ||||||
|             `utc_time` < DATE_ADD(NOW(), INTERVAL 31 DAY) AND |  | ||||||
|             `utc_time` >= NOW() AND |  | ||||||
|             `enabled` = 1 AND |  | ||||||
|             `status` = 'pending' AND |  | ||||||
|             `interval_seconds` IS NULL AND |  | ||||||
|             `interval_months` IS NULL AND |  | ||||||
|             `interval_days` IS NULL |  | ||||||
|         GROUP BY `time_key` |  | ||||||
|         ORDER BY `time_key`" |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
|  |  | ||||||
|     let schedule_interval_long = sqlx::query_as_unchecked!( |  | ||||||
|         TimeFrame, |  | ||||||
|         "SELECT |  | ||||||
|             FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 86400) * 86400) AS `time_key`, |  | ||||||
|             COUNT(1) AS `count` |  | ||||||
|         FROM reminders |  | ||||||
|         WHERE |  | ||||||
|             `utc_time` < DATE_ADD(NOW(), INTERVAL 31 DAY) AND |  | ||||||
|             `utc_time` >= NOW() AND |  | ||||||
|             `status` = 'pending' AND |  | ||||||
|             `enabled` = 1 AND ( |  | ||||||
|                 `interval_seconds` IS NOT NULL OR |  | ||||||
|                 `interval_months` IS NOT NULL OR |  | ||||||
|                 `interval_days` IS NOT NULL |  | ||||||
|             ) |  | ||||||
|         GROUP BY `time_key` |  | ||||||
|         ORDER BY `time_key`" |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
|  |  | ||||||
|     let history = sqlx::query_as_unchecked!( |  | ||||||
|         TimeFrame, |  | ||||||
|         "SELECT |  | ||||||
|             FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 86400) * 86400) AS `time_key`, |  | ||||||
|             COUNT(1) AS `count` |  | ||||||
|         FROM stat |  | ||||||
|         WHERE |  | ||||||
|             `utc_time` > DATE_SUB(NOW(), INTERVAL 31 DAY) AND |  | ||||||
|             `type` = 'reminder_sent' |  | ||||||
|         GROUP BY `time_key` |  | ||||||
|         ORDER BY `time_key`" |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
|  |  | ||||||
|     let history_failed = sqlx::query_as_unchecked!( |  | ||||||
|         TimeFrame, |  | ||||||
|         "SELECT |  | ||||||
|             FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 86400) * 86400) AS `time_key`, |  | ||||||
|             COUNT(1) AS `count` |  | ||||||
|         FROM stat |  | ||||||
|         WHERE |  | ||||||
|             `utc_time` > DATE_SUB(NOW(), INTERVAL 31 DAY) AND |  | ||||||
|             `type` = 'reminder_failed' |  | ||||||
|         GROUP BY `time_key` |  | ||||||
|         ORDER BY `time_key`" |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
|  |  | ||||||
|     let interval_count = sqlx::query!( |  | ||||||
|         "SELECT COUNT(1) AS count |  | ||||||
|         FROM reminders |  | ||||||
|         WHERE |  | ||||||
|             `status` = 'pending' AND ( |  | ||||||
|                 `interval_seconds` IS NOT NULL OR |  | ||||||
|                 `interval_months` IS NOT NULL OR |  | ||||||
|                 `interval_days` IS NOT NULL |  | ||||||
|             )" |  | ||||||
|     ) |  | ||||||
|     .fetch_one(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
|  |  | ||||||
|     let reminder_count = sqlx::query!( |  | ||||||
|         "SELECT COUNT(1) AS count |  | ||||||
|         FROM reminders |  | ||||||
|         WHERE |  | ||||||
|             `status` = 'pending' AND |  | ||||||
|             `interval_seconds` IS NULL AND |  | ||||||
|             `interval_months` IS NULL AND |  | ||||||
|             `interval_days` IS NULL" |  | ||||||
|     ) |  | ||||||
|     .fetch_one(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
|  |  | ||||||
|     Ok(json!({ |  | ||||||
|         "backlog": backlog.backlog, |  | ||||||
|         "scheduleShort": { |  | ||||||
|             "once": schedule_once, |  | ||||||
|             "interval": schedule_interval |  | ||||||
|         }, |  | ||||||
|         "scheduleLong": { |  | ||||||
|             "once": schedule_once_long, |  | ||||||
|             "interval": schedule_interval_long, |  | ||||||
|         }, |  | ||||||
|         "historyLong": { |  | ||||||
|             "sent": history, |  | ||||||
|             "failed": history_failed, |  | ||||||
|         }, |  | ||||||
|         "count": { |  | ||||||
|             "reminders": reminder_count.count, |  | ||||||
|             "intervals": interval_count.count, |  | ||||||
|         } |  | ||||||
|     })) |  | ||||||
| } |  | ||||||
| @@ -10,11 +10,9 @@ use serenity::{ | |||||||
| }; | }; | ||||||
| use sqlx::{MySql, Pool}; | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
| use crate::routes::{ | use crate::routes::dashboard::{ | ||||||
|     dashboard::{ |     create_reminder, generate_uid, ImportBody, JsonResult, Reminder, ReminderCsv, | ||||||
|         create_reminder, ImportBody, ReminderCreate, ReminderCsv, ReminderTemplateCsv, TodoCsv, |     ReminderTemplateCsv, TodoCsv, | ||||||
|     }, |  | ||||||
|     JsonResult, |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #[get("/api/guild/<id>/export/reminders")] | #[get("/api/guild/<id>/export/reminders")] | ||||||
| @@ -60,7 +58,6 @@ pub async fn export_reminders( | |||||||
|                  reminders.enabled, |                  reminders.enabled, | ||||||
|                  reminders.expires, |                  reminders.expires, | ||||||
|                  reminders.interval_seconds, |                  reminders.interval_seconds, | ||||||
|                  reminders.interval_days, |  | ||||||
|                  reminders.interval_months, |                  reminders.interval_months, | ||||||
|                  reminders.name, |                  reminders.name, | ||||||
|                  reminders.restartable, |                  reminders.restartable, | ||||||
| @@ -140,7 +137,7 @@ pub async fn import_reminders( | |||||||
|  |  | ||||||
|                         match channel_id.parse::<u64>() { |                         match channel_id.parse::<u64>() { | ||||||
|                             Ok(channel_id) => { |                             Ok(channel_id) => { | ||||||
|                                 let reminder = ReminderCreate { |                                 let reminder = Reminder { | ||||||
|                                     attachment: record.attachment, |                                     attachment: record.attachment, | ||||||
|                                     attachment_name: record.attachment_name, |                                     attachment_name: record.attachment_name, | ||||||
|                                     avatar: record.avatar, |                                     avatar: record.avatar, | ||||||
| @@ -162,11 +159,11 @@ pub async fn import_reminders( | |||||||
|                                     enabled: record.enabled, |                                     enabled: record.enabled, | ||||||
|                                     expires: record.expires, |                                     expires: record.expires, | ||||||
|                                     interval_seconds: record.interval_seconds, |                                     interval_seconds: record.interval_seconds, | ||||||
|                                     interval_days: record.interval_days, |  | ||||||
|                                     interval_months: record.interval_months, |                                     interval_months: record.interval_months, | ||||||
|                                     name: record.name, |                                     name: record.name, | ||||||
|                                     restartable: record.restartable, |                                     restartable: record.restartable, | ||||||
|                                     tts: record.tts, |                                     tts: record.tts, | ||||||
|  |                                     uid: generate_uid(), | ||||||
|                                     username: record.username, |                                     username: record.username, | ||||||
|                                     utc_time: record.utc_time, |                                     utc_time: record.utc_time, | ||||||
|                                 }; |                                 }; | ||||||
| @@ -321,6 +318,13 @@ pub async fn import_todos( | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 let _ = sqlx::query!( | ||||||
|  |                     "DELETE FROM todos WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||||
|  |                     id | ||||||
|  |                 ) | ||||||
|  |                 .execute(pool.inner()) | ||||||
|  |                 .await; | ||||||
|  |  | ||||||
|                 let query_str = format!( |                 let query_str = format!( | ||||||
|                     "INSERT INTO todos (value, channel_id, guild_id) VALUES {}", |                     "INSERT INTO todos (value, channel_id, guild_id) VALUES {}", | ||||||
|                     vec![query_placeholder].repeat(query_params.len()).join(",") |                     vec![query_placeholder].repeat(query_params.len()).join(",") | ||||||
|   | |||||||
| @@ -16,19 +16,14 @@ use serenity::{ | |||||||
| use sqlx::{MySql, Pool}; | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     check_guild_subscription, check_subscription, |  | ||||||
|     consts::{ |     consts::{ | ||||||
|         MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, |         MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, | ||||||
|         MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH, |         MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH, | ||||||
|         MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, |         MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, | ||||||
|         MIN_INTERVAL, |  | ||||||
|     }, |     }, | ||||||
|     routes::{ |     routes::dashboard::{ | ||||||
|         dashboard::{ |  | ||||||
|         create_database_channel, create_reminder, template_name_default, DeleteReminder, |         create_database_channel, create_reminder, template_name_default, DeleteReminder, | ||||||
|             DeleteReminderTemplate, PatchReminder, Reminder, ReminderCreate, ReminderTemplate, |         DeleteReminderTemplate, JsonResult, PatchReminder, Reminder, ReminderTemplate, | ||||||
|         }, |  | ||||||
|         JsonResult, |  | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -46,7 +41,6 @@ pub async fn get_guild_patreon( | |||||||
|     cookies: &CookieJar<'_>, |     cookies: &CookieJar<'_>, | ||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     offline!(Ok(json!({ "patreon": true }))); |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization!(cookies, ctx.inner(), id); | ||||||
|  |  | ||||||
|     match GuildId(id).to_guild_cached(ctx.inner()) { |     match GuildId(id).to_guild_cached(ctx.inner()) { | ||||||
| @@ -74,12 +68,6 @@ pub async fn get_guild_channels( | |||||||
|     cookies: &CookieJar<'_>, |     cookies: &CookieJar<'_>, | ||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     offline!(Ok(json!(vec![ChannelInfo { |  | ||||||
|         name: "general".to_string(), |  | ||||||
|         id: "1".to_string(), |  | ||||||
|         webhook_avatar: None, |  | ||||||
|         webhook_name: None, |  | ||||||
|     }]))); |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization!(cookies, ctx.inner(), id); | ||||||
|  |  | ||||||
|     match GuildId(id).to_guild_cached(ctx.inner()) { |     match GuildId(id).to_guild_cached(ctx.inner()) { | ||||||
| @@ -118,7 +106,6 @@ struct RoleInfo { | |||||||
|  |  | ||||||
| #[get("/api/guild/<id>/roles")] | #[get("/api/guild/<id>/roles")] | ||||||
| pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { | pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { | ||||||
|     offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }]))); |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization!(cookies, ctx.inner(), id); | ||||||
|  |  | ||||||
|     let roles_res = ctx.cache.guild_roles(id); |     let roles_res = ctx.cache.guild_roles(id); | ||||||
| @@ -260,9 +247,9 @@ pub async fn create_reminder_template( | |||||||
|             Ok(json!({})) |             Ok(json!({})) | ||||||
|         } |         } | ||||||
|         Err(e) => { |         Err(e) => { | ||||||
|             warn!("Could not create template for {}: {:?}", id, e); |             warn!("Could not fetch templates from {}: {:?}", id, e); | ||||||
|  |  | ||||||
|             json_err!("Could not create template") |             json_err!("Could not get templates") | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -298,7 +285,7 @@ pub async fn delete_reminder_template( | |||||||
| #[post("/api/guild/<id>/reminders", data = "<reminder>")] | #[post("/api/guild/<id>/reminders", data = "<reminder>")] | ||||||
| pub async fn create_guild_reminder( | pub async fn create_guild_reminder( | ||||||
|     id: u64, |     id: u64, | ||||||
|     reminder: Json<ReminderCreate>, |     reminder: Json<Reminder>, | ||||||
|     cookies: &CookieJar<'_>, |     cookies: &CookieJar<'_>, | ||||||
|     serenity_context: &State<Context>, |     serenity_context: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| @@ -318,22 +305,22 @@ pub async fn create_guild_reminder( | |||||||
|     .await |     .await | ||||||
| } | } | ||||||
|  |  | ||||||
| #[get("/api/guild/<id>/reminders?<status>")] | #[get("/api/guild/<id>/reminders")] | ||||||
| pub async fn get_reminders( | pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySql>>) -> JsonResult { | ||||||
|     id: u64, |     let channels_res = GuildId(id).channels(&ctx.inner()).await; | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     serenity_context: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
|     status: Option<String>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     check_authorization!(cookies, serenity_context.inner(), id); |  | ||||||
|  |  | ||||||
|     let status = status.unwrap_or("pending".to_string()); |     match channels_res { | ||||||
|  |         Ok(channels) => { | ||||||
|  |             let channels = channels | ||||||
|  |                 .keys() | ||||||
|  |                 .into_iter() | ||||||
|  |                 .map(|k| k.as_u64().to_string()) | ||||||
|  |                 .collect::<Vec<String>>() | ||||||
|  |                 .join(","); | ||||||
|  |  | ||||||
|             sqlx::query_as_unchecked!( |             sqlx::query_as_unchecked!( | ||||||
|                 Reminder, |                 Reminder, | ||||||
|         " |                 "SELECT | ||||||
|         SELECT |  | ||||||
|                  reminders.attachment, |                  reminders.attachment, | ||||||
|                  reminders.attachment_name, |                  reminders.attachment_name, | ||||||
|                  reminders.avatar, |                  reminders.avatar, | ||||||
| @@ -348,26 +335,21 @@ pub async fn get_reminders( | |||||||
|                  reminders.embed_image_url, |                  reminders.embed_image_url, | ||||||
|                  reminders.embed_thumbnail_url, |                  reminders.embed_thumbnail_url, | ||||||
|                  reminders.embed_title, |                  reminders.embed_title, | ||||||
|          IFNULL(reminders.embed_fields, '[]') AS embed_fields, |                  reminders.embed_fields, | ||||||
|                  reminders.enabled, |                  reminders.enabled, | ||||||
|                  reminders.expires, |                  reminders.expires, | ||||||
|                  reminders.interval_seconds, |                  reminders.interval_seconds, | ||||||
|          reminders.interval_days, |  | ||||||
|                  reminders.interval_months, |                  reminders.interval_months, | ||||||
|                  reminders.name, |                  reminders.name, | ||||||
|                  reminders.restartable, |                  reminders.restartable, | ||||||
|                  reminders.tts, |                  reminders.tts, | ||||||
|                  reminders.uid, |                  reminders.uid, | ||||||
|                  reminders.username, |                  reminders.username, | ||||||
|          reminders.utc_time, |                  reminders.utc_time | ||||||
|          reminders.status, |  | ||||||
|          reminders.status_change_time, |  | ||||||
|          reminders.status_message |  | ||||||
|                 FROM reminders |                 FROM reminders | ||||||
|                 LEFT JOIN channels ON channels.id = reminders.channel_id |                 LEFT JOIN channels ON channels.id = reminders.channel_id | ||||||
|         WHERE FIND_IN_SET(`status`, ?) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", |                 WHERE FIND_IN_SET(channels.channel, ?)", | ||||||
|         status, |                 channels | ||||||
|         id |  | ||||||
|             ) |             ) | ||||||
|             .fetch_all(pool.inner()) |             .fetch_all(pool.inner()) | ||||||
|             .await |             .await | ||||||
| @@ -377,6 +359,13 @@ pub async fn get_reminders( | |||||||
|  |  | ||||||
|                 json_err!("Could not load reminders") |                 json_err!("Could not load reminders") | ||||||
|             }) |             }) | ||||||
|  |         } | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Could not fetch channels from {}: {:?}", id, e); | ||||||
|  |  | ||||||
|  |             Ok(json!([])) | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[patch("/api/guild/<id>/reminders", data = "<reminder>")] | #[patch("/api/guild/<id>/reminders", data = "<reminder>")] | ||||||
| @@ -385,109 +374,35 @@ pub async fn edit_reminder( | |||||||
|     reminder: Json<PatchReminder>, |     reminder: Json<PatchReminder>, | ||||||
|     serenity_context: &State<Context>, |     serenity_context: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, serenity_context.inner(), id); |  | ||||||
|  |  | ||||||
|     let mut error = vec![]; |     let mut error = vec![]; | ||||||
|  |  | ||||||
|     let user_id = |  | ||||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); |  | ||||||
|  |  | ||||||
|     if reminder.message_ok() { |  | ||||||
|         update_field!(pool.inner(), error, reminder.[ |  | ||||||
|             content, |  | ||||||
|             embed_author, |  | ||||||
|             embed_description, |  | ||||||
|             embed_footer, |  | ||||||
|             embed_title, |  | ||||||
|             embed_fields, |  | ||||||
|             username |  | ||||||
|         ]); |  | ||||||
|     } else { |  | ||||||
|         error.push("Message exceeds limits.".to_string()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     update_field!(pool.inner(), error, reminder.[ |     update_field!(pool.inner(), error, reminder.[ | ||||||
|         attachment, |         attachment, | ||||||
|         attachment_name, |         attachment_name, | ||||||
|         avatar, |         avatar, | ||||||
|  |         content, | ||||||
|  |         embed_author, | ||||||
|         embed_author_url, |         embed_author_url, | ||||||
|         embed_color, |         embed_color, | ||||||
|  |         embed_description, | ||||||
|  |         embed_footer, | ||||||
|         embed_footer_url, |         embed_footer_url, | ||||||
|         embed_image_url, |         embed_image_url, | ||||||
|         embed_thumbnail_url, |         embed_thumbnail_url, | ||||||
|  |         embed_title, | ||||||
|  |         embed_fields, | ||||||
|         enabled, |         enabled, | ||||||
|         expires, |         expires, | ||||||
|  |         interval_seconds, | ||||||
|  |         interval_months, | ||||||
|         name, |         name, | ||||||
|         restartable, |         restartable, | ||||||
|         tts, |         tts, | ||||||
|  |         username, | ||||||
|         utc_time |         utc_time | ||||||
|     ]); |     ]); | ||||||
|  |  | ||||||
|     if reminder.interval_days.flatten().is_some() |  | ||||||
|         || reminder.interval_months.flatten().is_some() |  | ||||||
|         || reminder.interval_seconds.flatten().is_some() |  | ||||||
|     { |  | ||||||
|         if check_guild_subscription(&serenity_context.inner(), id).await |  | ||||||
|             || check_subscription(&serenity_context.inner(), user_id).await |  | ||||||
|         { |  | ||||||
|             let new_interval_length = match reminder.interval_days { |  | ||||||
|                 Some(interval) => interval.unwrap_or(0), |  | ||||||
|                 None => sqlx::query!( |  | ||||||
|                     "SELECT interval_days AS days FROM reminders WHERE uid = ?", |  | ||||||
|                     reminder.uid |  | ||||||
|                 ) |  | ||||||
|                 .fetch_one(pool.inner()) |  | ||||||
|                 .await |  | ||||||
|                 .map_err(|e| { |  | ||||||
|                     warn!("Error updating reminder interval: {:?}", e); |  | ||||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) |  | ||||||
|                 })? |  | ||||||
|                 .days |  | ||||||
|                 .unwrap_or(0), |  | ||||||
|             } * 86400 + match reminder.interval_months { |  | ||||||
|                 Some(interval) => interval.unwrap_or(0), |  | ||||||
|                 None => sqlx::query!( |  | ||||||
|                     "SELECT interval_months AS months FROM reminders WHERE uid = ?", |  | ||||||
|                     reminder.uid |  | ||||||
|                 ) |  | ||||||
|                 .fetch_one(pool.inner()) |  | ||||||
|                 .await |  | ||||||
|                 .map_err(|e| { |  | ||||||
|                     warn!("Error updating reminder interval: {:?}", e); |  | ||||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) |  | ||||||
|                 })? |  | ||||||
|                 .months |  | ||||||
|                 .unwrap_or(0), |  | ||||||
|             } * 2592000 + match reminder.interval_seconds { |  | ||||||
|                 Some(interval) => interval.unwrap_or(0), |  | ||||||
|                 None => sqlx::query!( |  | ||||||
|                     "SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?", |  | ||||||
|                     reminder.uid |  | ||||||
|                 ) |  | ||||||
|                 .fetch_one(pool.inner()) |  | ||||||
|                 .await |  | ||||||
|                 .map_err(|e| { |  | ||||||
|                     warn!("Error updating reminder interval: {:?}", e); |  | ||||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) |  | ||||||
|                 })? |  | ||||||
|                 .seconds |  | ||||||
|                 .unwrap_or(0), |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             if new_interval_length < *MIN_INTERVAL { |  | ||||||
|                 error.push(String::from("New interval is too short.")); |  | ||||||
|             } else { |  | ||||||
|                 update_field!(pool.inner(), error, reminder.[ |  | ||||||
|                     interval_days, |  | ||||||
|                     interval_months, |  | ||||||
|                     interval_seconds |  | ||||||
|                 ]); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if reminder.channel > 0 { |     if reminder.channel > 0 { | ||||||
|         let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner()); |         let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner()); | ||||||
|         match channel { |         match channel { | ||||||
| @@ -550,8 +465,7 @@ pub async fn edit_reminder( | |||||||
|  |  | ||||||
|     match sqlx::query_as_unchecked!( |     match sqlx::query_as_unchecked!( | ||||||
|         Reminder, |         Reminder, | ||||||
|         " |         "SELECT reminders.attachment, | ||||||
|         SELECT reminders.attachment, |  | ||||||
|          reminders.attachment_name, |          reminders.attachment_name, | ||||||
|          reminders.avatar, |          reminders.avatar, | ||||||
|          channels.channel, |          channels.channel, | ||||||
| @@ -569,17 +483,13 @@ pub async fn edit_reminder( | |||||||
|          reminders.enabled, |          reminders.enabled, | ||||||
|          reminders.expires, |          reminders.expires, | ||||||
|          reminders.interval_seconds, |          reminders.interval_seconds, | ||||||
|          reminders.interval_days, |  | ||||||
|          reminders.interval_months, |          reminders.interval_months, | ||||||
|          reminders.name, |          reminders.name, | ||||||
|          reminders.restartable, |          reminders.restartable, | ||||||
|          reminders.tts, |          reminders.tts, | ||||||
|          reminders.uid, |          reminders.uid, | ||||||
|          reminders.username, |          reminders.username, | ||||||
|          reminders.utc_time, |          reminders.utc_time | ||||||
|          reminders.status, |  | ||||||
|          reminders.status_change_time, |  | ||||||
|          reminders.status_message |  | ||||||
|         FROM reminders |         FROM reminders | ||||||
|         LEFT JOIN channels ON channels.id = reminders.channel_id |         LEFT JOIN channels ON channels.id = reminders.channel_id | ||||||
|         WHERE uid = ?", |         WHERE uid = ?", | ||||||
| @@ -603,10 +513,7 @@ pub async fn delete_reminder( | |||||||
|     reminder: Json<DeleteReminder>, |     reminder: Json<DeleteReminder>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     match sqlx::query!( |     match sqlx::query!("DELETE FROM reminders WHERE uid = ?", reminder.uid) | ||||||
|         "UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?", |  | ||||||
|         reminder.uid |  | ||||||
|     ) |  | ||||||
|         .execute(pool.inner()) |         .execute(pool.inner()) | ||||||
|         .await |         .await | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -2,15 +2,19 @@ use std::collections::HashMap; | |||||||
|  |  | ||||||
| use chrono::{naive::NaiveDateTime, Utc}; | use chrono::{naive::NaiveDateTime, Utc}; | ||||||
| use rand::{rngs::OsRng, seq::IteratorRandom}; | use rand::{rngs::OsRng, seq::IteratorRandom}; | ||||||
| use rocket::{http::CookieJar, response::Redirect, serde::json::json}; | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     response::Redirect, | ||||||
|  |     serde::json::{json, Value as JsonValue}, | ||||||
|  | }; | ||||||
| use rocket_dyn_templates::Template; | use rocket_dyn_templates::Template; | ||||||
| use serde::{Deserialize, Deserializer, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use serenity::{ | use serenity::{ | ||||||
|     client::Context, |     client::Context, | ||||||
|     http::Http, |     http::Http, | ||||||
|     model::id::{ChannelId, GuildId, UserId}, |     model::id::{ChannelId, GuildId, UserId}, | ||||||
| }; | }; | ||||||
| use sqlx::{types::Json, Executor}; | use sqlx::{types::Json, Executor, MySql, Pool}; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     check_guild_subscription, check_subscription, |     check_guild_subscription, check_subscription, | ||||||
| @@ -18,9 +22,8 @@ use crate::{ | |||||||
|         CHARACTERS, DAY, DEFAULT_AVATAR, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, |         CHARACTERS, DAY, DEFAULT_AVATAR, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, | ||||||
|         MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, |         MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, | ||||||
|         MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, |         MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, | ||||||
|         MAX_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL, |         MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL, | ||||||
|     }, |     }, | ||||||
|     routes::JsonResult, |  | ||||||
|     Database, Error, |     Database, Error, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -28,6 +31,7 @@ pub mod export; | |||||||
| pub mod guild; | pub mod guild; | ||||||
| pub mod user; | pub mod user; | ||||||
|  |  | ||||||
|  | pub type JsonResult = Result<JsonValue, JsonValue>; | ||||||
| type Unset<T> = Option<T>; | type Unset<T> = Option<T>; | ||||||
|  |  | ||||||
| fn name_default() -> String { | fn name_default() -> String { | ||||||
| @@ -46,18 +50,6 @@ fn id_default() -> u32 { | |||||||
|     0 |     0 | ||||||
| } | } | ||||||
|  |  | ||||||
| fn interval_default() -> Unset<Option<u32>> { |  | ||||||
|     None |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error> |  | ||||||
| where |  | ||||||
|     D: Deserializer<'de>, |  | ||||||
|     T: Deserialize<'de>, |  | ||||||
| { |  | ||||||
|     Ok(Some(Option::deserialize(deserializer)?)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Serialize, Deserialize)] | #[derive(Serialize, Deserialize)] | ||||||
| pub struct ReminderTemplate { | pub struct ReminderTemplate { | ||||||
|     #[serde(default = "id_default")] |     #[serde(default = "id_default")] | ||||||
| @@ -118,8 +110,8 @@ pub struct EmbedField { | |||||||
|     inline: bool, |     inline: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Deserialize)] | #[derive(Serialize, Deserialize)] | ||||||
| pub struct ReminderCreate { | pub struct Reminder { | ||||||
|     #[serde(with = "base64s")] |     #[serde(with = "base64s")] | ||||||
|     attachment: Option<Vec<u8>>, |     attachment: Option<Vec<u8>>, | ||||||
|     attachment_name: Option<String>, |     attachment_name: Option<String>, | ||||||
| @@ -140,39 +132,6 @@ pub struct ReminderCreate { | |||||||
|     enabled: bool, |     enabled: bool, | ||||||
|     expires: Option<NaiveDateTime>, |     expires: Option<NaiveDateTime>, | ||||||
|     interval_seconds: Option<u32>, |     interval_seconds: Option<u32>, | ||||||
|     interval_days: Option<u32>, |  | ||||||
|     interval_months: Option<u32>, |  | ||||||
|     #[serde(default = "name_default")] |  | ||||||
|     name: String, |  | ||||||
|     restartable: bool, |  | ||||||
|     tts: bool, |  | ||||||
|     username: Option<String>, |  | ||||||
|     utc_time: NaiveDateTime, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Serialize, Deserialize)] |  | ||||||
| pub struct Reminder { |  | ||||||
|     #[serde(with = "base64s")] |  | ||||||
|     attachment: Option<Vec<u8>>, |  | ||||||
|     attachment_name: Option<String>, |  | ||||||
|     avatar: Option<String>, |  | ||||||
|     #[serde(with = "string_opt")] |  | ||||||
|     channel: Option<u64>, |  | ||||||
|     content: String, |  | ||||||
|     embed_author: String, |  | ||||||
|     embed_author_url: Option<String>, |  | ||||||
|     embed_color: u32, |  | ||||||
|     embed_description: String, |  | ||||||
|     embed_footer: String, |  | ||||||
|     embed_footer_url: Option<String>, |  | ||||||
|     embed_image_url: Option<String>, |  | ||||||
|     embed_thumbnail_url: Option<String>, |  | ||||||
|     embed_title: String, |  | ||||||
|     embed_fields: Option<Json<Vec<EmbedField>>>, |  | ||||||
|     enabled: bool, |  | ||||||
|     expires: Option<NaiveDateTime>, |  | ||||||
|     interval_seconds: Option<u32>, |  | ||||||
|     interval_days: Option<u32>, |  | ||||||
|     interval_months: Option<u32>, |     interval_months: Option<u32>, | ||||||
|     #[serde(default = "name_default")] |     #[serde(default = "name_default")] | ||||||
|     name: String, |     name: String, | ||||||
| @@ -182,9 +141,6 @@ pub struct Reminder { | |||||||
|     uid: String, |     uid: String, | ||||||
|     username: Option<String>, |     username: Option<String>, | ||||||
|     utc_time: NaiveDateTime, |     utc_time: NaiveDateTime, | ||||||
|     status: String, |  | ||||||
|     status_message: Option<String>, |  | ||||||
|     status_change_time: Option<NaiveDateTime>, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Serialize, Deserialize)] | #[derive(Serialize, Deserialize)] | ||||||
| @@ -208,7 +164,6 @@ pub struct ReminderCsv { | |||||||
|     enabled: bool, |     enabled: bool, | ||||||
|     expires: Option<NaiveDateTime>, |     expires: Option<NaiveDateTime>, | ||||||
|     interval_seconds: Option<u32>, |     interval_seconds: Option<u32>, | ||||||
|     interval_days: Option<u32>, |  | ||||||
|     interval_months: Option<u32>, |     interval_months: Option<u32>, | ||||||
|     #[serde(default = "name_default")] |     #[serde(default = "name_default")] | ||||||
|     name: String, |     name: String, | ||||||
| @@ -222,13 +177,10 @@ pub struct ReminderCsv { | |||||||
| pub struct PatchReminder { | pub struct PatchReminder { | ||||||
|     uid: String, |     uid: String, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     attachment: Unset<Option<String>>, |     attachment: Unset<Option<String>>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     attachment_name: Unset<Option<String>>, |     attachment_name: Unset<Option<String>>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     avatar: Unset<Option<String>>, |     avatar: Unset<Option<String>>, | ||||||
|     #[serde(default = "channel_default")] |     #[serde(default = "channel_default")] | ||||||
|     #[serde(with = "string")] |     #[serde(with = "string")] | ||||||
| @@ -238,7 +190,6 @@ pub struct PatchReminder { | |||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     embed_author: Unset<String>, |     embed_author: Unset<String>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     embed_author_url: Unset<Option<String>>, |     embed_author_url: Unset<Option<String>>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     embed_color: Unset<u32>, |     embed_color: Unset<u32>, | ||||||
| @@ -247,13 +198,10 @@ pub struct PatchReminder { | |||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     embed_footer: Unset<String>, |     embed_footer: Unset<String>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     embed_footer_url: Unset<Option<String>>, |     embed_footer_url: Unset<Option<String>>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     embed_image_url: Unset<Option<String>>, |     embed_image_url: Unset<Option<String>>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     embed_thumbnail_url: Unset<Option<String>>, |     embed_thumbnail_url: Unset<Option<String>>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     embed_title: Unset<String>, |     embed_title: Unset<String>, | ||||||
| @@ -262,16 +210,10 @@ pub struct PatchReminder { | |||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     enabled: Unset<bool>, |     enabled: Unset<bool>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     expires: Unset<Option<NaiveDateTime>>, |     expires: Unset<Option<NaiveDateTime>>, | ||||||
|     #[serde(default = "interval_default")] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     interval_seconds: Unset<Option<u32>>, |     interval_seconds: Unset<Option<u32>>, | ||||||
|     #[serde(default = "interval_default")] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     interval_days: Unset<Option<u32>>, |  | ||||||
|     #[serde(default = "interval_default")] |  | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     interval_months: Unset<Option<u32>>, |     interval_months: Unset<Option<u32>>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     name: Unset<String>, |     name: Unset<String>, | ||||||
| @@ -280,36 +222,11 @@ pub struct PatchReminder { | |||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     tts: Unset<bool>, |     tts: Unset<bool>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     #[serde(deserialize_with = "deserialize_optional_field")] |  | ||||||
|     username: Unset<Option<String>>, |     username: Unset<Option<String>>, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     utc_time: Unset<NaiveDateTime>, |     utc_time: Unset<NaiveDateTime>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl PatchReminder { |  | ||||||
|     fn message_ok(&self) -> bool { |  | ||||||
|         self.content.as_ref().map_or(true, |c| c.len() <= MAX_CONTENT_LENGTH) |  | ||||||
|             && self.embed_author.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_AUTHOR_LENGTH) |  | ||||||
|             && self |  | ||||||
|                 .embed_description |  | ||||||
|                 .as_ref() |  | ||||||
|                 .map_or(true, |c| c.len() <= MAX_EMBED_DESCRIPTION_LENGTH) |  | ||||||
|             && self.embed_footer.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_FOOTER_LENGTH) |  | ||||||
|             && self.embed_title.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_TITLE_LENGTH) |  | ||||||
|             && self.embed_fields.as_ref().map_or(true, |c| { |  | ||||||
|                 c.0.len() <= MAX_EMBED_FIELDS |  | ||||||
|                     && c.0.iter().all(|f| { |  | ||||||
|                         f.title.len() <= MAX_EMBED_FIELD_TITLE_LENGTH |  | ||||||
|                             && f.value.len() <= MAX_EMBED_FIELD_VALUE_LENGTH |  | ||||||
|                     }) |  | ||||||
|             }) |  | ||||||
|             && self |  | ||||||
|                 .username |  | ||||||
|                 .as_ref() |  | ||||||
|                 .map_or(true, |c| c.as_ref().map_or(true, |v| v.len() <= MAX_USERNAME_LENGTH)) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub fn generate_uid() -> String { | pub fn generate_uid() -> String { | ||||||
|     let mut generator: OsRng = Default::default(); |     let mut generator: OsRng = Default::default(); | ||||||
|  |  | ||||||
| @@ -323,7 +240,15 @@ pub fn generate_uid() -> String { | |||||||
| mod string { | mod string { | ||||||
|     use std::{fmt::Display, str::FromStr}; |     use std::{fmt::Display, str::FromStr}; | ||||||
|  |  | ||||||
|     use serde::{de, Deserialize, Deserializer}; |     use serde::{de, Deserialize, Deserializer, Serializer}; | ||||||
|  |  | ||||||
|  |     pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error> | ||||||
|  |     where | ||||||
|  |         T: Display, | ||||||
|  |         S: Serializer, | ||||||
|  |     { | ||||||
|  |         serializer.collect_str(value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error> |     pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error> | ||||||
|     where |     where | ||||||
| @@ -335,34 +260,6 @@ mod string { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| mod string_opt { |  | ||||||
|     use std::{fmt::Display, str::FromStr}; |  | ||||||
|  |  | ||||||
|     use serde::{de, Deserialize, Deserializer, Serializer}; |  | ||||||
|  |  | ||||||
|     pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error> |  | ||||||
|     where |  | ||||||
|         T: Display, |  | ||||||
|         S: Serializer, |  | ||||||
|     { |  | ||||||
|         match value { |  | ||||||
|             Some(value) => serializer.collect_str(value), |  | ||||||
|             None => serializer.serialize_none(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error> |  | ||||||
|     where |  | ||||||
|         T: FromStr, |  | ||||||
|         T::Err: Display, |  | ||||||
|         D: Deserializer<'de>, |  | ||||||
|     { |  | ||||||
|         Option::deserialize(deserializer)? |  | ||||||
|             .map(|d: String| d.parse().map_err(de::Error::custom)) |  | ||||||
|             .transpose() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| mod base64s { | mod base64s { | ||||||
|     use serde::{de, Deserialize, Deserializer, Serializer}; |     use serde::{de, Deserialize, Deserializer, Serializer}; | ||||||
|  |  | ||||||
| @@ -404,28 +301,11 @@ pub struct TodoCsv { | |||||||
|  |  | ||||||
| pub async fn create_reminder( | pub async fn create_reminder( | ||||||
|     ctx: &Context, |     ctx: &Context, | ||||||
|     pool: impl sqlx::Executor<'_, Database = Database> + Copy, |     pool: &Pool<MySql>, | ||||||
|     guild_id: GuildId, |     guild_id: GuildId, | ||||||
|     user_id: UserId, |     user_id: UserId, | ||||||
|     reminder: ReminderCreate, |     reminder: Reminder, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     // check guild in db |  | ||||||
|     match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0) |  | ||||||
|         .fetch_one(pool) |  | ||||||
|         .await |  | ||||||
|     { |  | ||||||
|         Err(sqlx::Error::RowNotFound) => { |  | ||||||
|             if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.0) |  | ||||||
|                 .execute(pool) |  | ||||||
|                 .await |  | ||||||
|                 .is_err() |  | ||||||
|             { |  | ||||||
|                 return Err(json!({"error": "Guild could not be created"})); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         _ => {} |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // validate channel |     // validate channel | ||||||
|     let channel = ChannelId(reminder.channel).to_channel_cached(&ctx); |     let channel = ChannelId(reminder.channel).to_channel_cached(&ctx); | ||||||
|     let channel_exists = channel.is_some(); |     let channel_exists = channel.is_some(); | ||||||
| @@ -435,7 +315,7 @@ pub async fn create_reminder( | |||||||
|  |  | ||||||
|     if !channel_matches_guild || !channel_exists { |     if !channel_matches_guild || !channel_exists { | ||||||
|         warn!( |         warn!( | ||||||
|             "Error in `create_reminder`: channel {:?} not found for guild {} (channel exists: {})", |             "Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})", | ||||||
|             reminder.channel, guild_id, channel_exists |             reminder.channel, guild_id, channel_exists | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
| @@ -455,7 +335,6 @@ pub async fn create_reminder( | |||||||
|     let channel = channel.unwrap(); |     let channel = channel.unwrap(); | ||||||
|  |  | ||||||
|     // validate lengths |     // validate lengths | ||||||
|     check_length!(MAX_NAME_LENGTH, reminder.name); |  | ||||||
|     check_length!(MAX_CONTENT_LENGTH, reminder.content); |     check_length!(MAX_CONTENT_LENGTH, reminder.content); | ||||||
|     check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder.embed_description); |     check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder.embed_description); | ||||||
|     check_length!(MAX_EMBED_TITLE_LENGTH, reminder.embed_title); |     check_length!(MAX_EMBED_TITLE_LENGTH, reminder.embed_title); | ||||||
| @@ -491,12 +370,8 @@ pub async fn create_reminder( | |||||||
|     if reminder.utc_time < Utc::now().naive_utc() { |     if reminder.utc_time < Utc::now().naive_utc() { | ||||||
|         return Err(json!({"error": "Time must be in the future"})); |         return Err(json!({"error": "Time must be in the future"})); | ||||||
|     } |     } | ||||||
|     if reminder.interval_seconds.is_some() |     if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() { | ||||||
|         || reminder.interval_days.is_some() |  | ||||||
|         || reminder.interval_months.is_some() |  | ||||||
|     { |  | ||||||
|         if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32 |         if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32 | ||||||
|             + reminder.interval_days.unwrap_or(0) * DAY as u32 |  | ||||||
|             + reminder.interval_seconds.unwrap_or(0) |             + reminder.interval_seconds.unwrap_or(0) | ||||||
|             < *MIN_INTERVAL |             < *MIN_INTERVAL | ||||||
|         { |         { | ||||||
| @@ -505,10 +380,7 @@ pub async fn create_reminder( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // check patreon if necessary |     // check patreon if necessary | ||||||
|     if reminder.interval_seconds.is_some() |     if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() { | ||||||
|         || reminder.interval_days.is_some() |  | ||||||
|         || reminder.interval_months.is_some() |  | ||||||
|     { |  | ||||||
|         if !check_guild_subscription(&ctx, guild_id).await |         if !check_guild_subscription(&ctx, guild_id).await | ||||||
|             && !check_subscription(&ctx, user_id).await |             && !check_subscription(&ctx, user_id).await | ||||||
|         { |         { | ||||||
| @@ -519,23 +391,16 @@ pub async fn create_reminder( | |||||||
|     // base64 decode error dropped here |     // base64 decode error dropped here | ||||||
|     let attachment_data = reminder.attachment.as_ref().map(|s| base64::decode(s).ok()).flatten(); |     let attachment_data = reminder.attachment.as_ref().map(|s| base64::decode(s).ok()).flatten(); | ||||||
|     let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() }; |     let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() }; | ||||||
|     let username = if reminder.username.as_ref().map(|s| s.is_empty()).unwrap_or(true) { |  | ||||||
|         None |  | ||||||
|     } else { |  | ||||||
|         reminder.username |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     let new_uid = generate_uid(); |     let new_uid = generate_uid(); | ||||||
|  |  | ||||||
|     // write to db |     // write to db | ||||||
|     match sqlx::query!( |     match sqlx::query!( | ||||||
|         " |         "INSERT INTO reminders ( | ||||||
|         INSERT INTO reminders ( |  | ||||||
|          uid, |          uid, | ||||||
|          attachment, |          attachment, | ||||||
|          attachment_name, |          attachment_name, | ||||||
|          channel_id, |          channel_id, | ||||||
|          guild_id, |  | ||||||
|          avatar, |          avatar, | ||||||
|          content, |          content, | ||||||
|          embed_author, |          embed_author, | ||||||
| @@ -551,21 +416,17 @@ pub async fn create_reminder( | |||||||
|          enabled, |          enabled, | ||||||
|          expires, |          expires, | ||||||
|          interval_seconds, |          interval_seconds, | ||||||
|          interval_days, |  | ||||||
|          interval_months, |          interval_months, | ||||||
|          name, |          name, | ||||||
|          restartable, |          restartable, | ||||||
|          tts, |          tts, | ||||||
|          username, |          username, | ||||||
|          `utc_time` |          `utc_time` | ||||||
|         ) VALUES (?, ?, ?, ?, |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | ||||||
|         (SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, |  | ||||||
|         ?, ?, ?, ?, ?)", |  | ||||||
|         new_uid, |         new_uid, | ||||||
|         attachment_data, |         attachment_data, | ||||||
|         reminder.attachment_name, |         reminder.attachment_name, | ||||||
|         channel, |         channel, | ||||||
|         guild_id.0, |  | ||||||
|         reminder.avatar, |         reminder.avatar, | ||||||
|         reminder.content, |         reminder.content, | ||||||
|         reminder.embed_author, |         reminder.embed_author, | ||||||
| @@ -581,12 +442,11 @@ pub async fn create_reminder( | |||||||
|         reminder.enabled, |         reminder.enabled, | ||||||
|         reminder.expires, |         reminder.expires, | ||||||
|         reminder.interval_seconds, |         reminder.interval_seconds, | ||||||
|         reminder.interval_days, |  | ||||||
|         reminder.interval_months, |         reminder.interval_months, | ||||||
|         name, |         name, | ||||||
|         reminder.restartable, |         reminder.restartable, | ||||||
|         reminder.tts, |         reminder.tts, | ||||||
|         username, |         reminder.username, | ||||||
|         reminder.utc_time, |         reminder.utc_time, | ||||||
|     ) |     ) | ||||||
|     .execute(pool) |     .execute(pool) | ||||||
| @@ -613,17 +473,13 @@ pub async fn create_reminder( | |||||||
|              reminders.enabled, |              reminders.enabled, | ||||||
|              reminders.expires, |              reminders.expires, | ||||||
|              reminders.interval_seconds, |              reminders.interval_seconds, | ||||||
|              reminders.interval_days, |  | ||||||
|              reminders.interval_months, |              reminders.interval_months, | ||||||
|              reminders.name, |              reminders.name, | ||||||
|              reminders.restartable, |              reminders.restartable, | ||||||
|              reminders.tts, |              reminders.tts, | ||||||
|              reminders.uid, |              reminders.uid, | ||||||
|              reminders.username, |              reminders.username, | ||||||
|              reminders.utc_time, |              reminders.utc_time | ||||||
|              reminders.status, |  | ||||||
|              reminders.status_change_time, |  | ||||||
|              reminders.status_message |  | ||||||
|             FROM reminders |             FROM reminders | ||||||
|             LEFT JOIN channels ON channels.id = reminders.channel_id |             LEFT JOIN channels ON channels.id = reminders.channel_id | ||||||
|             WHERE uid = ?", |             WHERE uid = ?", | ||||||
| @@ -725,17 +581,7 @@ pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirec | |||||||
| } | } | ||||||
|  |  | ||||||
| #[get("/<_>")] | #[get("/<_>")] | ||||||
| pub async fn dashboard_1(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | ||||||
|     if cookies.get_private("userid").is_some() { |  | ||||||
|         let map: HashMap<&str, String> = HashMap::new(); |  | ||||||
|         Ok(Template::render("dashboard", &map)) |  | ||||||
|     } else { |  | ||||||
|         Err(Redirect::to("/login/discord")) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/<_>/<_>")] |  | ||||||
| pub async fn dashboard_2(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { |  | ||||||
|     if cookies.get_private("userid").is_some() { |     if cookies.get_private("userid").is_some() { | ||||||
|         let map: HashMap<&str, String> = HashMap::new(); |         let map: HashMap<&str, String> = HashMap::new(); | ||||||
|         Ok(Template::render("dashboard", &map)) |         Ok(Template::render("dashboard", &map)) | ||||||
|   | |||||||
| @@ -54,8 +54,6 @@ pub async fn get_user_info( | |||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonValue { | ) -> JsonValue { | ||||||
|     offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None })); |  | ||||||
|  |  | ||||||
|     if let Some(user_id) = |     if let Some(user_id) = | ||||||
|         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() |         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() | ||||||
|     { |     { | ||||||
| @@ -63,10 +61,7 @@ pub async fn get_user_info( | |||||||
|             .member(&ctx.inner(), user_id) |             .member(&ctx.inner(), user_id) | ||||||
|             .await; |             .await; | ||||||
|  |  | ||||||
|         let timezone = sqlx::query!( |         let timezone = sqlx::query!("SELECT timezone FROM users WHERE user = ?", user_id) | ||||||
|             "SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?", |  | ||||||
|             user_id |  | ||||||
|         ) |  | ||||||
|             .fetch_one(pool.inner()) |             .fetch_one(pool.inner()) | ||||||
|             .await |             .await | ||||||
|             .map_or(None, |q| Some(q.timezone)); |             .map_or(None, |q| Some(q.timezone)); | ||||||
| @@ -118,8 +113,6 @@ pub async fn update_user_info( | |||||||
|  |  | ||||||
| #[get("/api/user/guilds")] | #[get("/api/user/guilds")] | ||||||
| pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue { | pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue { | ||||||
|     offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }])); |  | ||||||
|  |  | ||||||
|     if let Some(access_token) = cookies.get_private("access_token") { |     if let Some(access_token) = cookies.get_private("access_token") { | ||||||
|         let request_res = reqwest_client |         let request_res = reqwest_client | ||||||
|             .get(format!("{}/users/@me/guilds", DISCORD_API)) |             .get(format!("{}/users/@me/guilds", DISCORD_API)) | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ use rocket::{ | |||||||
| }; | }; | ||||||
| use serenity::model::user::User; | use serenity::model::user::User; | ||||||
|  |  | ||||||
| use crate::{consts::DISCORD_API, routes}; | use crate::consts::DISCORD_API; | ||||||
|  |  | ||||||
| #[get("/discord")] | #[get("/discord")] | ||||||
| pub async fn discord_login( | pub async fn discord_login( | ||||||
| @@ -52,15 +52,6 @@ pub async fn discord_login( | |||||||
|     Redirect::to(auth_url.to_string()) |     Redirect::to(auth_url.to_string()) | ||||||
| } | } | ||||||
|  |  | ||||||
| #[get("/discord/logout")] |  | ||||||
| pub async fn discord_logout(cookies: &CookieJar<'_>) -> Redirect { |  | ||||||
|     cookies.remove_private(Cookie::named("username")); |  | ||||||
|     cookies.remove_private(Cookie::named("userid")); |  | ||||||
|     cookies.remove_private(Cookie::named("access_token")); |  | ||||||
|  |  | ||||||
|     Redirect::to(uri!(routes::index)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/discord/authorized?<code>&<state>")] | #[get("/discord/authorized?<code>&<state>")] | ||||||
| pub async fn discord_callback( | pub async fn discord_callback( | ||||||
|     code: &str, |     code: &str, | ||||||
| @@ -144,14 +135,14 @@ pub async fn discord_callback( | |||||||
|                     Err(Flash::new( |                     Err(Flash::new( | ||||||
|                         Redirect::to(uri!(super::return_to_same_site(""))), |                         Redirect::to(uri!(super::return_to_same_site(""))), | ||||||
|                         "warning", |                         "warning", | ||||||
|                         "Your login request was rejected. The server may be misconfigured. Please retry or alert us in Discord.", |                         "Your login request was rejected", | ||||||
|                     )) |                     )) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "danger", "Your request failed to validate, and so has been rejected (CSRF Validation Failure)")) |             Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "danger", "Your request failed to validate, and so has been rejected (error: CSRF Validation Failure)")) | ||||||
|         } |         } | ||||||
|     } else { |     } else { | ||||||
|         Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "warning", "Your request was missing information, and so has been rejected (CSRF Validation Tokens Missing)")) |         Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "warning", "Your request was missing information, and so has been rejected (error: CSRF Validation Tokens Missing)")) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,11 @@ | |||||||
| pub mod admin; |  | ||||||
| pub mod dashboard; | pub mod dashboard; | ||||||
| pub mod login; | pub mod login; | ||||||
| pub mod report; |  | ||||||
|  |  | ||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
|  |  | ||||||
| use rocket::{request::FlashMessage, serde::json::Value as JsonValue}; | use rocket::request::FlashMessage; | ||||||
| use rocket_dyn_templates::Template; | use rocket_dyn_templates::Template; | ||||||
|  |  | ||||||
| pub type JsonResult = Result<JsonValue, JsonValue>; |  | ||||||
|  |  | ||||||
| #[get("/")] | #[get("/")] | ||||||
| pub async fn index(flash: Option<FlashMessage<'_>>) -> Template { | pub async fn index(flash: Option<FlashMessage<'_>>) -> Template { | ||||||
|     let mut map: HashMap<&str, String> = HashMap::new(); |     let mut map: HashMap<&str, String> = HashMap::new(); | ||||||
|   | |||||||
| @@ -1,48 +0,0 @@ | |||||||
| use rocket::{ |  | ||||||
|     http::CookieJar, |  | ||||||
|     serde::{ |  | ||||||
|         json::{json, Json}, |  | ||||||
|         Deserialize, |  | ||||||
|     }, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| use crate::routes::JsonResult; |  | ||||||
|  |  | ||||||
| #[derive(Deserialize)] |  | ||||||
| pub struct ClientError { |  | ||||||
|     #[serde(rename = "reporterId")] |  | ||||||
|     reporter_id: String, |  | ||||||
|     url: String, |  | ||||||
|     #[serde(rename = "relativeTimestamp")] |  | ||||||
|     relative_timestamp: i64, |  | ||||||
|     #[serde(rename = "errorMessage")] |  | ||||||
|     error_message: String, |  | ||||||
|     #[serde(rename = "errorLine")] |  | ||||||
|     error_line: u64, |  | ||||||
|     #[serde(rename = "errorFile")] |  | ||||||
|     error_file: String, |  | ||||||
|     #[serde(rename = "errorType")] |  | ||||||
|     error_type: String, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[post("/report", data = "<client_error>")] |  | ||||||
| pub async fn report_error(cookies: &CookieJar<'_>, client_error: Json<ClientError>) -> JsonResult { |  | ||||||
|     if let Some(user_id) = cookies.get_private("userid") { |  | ||||||
|         error!( |  | ||||||
|             "User {} reports a client-side error. |  | ||||||
| {}, {}:{} at {}ms |  | ||||||
| {}: {} |  | ||||||
| Chain: {}", |  | ||||||
|             user_id, |  | ||||||
|             client_error.url, |  | ||||||
|             client_error.error_file, |  | ||||||
|             client_error.error_line, |  | ||||||
|             client_error.relative_timestamp, |  | ||||||
|             client_error.error_type, |  | ||||||
|             client_error.error_message, |  | ||||||
|             client_error.reporter_id |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Ok(json!({})) |  | ||||||
| } |  | ||||||
| @@ -11,22 +11,10 @@ div.reminderContent.is-collapsed .column.discord-frame { | |||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| div.reminderContent.is-collapsed .column.settings { | div.reminderContent.is-collapsed .collapses { | ||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| 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; | ||||||
| } | } | ||||||
| @@ -35,42 +23,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 .hide-box { | div.reminderContent.is-collapsed button.hide-box { | ||||||
|     display: inline-flex; |     display: inline-flex; | ||||||
| } | } | ||||||
|  |  | ||||||
| div.reminderContent.is-collapsed .hide-box i { | div.reminderContent.is-collapsed button.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; | ||||||
| @@ -97,86 +85,18 @@ div.discord-embed { | |||||||
|     position: relative; |     position: relative; | ||||||
| } | } | ||||||
|  |  | ||||||
| div.split-controls { |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: column; |  | ||||||
|     justify-content: space-between; |  | ||||||
|     flex-grow: 2; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .reminder-topbar > div { |  | ||||||
|     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 { | div.reminderContent { | ||||||
|     margin-top: 10px; |     padding: 2px; | ||||||
|     margin-bottom: 10px; |  | ||||||
|     padding: 14px; |  | ||||||
|     background-color: #f5f5f5; |     background-color: #f5f5f5; | ||||||
|     border-radius: 8px; |     border-radius: 8px; | ||||||
|  |     margin: 8px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.interval-group > button { | ||||||
|  |     margin-left: auto; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* 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; | ||||||
| @@ -190,13 +110,12 @@ 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: 3ch; |     width: 6ch; | ||||||
| } | } | ||||||
|  |  | ||||||
| div.interval-group { | div.interval-group { | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: row; |     flex-direction: row; | ||||||
|     justify-content: space-between; |  | ||||||
| } | } | ||||||
| /* !Interval inputs */ | /* !Interval inputs */ | ||||||
|  |  | ||||||
| @@ -261,23 +180,6 @@ 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; | ||||||
| } | } | ||||||
| @@ -309,19 +211,10 @@ div.dashboard-sidebar:not(.mobile-sidebar) { | |||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
| } | } | ||||||
|  |  | ||||||
| ul.guildList { |  | ||||||
|     flex-grow: 1; |  | ||||||
|     flex-shrink: 1; |  | ||||||
|     overflow: auto; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | ||||||
|     flex-shrink: 0; |     position: fixed; | ||||||
|     flex-grow: 0; |     bottom: 0; | ||||||
| } |     width: 226px; | ||||||
|  |  | ||||||
| div.dashboard-sidebar svg { |  | ||||||
|     flex-shrink: 0; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| div.mobile-sidebar { | div.mobile-sidebar { | ||||||
| @@ -400,7 +293,10 @@ input.default-width { | |||||||
| } | } | ||||||
|  |  | ||||||
| .message-input:placeholder-shown { | .message-input:placeholder-shown { | ||||||
|     font-style: italic; |     border-top: none; | ||||||
|  |     border-left: none; | ||||||
|  |     border-right: none; | ||||||
|  |     border-bottom-style: dashed; | ||||||
|     background-color: #40444b; |     background-color: #40444b; | ||||||
|     color: #fff; |     color: #fff; | ||||||
| } | } | ||||||
| @@ -471,7 +367,8 @@ input.default-width { | |||||||
| .customizable.is-400x300 img { | .customizable.is-400x300 img { | ||||||
|     margin-top: 10px; |     margin-top: 10px; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|     height: 100px; |     min-height: 100px; | ||||||
|  |     max-height: 400px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .customizable.is-32x32 img { | .customizable.is-32x32 img { | ||||||
| @@ -565,7 +462,6 @@ 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 { | ||||||
| @@ -615,84 +511,21 @@ input.default-width { | |||||||
|     border-bottom: 1px solid #fff; |     border-bottom: 1px solid #fff; | ||||||
| } | } | ||||||
|  |  | ||||||
| .channel-selector { | @media only screen and (max-width: 768px) { | ||||||
|     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; | ||||||
|     } |     } | ||||||
| } |  | ||||||
|  |  | ||||||
| @media only screen and (max-width: 768px) { |     .customizable.is-24x24 img { | ||||||
|     .button-row { |         width: 16px; | ||||||
|         display: flex; |         height: 16px; | ||||||
|         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; | ||||||
| @@ -704,86 +537,6 @@ li.highlight { | |||||||
|  |  | ||||||
| /* 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 { | ||||||
| @@ -815,44 +568,11 @@ div.reminderError .reminderMessage { | |||||||
|     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; | ||||||
| } | } | ||||||
| @@ -860,27 +580,3 @@ a.switch-pane.is-active ~ .guild-submenu { | |||||||
| .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; |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,15 +1,14 @@ | |||||||
| { | { | ||||||
|     "name": "Reminder Bot Dashboard", |     "name": "", | ||||||
|     "short_name": "Reminders", |     "short_name": "", | ||||||
|     "start_url": "/dashboard", |  | ||||||
|     "icons": [ |     "icons": [ | ||||||
|         { |         { | ||||||
|             "src": "/static/favicon/android-chrome-192x192.png", |             "src": "/android-chrome-192x192.png", | ||||||
|             "sizes": "192x192", |             "sizes": "192x192", | ||||||
|             "type": "image/png" |             "type": "image/png" | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "src": "/static/favicon/android-chrome-512x512.png", |             "src": "/android-chrome-512x512.png", | ||||||
|             "sizes": "512x512", |             "sizes": "512x512", | ||||||
|             "type": "image/png" |             "type": "image/png" | ||||||
|         } |         } | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 81 KiB | 
| @@ -1,131 +0,0 @@ | |||||||
| document.addEventListener("DOMContentLoaded", () => { |  | ||||||
|     fetch("/admin/data") |  | ||||||
|         .then((resp) => resp.json()) |  | ||||||
|         .then((data) => { |  | ||||||
|             document.querySelector("#backlog").textContent = data.backlog; |  | ||||||
|             document.querySelector("#reminders").textContent = data.count.reminders; |  | ||||||
|             document.querySelector("#intervals").textContent = data.count.intervals; |  | ||||||
|  |  | ||||||
|             let historySent = data.historyLong.sent.reduce( |  | ||||||
|                 (iv, frame) => iv + frame.count, |  | ||||||
|                 0 |  | ||||||
|             ); |  | ||||||
|             let historyFailed = data.historyLong.failed.reduce( |  | ||||||
|                 (iv, frame) => iv + frame.count, |  | ||||||
|                 0 |  | ||||||
|             ); |  | ||||||
|             let rate = historyFailed / (historySent + historyFailed); |  | ||||||
|             let formatted = Math.round(rate * 10000) / 100; |  | ||||||
|  |  | ||||||
|             document.querySelector("#historySent").textContent = historySent; |  | ||||||
|             document.querySelector("#historyFailed").textContent = historyFailed; |  | ||||||
|             document.querySelector("#failRate").textContent = `${formatted}%`; |  | ||||||
|  |  | ||||||
|             new Chart(document.getElementById("schedule"), { |  | ||||||
|                 type: "bar", |  | ||||||
|                 data: { |  | ||||||
|                     labels: [ |  | ||||||
|                         ...data.scheduleShort.once, |  | ||||||
|                         ...data.scheduleShort.interval, |  | ||||||
|                     ].map((row) => luxon.DateTime.fromISO(row.time_key)), |  | ||||||
|                     datasets: [ |  | ||||||
|                         { |  | ||||||
|                             label: "Reminders", |  | ||||||
|                             data: data.scheduleShort.once.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             label: "Intervals", |  | ||||||
|                             data: data.scheduleShort.interval.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                     ], |  | ||||||
|                 }, |  | ||||||
|                 options: { |  | ||||||
|                     responsive: true, |  | ||||||
|                     maintainAspectRatio: false, |  | ||||||
|                     scales: { |  | ||||||
|                         x: { |  | ||||||
|                             stacked: true, |  | ||||||
|                             type: "time", |  | ||||||
|                             time: { |  | ||||||
|                                 unit: "minute", |  | ||||||
|                             }, |  | ||||||
|                         }, |  | ||||||
|                         y: { |  | ||||||
|                             stacked: true, |  | ||||||
|                         }, |  | ||||||
|                     }, |  | ||||||
|                 }, |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             new Chart(document.getElementById("scheduleLong"), { |  | ||||||
|                 type: "bar", |  | ||||||
|                 data: { |  | ||||||
|                     labels: [ |  | ||||||
|                         ...data.scheduleLong.once, |  | ||||||
|                         ...data.scheduleLong.interval, |  | ||||||
|                     ].map((row) => luxon.DateTime.fromISO(row.time_key)), |  | ||||||
|                     datasets: [ |  | ||||||
|                         { |  | ||||||
|                             label: "Reminders", |  | ||||||
|                             data: data.scheduleLong.once.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             label: "Intervals", |  | ||||||
|                             data: data.scheduleLong.interval.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                     ], |  | ||||||
|                 }, |  | ||||||
|                 options: { |  | ||||||
|                     responsive: true, |  | ||||||
|                     maintainAspectRatio: false, |  | ||||||
|                     scales: { |  | ||||||
|                         x: { |  | ||||||
|                             stacked: true, |  | ||||||
|                             type: "time", |  | ||||||
|                             time: { |  | ||||||
|                                 unit: "day", |  | ||||||
|                             }, |  | ||||||
|                         }, |  | ||||||
|                         y: { |  | ||||||
|                             stacked: true, |  | ||||||
|                         }, |  | ||||||
|                     }, |  | ||||||
|                 }, |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             new Chart(document.getElementById("historyLong"), { |  | ||||||
|                 type: "bar", |  | ||||||
|                 data: { |  | ||||||
|                     labels: [...data.historyLong.sent, ...data.historyLong.failed].map( |  | ||||||
|                         (row) => luxon.DateTime.fromISO(row.time_key) |  | ||||||
|                     ), |  | ||||||
|                     datasets: [ |  | ||||||
|                         { |  | ||||||
|                             label: "Success", |  | ||||||
|                             data: data.historyLong.sent.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             label: "Fail", |  | ||||||
|                             data: data.historyLong.failed.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                     ], |  | ||||||
|                 }, |  | ||||||
|                 options: { |  | ||||||
|                     responsive: true, |  | ||||||
|                     maintainAspectRatio: false, |  | ||||||
|                     scales: { |  | ||||||
|                         x: { |  | ||||||
|                             stacked: true, |  | ||||||
|                             type: "time", |  | ||||||
|                             time: { |  | ||||||
|                                 unit: "day", |  | ||||||
|                             }, |  | ||||||
|                         }, |  | ||||||
|                         y: { |  | ||||||
|                             stacked: true, |  | ||||||
|                         }, |  | ||||||
|                     }, |  | ||||||
|                 }, |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,7 +0,0 @@ | |||||||
| /*! |  | ||||||
|  * chartjs-adapter-luxon v1.3.1 |  | ||||||
|  * https://www.chartjs.org |  | ||||||
|  * (c) 2023 chartjs-adapter-luxon Contributors |  | ||||||
|  * Released under the MIT license |  | ||||||
|  */ |  | ||||||
| !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})})); |  | ||||||
| @@ -7,8 +7,8 @@ function get_interval(element) { | |||||||
|  |  | ||||||
|     return { |     return { | ||||||
|         months: parseInt(months) || null, |         months: parseInt(months) || null, | ||||||
|         days: parseInt(days) || null, |  | ||||||
|         seconds: |         seconds: | ||||||
|  |             (parseInt(days) || 0) * 86400 + | ||||||
|                 (parseInt(hours) || 0) * 3600 + |                 (parseInt(hours) || 0) * 3600 + | ||||||
|                 (parseInt(minutes) || 0) * 60 + |                 (parseInt(minutes) || 0) * 60 + | ||||||
|                 (parseInt(seconds) || 0) || null, |                 (parseInt(seconds) || 0) || null, | ||||||
| @@ -22,15 +22,6 @@ function update_interval(element) { | |||||||
|     let minutes = element.querySelector('input[name="interval_minutes"]'); |     let minutes = element.querySelector('input[name="interval_minutes"]'); | ||||||
|     let seconds = element.querySelector('input[name="interval_seconds"]'); |     let seconds = element.querySelector('input[name="interval_seconds"]'); | ||||||
|  |  | ||||||
|     let interval = get_interval(element); |  | ||||||
|  |  | ||||||
|     if (interval.months === null && interval.days === null && interval.seconds === null) { |  | ||||||
|         months.value = ""; |  | ||||||
|         days.value = ""; |  | ||||||
|         hours.value = ""; |  | ||||||
|         minutes.value = ""; |  | ||||||
|         seconds.value = ""; |  | ||||||
|     } else { |  | ||||||
|     months.value = months.value.padStart(1, "0"); |     months.value = months.value.padStart(1, "0"); | ||||||
|     days.value = days.value.padStart(1, "0"); |     days.value = days.value.padStart(1, "0"); | ||||||
|     hours.value = hours.value.padStart(2, "0"); |     hours.value = hours.value.padStart(2, "0"); | ||||||
| @@ -42,10 +33,7 @@ function update_interval(element) { | |||||||
|         let remainder = seconds.value % 60; |         let remainder = seconds.value % 60; | ||||||
|  |  | ||||||
|         seconds.value = String(remainder).padStart(2, "0"); |         seconds.value = String(remainder).padStart(2, "0"); | ||||||
|             minutes.value = String(Number(minutes.value) + Number(quotient)).padStart( |         minutes.value = String(Number(minutes.value) + Number(quotient)).padStart(2, "0"); | ||||||
|                 2, |  | ||||||
|                 "0" |  | ||||||
|             ); |  | ||||||
|     } |     } | ||||||
|     if (minutes.value >= 60) { |     if (minutes.value >= 60) { | ||||||
|         let quotient = Math.floor(minutes.value / 60); |         let quotient = Math.floor(minutes.value / 60); | ||||||
| @@ -54,6 +42,12 @@ function update_interval(element) { | |||||||
|         minutes.value = String(remainder).padStart(2, "0"); |         minutes.value = String(remainder).padStart(2, "0"); | ||||||
|         hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0"); |         hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0"); | ||||||
|     } |     } | ||||||
|  |     if (hours.value >= 24) { | ||||||
|  |         let quotient = Math.floor(hours.value / 24); | ||||||
|  |         let remainder = hours.value % 24; | ||||||
|  |  | ||||||
|  |         hours.value = String(remainder).padStart(2, "0"); | ||||||
|  |         days.value = Number(days.value) + Number(quotient); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ const $downloader = document.querySelector("a#downloader"); | |||||||
| const $uploader = document.querySelector("input#uploader"); | const $uploader = document.querySelector("input#uploader"); | ||||||
|  |  | ||||||
| let channels = []; | let channels = []; | ||||||
| let reminderErrors = []; |  | ||||||
| let guildNames = {}; | let guildNames = {}; | ||||||
| let roles = []; | let roles = []; | ||||||
| let templates = {}; | let templates = {}; | ||||||
| @@ -34,11 +33,7 @@ let globalPatreon = false; | |||||||
| let guildPatreon = false; | let guildPatreon = false; | ||||||
|  |  | ||||||
| function guildId() { | function guildId() { | ||||||
|     return document.querySelector("li > a.is-active").parentElement.dataset["guild"]; |     return document.querySelector(".guildList a.is-active").dataset["guild"]; | ||||||
| } |  | ||||||
|  |  | ||||||
| function guildName() { |  | ||||||
|     return guildNames[guildId()]; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function colorToInt(r, g, b) { | function colorToInt(r, g, b) { | ||||||
| @@ -57,40 +52,22 @@ function switch_pane(selector) { | |||||||
|         el.classList.add("is-hidden"); |         el.classList.add("is-hidden"); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     document.querySelector(`*[data-name=${selector}]`).classList.remove("is-hidden"); |     document.getElementById(selector).classList.remove("is-hidden"); | ||||||
| } | } | ||||||
|  |  | ||||||
| function update_select(sel) { | function update_select(sel) { | ||||||
|     let channelDisplay = sel.closest("div.reminderContent").querySelector(".channel-bar"); |  | ||||||
|  |  | ||||||
|     if (channelDisplay !== null) { |  | ||||||
|         channelDisplay.textContent = `#${sel.selectedOptions[0].textContent}`; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (sel.selectedOptions[0] === undefined) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const avatarInput = sel.closest("div.reminderContent").querySelector("img.avatar"); |  | ||||||
|  |  | ||||||
|     if (!avatarInput.dataset["set"]) { |  | ||||||
|     if (sel.selectedOptions[0].dataset["webhookAvatar"]) { |     if (sel.selectedOptions[0].dataset["webhookAvatar"]) { | ||||||
|             avatarInput.src = sel.selectedOptions[0].dataset["webhookAvatar"]; |         sel.closest("div.reminderContent").querySelector("img.discord-avatar").src = | ||||||
|  |             sel.selectedOptions[0].dataset["webhookAvatar"]; | ||||||
|     } else { |     } else { | ||||||
|             avatarInput.src = "/static/img/icon.png"; |         sel.closest("div.reminderContent").querySelector("img.discord-avatar").src = ""; | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const usernameInput = sel |  | ||||||
|         .closest("div.reminderContent") |  | ||||||
|         .querySelector("input.discord-username"); |  | ||||||
|  |  | ||||||
|     if (usernameInput.value.length === 0) { |  | ||||||
|     if (sel.selectedOptions[0].dataset["webhookName"]) { |     if (sel.selectedOptions[0].dataset["webhookName"]) { | ||||||
|             usernameInput.value = sel.selectedOptions[0].dataset["webhookName"]; |         sel.closest("div.reminderContent").querySelector("input.discord-username").value = | ||||||
|  |             sel.selectedOptions[0].dataset["webhookName"]; | ||||||
|     } else { |     } else { | ||||||
|             usernameInput.value = "Reminder"; |         sel.closest("div.reminderContent").querySelector("input.discord-username").value = | ||||||
|         } |             ""; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -161,18 +138,12 @@ async function fetch_channels(guild_id) { | |||||||
|     const event = new Event("channelsLoading"); |     const event = new Event("channelsLoading"); | ||||||
|     document.dispatchEvent(event); |     document.dispatchEvent(event); | ||||||
|  |  | ||||||
|     let hasError = false; |  | ||||||
|  |  | ||||||
|     await fetch(`/dashboard/api/guild/${guild_id}/channels`) |     await fetch(`/dashboard/api/guild/${guild_id}/channels`) | ||||||
|         .then((response) => response.json()) |         .then((response) => response.json()) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|             if (data.error) { |             if (data.error) { | ||||||
|                 if (data.error === "Bot not in guild") { |                 if (data.error === "Bot not in guild") { | ||||||
|                     switch_pane("guild-error"); |                     switch_pane("guild-error"); | ||||||
|                     hasError = true; |  | ||||||
|                 } else if (data.error === "Incorrect permissions") { |  | ||||||
|                     switch_pane("user-error"); |  | ||||||
|                     hasError = true; |  | ||||||
|                 } else { |                 } else { | ||||||
|                     show_error(data.error); |                     show_error(data.error); | ||||||
|                 } |                 } | ||||||
| @@ -184,8 +155,6 @@ async function fetch_channels(guild_id) { | |||||||
|             const event = new Event("channelsLoaded"); |             const event = new Event("channelsLoaded"); | ||||||
|             document.dispatchEvent(event); |             document.dispatchEvent(event); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|     return hasError; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| async function fetch_reminders(guild_id) { | async function fetch_reminders(guild_id) { | ||||||
| @@ -236,18 +205,14 @@ async function serialize_reminder(node, mode) { | |||||||
|         utc_time = luxon.DateTime.fromISO( |         utc_time = luxon.DateTime.fromISO( | ||||||
|             node.querySelector('input[name="time"]').value |             node.querySelector('input[name="time"]').value | ||||||
|         ).setZone("UTC"); |         ).setZone("UTC"); | ||||||
|  |  | ||||||
|         if (utc_time.invalid) { |         if (utc_time.invalid) { | ||||||
|             return { error: "Time provided invalid." }; |             return { error: "Time provided invalid." }; | ||||||
|         } else { |         } else { | ||||||
|             utc_time = utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"); |             utc_time = utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let expiration = node.querySelector('input[name="expiration"]').value; |  | ||||||
|  |  | ||||||
|         if (expiration) { |  | ||||||
|         expiration_time = luxon.DateTime.fromISO( |         expiration_time = luxon.DateTime.fromISO( | ||||||
|                 node.querySelector('input[name="expiration"]').value |             node.querySelector('input[name="time"]').value | ||||||
|         ).setZone("UTC"); |         ).setZone("UTC"); | ||||||
|         if (expiration_time.invalid) { |         if (expiration_time.invalid) { | ||||||
|             return { error: "Expiration provided invalid." }; |             return { error: "Expiration provided invalid." }; | ||||||
| @@ -255,12 +220,6 @@ async function serialize_reminder(node, mode) { | |||||||
|             expiration_time = expiration_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"); |             expiration_time = expiration_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let name = node.querySelector('input[name="name"]').value; |  | ||||||
|     if (name.length > 100) { |  | ||||||
|         return { error: "Name exceeds maximum length (100)." }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let rgb_color = window.getComputedStyle( |     let rgb_color = window.getComputedStyle( | ||||||
|         node.querySelector("div.discord-embed") |         node.querySelector("div.discord-embed") | ||||||
| @@ -324,17 +283,15 @@ async function serialize_reminder(node, mode) { | |||||||
|     const embed_title = node.querySelector('textarea[name="embed_title"]').value; |     const embed_title = node.querySelector('textarea[name="embed_title"]').value; | ||||||
|  |  | ||||||
|     if ( |     if ( | ||||||
|         content.length === 0 && |         attachment === null && | ||||||
|         embed_author.length === 0 && |         content.length == 0 && | ||||||
|         embed_title.length === 0 && |  | ||||||
|         embed_description.length === 0 && |  | ||||||
|         embed_footer.length === 0 && |  | ||||||
|         embed_author_url === null && |         embed_author_url === null && | ||||||
|  |         embed_author.length == 0 && | ||||||
|  |         embed_description.length == 0 && | ||||||
|  |         embed_footer.length == 0 && | ||||||
|         embed_footer_url === null && |         embed_footer_url === null && | ||||||
|         embed_image_url === null && |         embed_image_url === null && | ||||||
|         embed_thumbnail_url === null && |         embed_thumbnail_url === null | ||||||
|         fields.length === 0 && |  | ||||||
|         attachment === null |  | ||||||
|     ) { |     ) { | ||||||
|         return { error: "Reminder needs content." }; |         return { error: "Reminder needs content." }; | ||||||
|     } |     } | ||||||
| @@ -347,7 +304,7 @@ async function serialize_reminder(node, mode) { | |||||||
|         restartable: false, |         restartable: false, | ||||||
|         attachment: attachment, |         attachment: attachment, | ||||||
|         attachment_name: attachment_name, |         attachment_name: attachment_name, | ||||||
|         avatar: has_source(node.querySelector("img.avatar").src), |         avatar: has_source(node.querySelector("img.discord-avatar").src), | ||||||
|         channel: node.querySelector("select.channel-selector").value, |         channel: node.querySelector("select.channel-selector").value, | ||||||
|         content: content, |         content: content, | ||||||
|         embed_author_url: embed_author_url, |         embed_author_url: embed_author_url, | ||||||
| @@ -362,7 +319,6 @@ async function serialize_reminder(node, mode) { | |||||||
|         embed_fields: fields, |         embed_fields: fields, | ||||||
|         expires: expiration_time, |         expires: expiration_time, | ||||||
|         interval_seconds: mode !== "template" ? interval.seconds : null, |         interval_seconds: mode !== "template" ? interval.seconds : null, | ||||||
|         interval_days: mode !== "template" ? interval.days : null, |  | ||||||
|         interval_months: mode !== "template" ? interval.months : null, |         interval_months: mode !== "template" ? interval.months : null, | ||||||
|         name: node.querySelector('input[name="name"]').value, |         name: node.querySelector('input[name="name"]').value, | ||||||
|         tts: node.querySelector('input[name="tts"]').checked, |         tts: node.querySelector('input[name="tts"]').checked, | ||||||
| @@ -375,9 +331,6 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
|     // populate channels |     // populate channels | ||||||
|     set_channels(frame.querySelector("select.channel-selector")); |     set_channels(frame.querySelector("select.channel-selector")); | ||||||
|  |  | ||||||
|     frame.querySelector(`*[name="interval_hours"]`).value = 0; |  | ||||||
|     frame.querySelector(`*[name="interval_minutes"]`).value = 0; |  | ||||||
|  |  | ||||||
|     // populate majority of items |     // populate majority of items | ||||||
|     for (let prop in reminder) { |     for (let prop in reminder) { | ||||||
|         if (reminder.hasOwnProperty(prop) && reminder[prop] !== null) { |         if (reminder.hasOwnProperty(prop) && reminder[prop] !== null) { | ||||||
| @@ -392,27 +345,15 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
|                 if ($input !== null) { |                 if ($input !== null) { | ||||||
|                     $input.value = reminder[prop]; |                     $input.value = reminder[prop]; | ||||||
|                 } else if ($image !== null) { |                 } else if ($image !== null) { | ||||||
|                     console.log(`loading img ${prop}`); |  | ||||||
|                     $image.src = reminder[prop]; |                     $image.src = reminder[prop]; | ||||||
|                     $image.dataset["set"] = "1"; |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     update_interval(frame); |     const lastChild = frame.querySelector("div.embed-multifield-box .embed-field-box"); | ||||||
|     update_select(frame.querySelector(".channel-selector")); |  | ||||||
|  |  | ||||||
|     const lastChild = frame.querySelector( |     for (let field of reminder["embed_fields"]) { | ||||||
|         "div.embed-multifield-box .embed-field-box:last-child" |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     // Drop existing fields |  | ||||||
|     frame |  | ||||||
|         .querySelectorAll(".embed-field-box:not(:last-child)") |  | ||||||
|         .forEach((el) => el.remove()); |  | ||||||
|  |  | ||||||
|     for (let field of reminder["embed_fields"] || []) { |  | ||||||
|         let embed_field = $embedFieldTemplate.content.cloneNode(true); |         let embed_field = $embedFieldTemplate.content.cloneNode(true); | ||||||
|         embed_field.querySelector("textarea.discord-field-title").value = field["title"]; |         embed_field.querySelector("textarea.discord-field-title").value = field["title"]; | ||||||
|         embed_field.querySelector("textarea.discord-field-value").value = field["value"]; |         embed_field.querySelector("textarea.discord-field-value").value = field["value"]; | ||||||
| @@ -438,7 +379,7 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
|         timeInput.value = localTime.toFormat("yyyy-LL-dd'T'HH:mm:ss"); |         timeInput.value = localTime.toFormat("yyyy-LL-dd'T'HH:mm:ss"); | ||||||
|  |  | ||||||
|         if (reminder["expires"]) { |         if (reminder["expires"]) { | ||||||
|             let expiresInput = frame.querySelector('input[name="expiration"]'); |             let expiresInput = frame.querySelector('input[name="time"]'); | ||||||
|             let expiresTime = luxon.DateTime.fromISO(reminder["expires"], { |             let expiresTime = luxon.DateTime.fromISO(reminder["expires"], { | ||||||
|                 zone: "UTC", |                 zone: "UTC", | ||||||
|             }).setZone(timezone); |             }).setZone(timezone); | ||||||
| @@ -454,27 +395,13 @@ document.addEventListener("guildSwitched", async (e) => { | |||||||
|         .querySelectorAll(".patreon-only") |         .querySelectorAll(".patreon-only") | ||||||
|         .forEach((el) => el.classList.add("is-locked")); |         .forEach((el) => el.classList.add("is-locked")); | ||||||
|  |  | ||||||
|     let $li = document.querySelectorAll(`li[data-guild="${e.detail.guild_id}"]`); |     let $anchor = document.querySelector( | ||||||
|  |         `.switch-pane[data-guild="${e.detail.guild_id}"]` | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     if ($li.length === 0) { |     switch_pane($anchor.dataset["pane"]); | ||||||
|         switch_pane("user-error"); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     switch_pane(e.detail.pane); |  | ||||||
|     reset_guild_pane(); |     reset_guild_pane(); | ||||||
|     document |     $anchor.classList.add("is-active"); | ||||||
|         .querySelectorAll(`li[data-guild="${e.detail.guild_id}"] > a`) |  | ||||||
|         .forEach((el) => { |  | ||||||
|             el.classList.add("is-active"); |  | ||||||
|         }); |  | ||||||
|     document |  | ||||||
|         .querySelectorAll( |  | ||||||
|             `li[data-guild="${e.detail.guild_id}"] *[data-pane="${e.detail.pane}"]` |  | ||||||
|         ) |  | ||||||
|         .forEach((el) => { |  | ||||||
|             el.classList.add("is-active"); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { |     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { | ||||||
|         document |         document | ||||||
| @@ -482,32 +409,19 @@ document.addEventListener("guildSwitched", async (e) => { | |||||||
|             .forEach((el) => el.classList.remove("is-locked")); |             .forEach((el) => el.classList.remove("is-locked")); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const event = new CustomEvent("paneLoad", { |     fetch_roles(e.detail.guild_id); | ||||||
|         detail: { |     fetch_templates(e.detail.guild_id); | ||||||
|             guild_id: e.detail.guild_id, |     await fetch_channels(e.detail.guild_id); | ||||||
|             pane: e.detail.pane, |     fetch_reminders(e.detail.guild_id); | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
|     document.dispatchEvent(event); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| document.addEventListener("paneLoad", async (ev) => { |  | ||||||
|     const hasError = await fetch_channels(ev.detail.guild_id); |  | ||||||
|     if (!hasError) { |  | ||||||
|         fetch_roles(ev.detail.guild_id); |  | ||||||
|         fetch_templates(ev.detail.guild_id); |  | ||||||
|         fetch_reminders(ev.detail.guild_id); |  | ||||||
|  |  | ||||||
|     document.querySelectorAll("p.pageTitle").forEach((el) => { |     document.querySelectorAll("p.pageTitle").forEach((el) => { | ||||||
|             el.textContent = `${guildName()} Reminders`; |         el.textContent = `${e.detail.guild_name} Reminders`; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     document.querySelectorAll("select.channel-selector").forEach((el) => { |     document.querySelectorAll("select.channel-selector").forEach((el) => { | ||||||
|         el.addEventListener("change", (e) => { |         el.addEventListener("change", (e) => { | ||||||
|             update_select(e.target); |             update_select(e.target); | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $loader.classList.add("is-hidden"); |     $loader.classList.add("is-hidden"); | ||||||
| }); | }); | ||||||
| @@ -519,12 +433,6 @@ document.addEventListener("channelsLoaded", () => { | |||||||
| document.addEventListener("remindersLoaded", (event) => { | document.addEventListener("remindersLoaded", (event) => { | ||||||
|     const guild = guildId(); |     const guild = guildId(); | ||||||
|  |  | ||||||
|     document.querySelectorAll("select.channel-selector").forEach((el) => { |  | ||||||
|         el.addEventListener("change", (e) => { |  | ||||||
|             update_select(e.target); |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     for (let reminder of event.detail) { |     for (let reminder of event.detail) { | ||||||
|         let node = reminder.node; |         let node = reminder.node; | ||||||
|  |  | ||||||
| @@ -552,9 +460,9 @@ document.addEventListener("remindersLoaded", (event) => { | |||||||
|                     if (data.error) { |                     if (data.error) { | ||||||
|                         show_error(data.error); |                         show_error(data.error); | ||||||
|                     } else { |                     } else { | ||||||
|                         enableBtn.dataset["action"] = data.reminder["enabled"] |                         enableBtn.dataset["action"] = data["enabled"] | ||||||
|                             ? "disable" |                             ? "enable" | ||||||
|                             : "enable"; |                             : "disable"; | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|         }); |         }); | ||||||
| @@ -589,8 +497,6 @@ document.addEventListener("remindersLoaded", (event) => { | |||||||
|                 .then((response) => response.json()) |                 .then((response) => response.json()) | ||||||
|                 .then((data) => { |                 .then((data) => { | ||||||
|                     for (let error of data.errors) show_error(error); |                     for (let error of data.errors) show_error(error); | ||||||
|  |  | ||||||
|                     deserialize_reminder(data.reminder, node, "reload"); |  | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|             $saveBtn.querySelector("span.icon > i").classList = ["fas fa-check"]; |             $saveBtn.querySelector("span.icon > i").classList = ["fas fa-check"]; | ||||||
| @@ -651,7 +557,7 @@ document.querySelectorAll(".show-modal").forEach((element) => { | |||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| document.addEventListener("DOMContentLoaded", async () => { | document.addEventListener("DOMContentLoaded", () => { | ||||||
|     $loader.classList.remove("is-hidden"); |     $loader.classList.remove("is-hidden"); | ||||||
|  |  | ||||||
|     mentions.attach(document.querySelectorAll("textarea")); |     mentions.attach(document.querySelectorAll("textarea")); | ||||||
| @@ -671,7 +577,7 @@ document.addEventListener("DOMContentLoaded", async () => { | |||||||
|         hideBox.closest(".reminderContent").classList.toggle("is-collapsed"); |         hideBox.closest(".reminderContent").classList.toggle("is-collapsed"); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     await fetch("/dashboard/api/user") |     fetch("/dashboard/api/user") | ||||||
|         .then((response) => response.json()) |         .then((response) => response.json()) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|             if (data.error) { |             if (data.error) { | ||||||
| @@ -685,7 +591,7 @@ document.addEventListener("DOMContentLoaded", async () => { | |||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|     await fetch("/dashboard/api/user/guilds") |     fetch("/dashboard/api/user/guilds") | ||||||
|         .then((response) => response.json()) |         .then((response) => response.json()) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|             if (data.error) { |             if (data.error) { | ||||||
| @@ -706,56 +612,36 @@ document.addEventListener("DOMContentLoaded", async () => { | |||||||
|                             "%guildname%", |                             "%guildname%", | ||||||
|                             guild.name |                             guild.name | ||||||
|                         ); |                         ); | ||||||
|  |                         $anchor.dataset["guild"] = guild.id; | ||||||
|                         $anchor.dataset["name"] = guild.name; |                         $anchor.dataset["name"] = guild.name; | ||||||
|                         $anchor.href = `/dashboard/${guild.id}/reminders`; |                         $anchor.href = `/dashboard/${guild.id}?name=${guild.name}`; | ||||||
|  |  | ||||||
|                         const $li = $anchor.parentElement; |                         $anchor.addEventListener("click", async (e) => { | ||||||
|                         $li.dataset["guild"] = guild.id; |  | ||||||
|  |  | ||||||
|                         $li.querySelectorAll("a").forEach((el) => { |  | ||||||
|                             el.addEventListener("click", (e) => { |  | ||||||
|                                 const pane = el.dataset["pane"]; |  | ||||||
|                                 const slug = el.dataset["slug"]; |  | ||||||
|  |  | ||||||
|                                 if (pane !== undefined && slug !== undefined) { |  | ||||||
|                             e.preventDefault(); |                             e.preventDefault(); | ||||||
|  |                             window.history.pushState({}, "", `/dashboard/${guild.id}`); | ||||||
|                                     switch_pane(pane); |  | ||||||
|  |  | ||||||
|                                     window.history.pushState( |  | ||||||
|                                         {}, |  | ||||||
|                                         "", |  | ||||||
|                                         `/dashboard/${guild.id}/${slug}` |  | ||||||
|                                     ); |  | ||||||
|                             const event = new CustomEvent("guildSwitched", { |                             const event = new CustomEvent("guildSwitched", { | ||||||
|                                 detail: { |                                 detail: { | ||||||
|  |                                     guild_name: guild.name, | ||||||
|                                     guild_id: guild.id, |                                     guild_id: guild.id, | ||||||
|                                             pane, |  | ||||||
|                                 }, |                                 }, | ||||||
|                             }); |                             }); | ||||||
|  |  | ||||||
|                             document.dispatchEvent(event); |                             document.dispatchEvent(event); | ||||||
|                                 } |  | ||||||
|                             }); |  | ||||||
|                         }); |                         }); | ||||||
|  |  | ||||||
|                         element.append($clone); |                         element.append($clone); | ||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 const matches = window.location.href.match( |                 const matches = window.location.href.match(/dashboard\/(\d+)/); | ||||||
|                     /dashboard\/(\d+)(\/)?([a-zA-Z\-]+)?/ |  | ||||||
|                 ); |  | ||||||
|                 if (matches) { |                 if (matches) { | ||||||
|                     let id = matches[1]; |                     let id = matches[1]; | ||||||
|                     let kind = matches[3]; |  | ||||||
|                     let name = guildNames[id]; |                     let name = guildNames[id]; | ||||||
|  |  | ||||||
|                     const event = new CustomEvent("guildSwitched", { |                     const event = new CustomEvent("guildSwitched", { | ||||||
|                         detail: { |                         detail: { | ||||||
|                             guild_name: name, |                             guild_name: name, | ||||||
|                             guild_id: id, |                             guild_id: id, | ||||||
|                             pane: kind, |  | ||||||
|                         }, |                         }, | ||||||
|                     }); |                     }); | ||||||
|  |  | ||||||
| @@ -829,7 +715,6 @@ $createReminderBtn.addEventListener("click", async () => { | |||||||
|     let reminder = await serialize_reminder($createReminder, "create"); |     let reminder = await serialize_reminder($createReminder, "create"); | ||||||
|     if (reminder.error) { |     if (reminder.error) { | ||||||
|         show_error(reminder.error); |         show_error(reminder.error); | ||||||
|         $createReminderBtn.querySelector("span.icon > i").classList = ["fas fa-sparkles"]; |  | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -887,14 +772,6 @@ $createTemplateBtn.addEventListener("click", async () => { | |||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|     let reminder = await serialize_reminder($createReminder, "template"); |     let reminder = await serialize_reminder($createReminder, "template"); | ||||||
|     if (reminder.error) { |  | ||||||
|         show_error(reminder.error); |  | ||||||
|         $createTemplateBtn.querySelector("span.icon > i").classList = [ |  | ||||||
|             "fas fa-file-spreadsheet", |  | ||||||
|         ]; |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let guild = guildId(); |     let guild = guildId(); | ||||||
|  |  | ||||||
|     fetch(`/dashboard/api/guild/${guild}/templates`, { |     fetch(`/dashboard/api/guild/${guild}/templates`, { | ||||||
| @@ -936,7 +813,6 @@ $loadTemplateBtn.addEventListener("click", (ev) => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| $deleteTemplateBtn.addEventListener("click", (ev) => { | $deleteTemplateBtn.addEventListener("click", (ev) => { | ||||||
|     if (parseInt($templateSelect.value) !== null) { |  | ||||||
|     fetch(`/dashboard/api/guild/${guildId()}/templates`, { |     fetch(`/dashboard/api/guild/${guildId()}/templates`, { | ||||||
|         method: "DELETE", |         method: "DELETE", | ||||||
|         headers: { |         headers: { | ||||||
| @@ -954,7 +830,13 @@ $deleteTemplateBtn.addEventListener("click", (ev) => { | |||||||
|                     .remove(); |                     .remove(); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } | }); | ||||||
|  |  | ||||||
|  | document.querySelectorAll("textarea.autoresize").forEach((element) => { | ||||||
|  |     element.addEventListener("input", () => { | ||||||
|  |         element.style.height = ""; | ||||||
|  |         element.style.height = element.scrollHeight + 3 + "px"; | ||||||
|  |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| let $img; | let $img; | ||||||
| @@ -1012,13 +894,6 @@ document.addEventListener("remindersLoaded", () => { | |||||||
|                 window.getComputedStyle($discordFrame).borderLeftColor; |                 window.getComputedStyle($discordFrame).borderLeftColor; | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     document.querySelectorAll("textarea.autoresize").forEach((element) => { |  | ||||||
|         element.addEventListener("input", () => { |  | ||||||
|             element.style.height = ""; |  | ||||||
|             element.style.height = element.scrollHeight + 3 + "px"; |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| function check_embed_fields() { | function check_embed_fields() { | ||||||
| @@ -1094,13 +969,6 @@ document.addEventListener("click", (ev) => { | |||||||
|     if (ev.target.closest("button.inline-btn") !== null) { |     if (ev.target.closest("button.inline-btn") !== null) { | ||||||
|         let inlined = ev.target.closest(".embed-field-box").dataset["inlined"]; |         let inlined = ev.target.closest(".embed-field-box").dataset["inlined"]; | ||||||
|         ev.target.closest(".embed-field-box").dataset["inlined"] = |         ev.target.closest(".embed-field-box").dataset["inlined"] = | ||||||
|             inlined === "1" ? "0" : "1"; |             inlined == "1" ? "0" : "1"; | ||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| document.addEventListener("DOMContentLoaded", () => { |  | ||||||
|     let now = luxon.DateTime.now().setZone(timezone); |  | ||||||
|     document.querySelectorAll(".prefill-now").forEach((el) => { |  | ||||||
|         el.value = now.toFormat("yyyy-LL-dd'T'HH:mm:ss"); |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
|   | |||||||
| @@ -1,45 +0,0 @@ | |||||||
| function loadErrors() { |  | ||||||
|     return fetch( |  | ||||||
|         `/dashboard/api/guild/${guildId()}/reminders?status=deleted,sent,failed` |  | ||||||
|     ).then((response) => response.json()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| document.addEventListener("paneLoad", (ev) => { |  | ||||||
|     if (ev.detail.pane !== "errors") { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     document.querySelectorAll(".reminderError").forEach((el) => el.remove()); |  | ||||||
|  |  | ||||||
|     const template = document.getElementById("reminderError"); |  | ||||||
|     const container = document.getElementById("reminderLog"); |  | ||||||
|  |  | ||||||
|     loadErrors() |  | ||||||
|         .then((res) => { |  | ||||||
|             res = res |  | ||||||
|                 .filter((r) => r.status_change_time !== null) |  | ||||||
|                 .sort((a, b) => a.status_change_time < b.status_change_time); |  | ||||||
|  |  | ||||||
|             for (const reminder of res) { |  | ||||||
|                 const newRow = template.content.cloneNode(true); |  | ||||||
|  |  | ||||||
|                 newRow.querySelector(".reminderError").dataset["case"] = reminder.status; |  | ||||||
|  |  | ||||||
|                 const statusTime = new luxon.DateTime.fromISO( |  | ||||||
|                     reminder.status_change_time, |  | ||||||
|                     { zone: "UTC" } |  | ||||||
|                 ); |  | ||||||
|                 newRow.querySelector(".reminderName").textContent = reminder.name; |  | ||||||
|                 newRow.querySelector(".reminderMessage").textContent = |  | ||||||
|                     reminder.status_message; |  | ||||||
|                 newRow.querySelector(".reminderTime").textContent = statusTime |  | ||||||
|                     .toLocal() |  | ||||||
|                     .toLocaleString(luxon.DateTime.DATETIME_MED); |  | ||||||
|  |  | ||||||
|                 container.appendChild(newRow); |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|         .finally(() => { |  | ||||||
|             $loader.classList.add("is-hidden"); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| const REPORTER_ID = crypto.randomUUID(); |  | ||||||
|  |  | ||||||
| window.addEventListener("error", async (ev) => { |  | ||||||
|     await fetch("/report", { |  | ||||||
|         method: "POST", |  | ||||||
|         body: JSON.stringify({ |  | ||||||
|             reporterId: REPORTER_ID, |  | ||||||
|             url: window.location.href, |  | ||||||
|             relativeTimestamp: ev.timeStamp, |  | ||||||
|             errorMessage: ev.message, |  | ||||||
|             errorLine: ev.lineno, |  | ||||||
|             errorFile: ev.filename, |  | ||||||
|             errorType: ev.type, |  | ||||||
|         }), |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| @@ -1,89 +0,0 @@ | |||||||
| <!DOCTYPE html> |  | ||||||
| <html lang="EN"> |  | ||||||
| <head> |  | ||||||
|     <script src="/static/js/reporter.js" type="application/javascript"></script> |  | ||||||
|  |  | ||||||
|     <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/favicon/site.webmanifest"> |  | ||||||
|     <meta name="msapplication-TileColor" content="#da532c"> |  | ||||||
|     <meta name="theme-color" content="#ffffff"> |  | ||||||
|  |  | ||||||
|     <title>Reminder Bot | Admin</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"> |  | ||||||
|  |  | ||||||
|     <script src="/static/js/luxon.min.js"></script> |  | ||||||
| </head> |  | ||||||
| <body style="width: 100%;"> |  | ||||||
|  |  | ||||||
| <p class="title pageTitle">Admin dashboard</p> |  | ||||||
| <section id="main"> |  | ||||||
|     <div class="stat-row"> |  | ||||||
|         <div class="stat-box" style="height: 400px;"> |  | ||||||
|             <canvas id="schedule"></canvas> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="stat-row"> |  | ||||||
|         <div class="stat-box figure"> |  | ||||||
|             <p>Backlog</p> |  | ||||||
|             <p class="figure-num" id="backlog">?</p> |  | ||||||
|         </div> |  | ||||||
|         <div class="stat-box figure"> |  | ||||||
|             <p>Reminders</p> |  | ||||||
|             <p class="figure-num" id="reminders">?</p> |  | ||||||
|         </div> |  | ||||||
|         <div class="stat-box figure"> |  | ||||||
|             <p>Intervals</p> |  | ||||||
|             <p class="figure-num" id="intervals">?</p> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="stat-row"> |  | ||||||
|         <div class="stat-box" style="height: 400px;"> |  | ||||||
|             <canvas id="scheduleLong"></canvas> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="stat-row"> |  | ||||||
|         <div class="stat-box figure"> |  | ||||||
|             <p>Last 31 days (success)</p> |  | ||||||
|             <p class="figure-num" id="historySent">?</p> |  | ||||||
|         </div> |  | ||||||
|         <div class="stat-box figure"> |  | ||||||
|             <p>Last 31 days (failed)</p> |  | ||||||
|             <p class="figure-num" id="historyFailed">?</p> |  | ||||||
|         </div> |  | ||||||
|         <div class="stat-box figure"> |  | ||||||
|             <p>Last 31 days (failure rate)</p> |  | ||||||
|             <p class="figure-num" id="failRate">?</p> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="stat-row"> |  | ||||||
|         <div class="stat-box" style="height: 400px;"> |  | ||||||
|             <canvas id="historyLong"></canvas> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </section> |  | ||||||
|  |  | ||||||
| <script src="/static/js/chart.js" defer></script> |  | ||||||
| <script src="/static/js/chartjs-adapter-luxon.js" defer></script> |  | ||||||
| <script src="/static/js/admin.js" defer></script> |  | ||||||
|  |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
| @@ -13,7 +13,7 @@ | |||||||
|     <link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png"> |     <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="32x32" href="/static/favicon/favicon-32x32.png"> | ||||||
|     <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png"> |     <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png"> | ||||||
|     <link rel="manifest" href="/static/site.webmanifest"> |     <link rel="manifest" href="/static/favicon/site.webmanifest"> | ||||||
|     <meta name="msapplication-TileColor" content="#da532c"> |     <meta name="msapplication-TileColor" content="#da532c"> | ||||||
|     <meta name="theme-color" content="#ffffff"> |     <meta name="theme-color" content="#ffffff"> | ||||||
|  |  | ||||||
| @@ -51,8 +51,8 @@ | |||||||
|                 <a class="navbar-item" href="https://invite.reminder-bot.com"> |                 <a class="navbar-item" href="https://invite.reminder-bot.com"> | ||||||
|                     <i class="fas fa-plus"></i> |                     <i class="fas fa-plus"></i> | ||||||
|                 </a> |                 </a> | ||||||
|                 <a class="navbar-item" href="https://gitea.jellypro.xyz/jude"> |                 <a class="navbar-item" href="https://github.com/jellywx"> | ||||||
|                     <i class="fab fa-git-square"></i> |                     <i class="fab fa-github"></i> | ||||||
|                 </a> |                 </a> | ||||||
|                 <a class="navbar-item" href="https://discord.jellywx.com"> |                 <a class="navbar-item" href="https://discord.jellywx.com"> | ||||||
|                     <i class="fab fa-discord"></i> |                     <i class="fab fa-discord"></i> | ||||||
| @@ -128,7 +128,7 @@ | |||||||
|                 </div> |                 </div> | ||||||
|             {% elif show_login %} |             {% elif show_login %} | ||||||
|                 <div class="hero-foot has-text-centered"> |                 <div class="hero-foot has-text-centered"> | ||||||
|                     <a class="button is-size-4 is-rounded is-light" href="/login/discord"> |                     <a class="button is-size-4 is-rounded is-light" href="/oauth/login"> | ||||||
|                         <p class="is-size-4"> |                         <p class="is-size-4"> | ||||||
|                             <span>Login with Discord</span> <span class="icon"><i class="fas fa-chevron-right"></i></span> |                             <span>Login with Discord</span> <span class="icon"><i class="fas fa-chevron-right"></i></span> | ||||||
|                         </p> |                         </p> | ||||||
| @@ -155,7 +155,7 @@ | |||||||
|                 <br> |                 <br> | ||||||
|                 <a href="/cookies">Cookies</a> | <a href="/privacy">Privacy Policy</a> | <a href="/terms">Terms of Service</a> |                 <a href="/cookies">Cookies</a> | <a href="/privacy">Privacy Policy</a> | <a href="/terms">Terms of Service</a> | ||||||
|                 <br> |                 <br> | ||||||
|                 <a href="https://patreon.com/jellywx"><strong>Patreon</strong></a> | <a href="https://discord.jellywx.com"><strong>Discord</strong></a> | <a href="https://gitea.jellypro.xyz/jude"><strong>Gitea</strong></a> |                 <a href="https://patreon.com/jellywx"><strong>Patreon</strong></a> | <a href="https://discord.jellywx.com"><strong>Discord</strong></a> | <a href="https://github.com/JellyWX"><strong>GitHub</strong></a> | ||||||
|                 <br> |                 <br> | ||||||
|                 or, <a href="mailto:jude@jellywx.com">Email me</a> |                 or, <a href="mailto:jude@jellywx.com">Email me</a> | ||||||
|             </p> |             </p> | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| <html lang="EN"> | <html lang="EN"> | ||||||
| <head> | <head> | ||||||
|     <script src="/static/js/reporter.js" type="application/javascript"></script> |  | ||||||
|  |  | ||||||
|     <meta name="description" content="The most powerful Discord Reminders Bot"> |     <meta name="description" content="The most powerful Discord Reminders Bot"> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> |     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
|     <meta charset="UTF-8"> |     <meta charset="UTF-8"> | ||||||
| @@ -40,14 +38,14 @@ | |||||||
|     <div class="navbar-brand"> |     <div class="navbar-brand"> | ||||||
|         <a class="navbar-item" href="/"> |         <a class="navbar-item" href="/"> | ||||||
|             <figure class="image"> |             <figure class="image"> | ||||||
|                 <img width="28px" height="28px" src="/static/img/logo_nobg.webp" alt="Reminder Bot Logo"> |                 <img src="/static/img/logo_flat.webp" alt="Reminder Bot Logo"> | ||||||
|             </figure> |             </figure> | ||||||
|         </a> |         </a> | ||||||
|  |  | ||||||
|         <p class="navbar-item pageTitle"> |         <p class="navbar-item pageTitle"> | ||||||
|         </p> |         </p> | ||||||
|  |  | ||||||
|         <a role="button" class="dashboard-burger navbar-burger is-right" aria-label="menu" aria-expanded="false" |         <a role="button" class="navbar-burger is-right" aria-label="menu" aria-expanded="false" | ||||||
|            data-target="mobileSidebar"> |            data-target="mobileSidebar"> | ||||||
|             <span aria-hidden="true"></span> |             <span aria-hidden="true"></span> | ||||||
|             <span aria-hidden="true"></span> |             <span aria-hidden="true"></span> | ||||||
| @@ -193,8 +191,19 @@ | |||||||
|                     </label> |                     </label> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|  |             <div class="control"> | ||||||
|  |                 <div class="field"> | ||||||
|  |                     <label> | ||||||
|  |                         <input type="radio" class="default-width" name="exportSelect" value="reminder_templates"> | ||||||
|  |                         Reminder templates | ||||||
|  |                     </label> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|             <br> |             <br> | ||||||
|             <div class="has-text-centered"> |             <div class="has-text-centered"> | ||||||
|  |                 <div style="color: red; font-weight: bold;"> | ||||||
|  |                     By selecting "Import", you understand that this will overwrite existing data. | ||||||
|  |                 </div> | ||||||
|                 <div style="color: red"> |                 <div style="color: red"> | ||||||
|                     Please first read the <a href="/help/iemanager">support page</a> |                     Please first read the <a href="/help/iemanager">support page</a> | ||||||
|                 </div> |                 </div> | ||||||
| @@ -233,54 +242,7 @@ | |||||||
|     <div class="column is-2 is-sidebar-menu dashboard-sidebar is-hidden-touch"> |     <div class="column is-2 is-sidebar-menu dashboard-sidebar is-hidden-touch"> | ||||||
|         <a href="/"> |         <a href="/"> | ||||||
|             <div class="brand"> |             <div class="brand"> | ||||||
|                 <img src="/static/img/logo_nobg.webp" alt="Reminder bot logo" |                 <img src="/static/img/logo_flat.webp" alt="Reminder bot logo" | ||||||
|                      width="52px" height="52px" |  | ||||||
|                      class="dashboard-brand"> |  | ||||||
|             </div> |  | ||||||
|         </a> |  | ||||||
|         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 160"> |  | ||||||
|             <g transform="scale(1, 0.5)"> |  | ||||||
|                 <path fill="#8fb677" fill-opacity="1" |  | ||||||
|                       d="M0,192L60,170.7C120,149,240,107,360,96C480,85,600,107,720,138.7C840,171,960,213,1080,197.3C1200,181,1320,107,1380,69.3L1440,32L1440,0L1380,0C1320,0,1200,0,1080,0C960,0,840,0,720,0C600,0,480,0,360,0C240,0,120,0,60,0L0,0Z"></path> |  | ||||||
|             </g> |  | ||||||
|         </svg> |  | ||||||
|         <aside class="menu"> |  | ||||||
|             <p class="menu-label"> |  | ||||||
|                 Servers |  | ||||||
|             </p> |  | ||||||
|             <ul class="menu-list guildList"> |  | ||||||
|  |  | ||||||
|             </ul> |  | ||||||
|             <div class="aside-footer"> |  | ||||||
|                 <p class="menu-label"> |  | ||||||
|                     Options |  | ||||||
|                 </p> |  | ||||||
|                 <ul class="menu-list"> |  | ||||||
|                     <li> |  | ||||||
|                         {# |  | ||||||
|                         <a class="show-modal" data-modal="dataManagerModal"> |  | ||||||
|                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export |  | ||||||
|                         </a> |  | ||||||
|                         #} |  | ||||||
|                         <a class="show-modal" data-modal="chooseTimezoneModal"> |  | ||||||
|                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone |  | ||||||
|                         </a> |  | ||||||
|                         <a href="/login/discord/logout"> |  | ||||||
|                             <span class="icon"><i class="fas fa-sign-out"></i></span> Log out |  | ||||||
|                         </a> |  | ||||||
|                         <a href="https://discord.jellywx.com" class="feedback"> |  | ||||||
|                             <span class="icon"><i class="fab fa-discord"></i></span> Give feedback |  | ||||||
|                         </a> |  | ||||||
|                     </li> |  | ||||||
|                 </ul> |  | ||||||
|             </div> |  | ||||||
|         </aside> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div class="dashboard-sidebar mobile-sidebar is-hidden-desktop" id="mobileSidebar"> |  | ||||||
|         <a href="/"> |  | ||||||
|             <div class="brand"> |  | ||||||
|                 <img src="/static/img/logo_nobg.webp" alt="Reminder bot logo" |  | ||||||
|                      class="dashboard-brand"> |                      class="dashboard-brand"> | ||||||
|             </div> |             </div> | ||||||
|         </a> |         </a> | ||||||
| @@ -303,19 +265,49 @@ | |||||||
|                 </p> |                 </p> | ||||||
|                 <ul class="menu-list"> |                 <ul class="menu-list"> | ||||||
|                     <li> |                     <li> | ||||||
|                         {# |  | ||||||
|                         <a class="show-modal" data-modal="dataManagerModal"> |                         <a class="show-modal" data-modal="dataManagerModal"> | ||||||
|                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export |                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export | ||||||
|                         </a> |                         </a> | ||||||
|                         #} |  | ||||||
|                         <a class="show-modal" data-modal="chooseTimezoneModal"> |                         <a class="show-modal" data-modal="chooseTimezoneModal"> | ||||||
|                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone |                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone | ||||||
|                         </a> |                         </a> | ||||||
|                         <a href="/login/discord/logout"> |                     </li> | ||||||
|                             <span class="icon"><i class="fas fa-sign-out"></i></span> Log out |                 </ul> | ||||||
|  |             </div> | ||||||
|  |         </aside> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="dashboard-sidebar mobile-sidebar is-hidden-desktop" id="mobileSidebar"> | ||||||
|  |         <a href="/"> | ||||||
|  |             <div class="brand"> | ||||||
|  |                 <img src="/static/img/logo_flat.webp" alt="Reminder bot logo" | ||||||
|  |                      class="dashboard-brand"> | ||||||
|  |             </div> | ||||||
|         </a> |         </a> | ||||||
|                         <a href="https://discord.jellywx.com/" class="feedback"> |         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 160"> | ||||||
|                             <span class="icon"><i class="fab fa-discord"></i></span> Give feedback |             <g transform="scale(1, 0.5)"> | ||||||
|  |                 <path fill="#8fb677" fill-opacity="1" | ||||||
|  |                       d="M0,192L60,170.7C120,149,240,107,360,96C480,85,600,107,720,138.7C840,171,960,213,1080,197.3C1200,181,1320,107,1380,69.3L1440,32L1440,0L1380,0C1320,0,1200,0,1080,0C960,0,840,0,720,0C600,0,480,0,360,0C240,0,120,0,60,0L0,0Z"></path> | ||||||
|  |             </g> | ||||||
|  |         </svg> | ||||||
|  |         <aside class="menu"> | ||||||
|  |             <p class="menu-label"> | ||||||
|  |                 Servers | ||||||
|  |             </p> | ||||||
|  |             <ul class="menu-list guildList"> | ||||||
|  |  | ||||||
|  |             </ul> | ||||||
|  |             <div class="aside-footer"> | ||||||
|  |                 <p class="menu-label"> | ||||||
|  |                     Settings | ||||||
|  |                 </p> | ||||||
|  |                 <ul class="menu-list"> | ||||||
|  |                     <li> | ||||||
|  |                         <a class="show-modal" data-modal="dataManagerModal"> | ||||||
|  |                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export | ||||||
|  |                         </a> | ||||||
|  |                         <a class="show-modal" data-modal="chooseTimezoneModal"> | ||||||
|  |                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone | ||||||
|                         </a> |                         </a> | ||||||
|                     </li> |                     </li> | ||||||
|                 </ul> |                 </ul> | ||||||
| @@ -333,17 +325,25 @@ | |||||||
|                 <p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p> |                 <p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p> | ||||||
|             </div> |             </div> | ||||||
|         </section> |         </section> | ||||||
|         <section data-name="reminders" class="is-hidden"> |         <section id="guild" class="is-hidden"> | ||||||
|             {% include "reminder_dashboard/reminder_dashboard" %} |             {% include "reminder_dashboard/reminder_dashboard" %} | ||||||
|         </section> |         </section> | ||||||
|         <section data-name="errors" class="is-hidden"> |         <section id="guild-error" class="is-hidden hero is-fullheight"> | ||||||
|             {% include "reminder_dashboard/reminder_errors" %} |             <div class="hero-body"> | ||||||
|         </section> |                 <div class="container has-text-centered"> | ||||||
|         <section data-name="guild-error" class="is-hidden"> |                     <p class="title"> | ||||||
|             {% include "reminder_dashboard/guild_error" %} |                         We couldn't get this server's data | ||||||
|         </section> |                     </p> | ||||||
|         <section data-name="user-error" class="is-hidden"> |                     <p class="subtitle"> | ||||||
|             {% include "reminder_dashboard/user_error" %} |                         Please check Reminder Bot is in the server, and has correct permissions. | ||||||
|  |                     </p> | ||||||
|  |                     <a class="button is-size-4 is-rounded is-success" href="https://invite.reminder-bot.com"> | ||||||
|  |                         <p class="is-size-4"> | ||||||
|  |                             <span>Add to Server</span> <span class="icon"><i class="fas fa-chevron-right"></i></span> | ||||||
|  |                         </p> | ||||||
|  |                     </a> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|         </section> |         </section> | ||||||
|     </div> |     </div> | ||||||
|     <!-- /main content --> |     <!-- /main content --> | ||||||
| @@ -376,28 +376,14 @@ | |||||||
|  |  | ||||||
| <template id="guildListEntry"> | <template id="guildListEntry"> | ||||||
|     <li> |     <li> | ||||||
|         <a class="switch-pane" data-pane="reminders" data-slug="reminders"> |         <a class="switch-pane" data-pane="guild"> | ||||||
|             <span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span> |             <span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span> | ||||||
|         </a> |         </a> | ||||||
|         <ul class="guild-submenu"> |  | ||||||
|             <li> |  | ||||||
|                 <a class="switch-pane" data-pane="reminders" data-slug="reminders"> |  | ||||||
|                     <span class="icon"><i class="fas fa-calendar-alt"></i></span> Reminders |  | ||||||
|                 </a> |  | ||||||
|                 <a class="switch-pane" data-pane="errors" data-slug="errors"> |  | ||||||
|                     <span class="icon"><i class="fas fa-file-alt"></i></span> Logs |  | ||||||
|                 </a> |  | ||||||
|             </li> |  | ||||||
|         </ul> |  | ||||||
|     </li> |     </li> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <template id="guildReminder"> | <template id="guildReminder"> | ||||||
|     {% include "reminder_dashboard/templates/guild_reminder" %} |     {% include "reminder_dashboard/guild_reminder" %} | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <template id="reminderError"> |  | ||||||
|     {% include "reminder_dashboard/templates/reminder_error" %} |  | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script src="/static/js/iro.js"></script> | <script src="/static/js/iro.js"></script> | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ | |||||||
|             </div> |             </div> | ||||||
|             <div class="tile is-parent"> |             <div class="tile is-parent"> | ||||||
|                 <article class="tile is-child notification"> |                 <article class="tile is-child notification"> | ||||||
|                     <p class="title">Create reminders</p> |                     <p class="title">Creating reminders</p> | ||||||
|                     <p class="subtitle">Learn to create reminders for your server</p> |                     <p class="subtitle">Learn to create reminders for your server</p> | ||||||
|                     <div class="content has-text-centered"> |                     <div class="content has-text-centered"> | ||||||
|                         <a class="button is-size-4 is-rounded is-light" href="/help/create_reminder"> |                         <a class="button is-size-4 is-rounded is-light" href="/help/create_reminder"> | ||||||
| @@ -52,47 +52,47 @@ | |||||||
|                 </article> |                 </article> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
| <!--        <div class="tile is-ancestor">--> |         <div class="tile is-ancestor"> | ||||||
| <!--            <div class="tile is-parent">--> |             <div class="tile is-parent"> | ||||||
| <!--                <article class="tile is-child notification">--> |                 <article class="tile is-child notification"> | ||||||
| <!--                    <p class="title">Timers</p>--> |                     <p class="title">Timers</p> | ||||||
| <!--                    <p class="subtitle">Learn to manage timers</p>--> |                     <p class="subtitle">Learn to manage timers</p> | ||||||
| <!--                    <div class="content has-text-centered">--> |                     <div class="content has-text-centered"> | ||||||
| <!--                        <a class="button is-size-4 is-rounded is-light" href="/help/timers">--> |                         <a class="button is-size-4 is-rounded is-light" href="/help/timers"> | ||||||
| <!--                            <p class="is-size-4">--> |                             <p class="is-size-4"> | ||||||
| <!--                                Read <span class="icon"><i class="fas fa-chevron-right"></i></span>--> |                                 Read <span class="icon"><i class="fas fa-chevron-right"></i></span> | ||||||
| <!--                            </p>--> |                             </p> | ||||||
| <!--                        </a>--> |                         </a> | ||||||
| <!--                    </div>--> |                     </div> | ||||||
| <!--                </article>--> |                 </article> | ||||||
| <!--            </div>--> |             </div> | ||||||
| <!--            <div class="tile is-parent">--> |             <div class="tile is-parent"> | ||||||
| <!--                <article class="tile is-child notification">--> |                 <article class="tile is-child notification"> | ||||||
| <!--                    <p class="title">Todo Lists</p>--> |                     <p class="title">Todo Lists</p> | ||||||
| <!--                    <p class="subtitle">Learn to manage various todo lists</p>--> |                     <p class="subtitle">Learn to manage various todo lists</p> | ||||||
| <!--                    <div class="content has-text-centered">--> |                     <div class="content has-text-centered"> | ||||||
| <!--                        <a class="button is-size-4 is-rounded is-light" href="/help/todo_lists">--> |                         <a class="button is-size-4 is-rounded is-light" href="/help/todo_lists"> | ||||||
| <!--                            <p class="is-size-4">--> |                             <p class="is-size-4"> | ||||||
| <!--                                Read <span class="icon"><i class="fas fa-chevron-right"></i></span>--> |                                 Read <span class="icon"><i class="fas fa-chevron-right"></i></span> | ||||||
| <!--                            </p>--> |                             </p> | ||||||
| <!--                        </a>--> |                         </a> | ||||||
| <!--                    </div>--> |                     </div> | ||||||
| <!--                </article>--> |                 </article> | ||||||
| <!--            </div>--> |             </div> | ||||||
| <!--            <div class="tile is-parent is-vertical">--> |             <div class="tile is-parent is-vertical"> | ||||||
| <!--                <article class="tile is-child notification">--> |                 <article class="tile is-child notification"> | ||||||
| <!--                    <p class="title">Macros</p>--> |                     <p class="title">Macros</p> | ||||||
| <!--                    <p class="subtitle">Learn how to create combination commands called macros, to suit advanced use cases</p>--> |                     <p class="subtitle">Learn how to create combination commands called macros, to suit advanced use cases</p> | ||||||
| <!--                    <div class="content has-text-centered">--> |                     <div class="content has-text-centered"> | ||||||
| <!--                        <a class="button is-size-4 is-rounded is-light" href="/help/macros">--> |                         <a class="button is-size-4 is-rounded is-light" href="/help/macros"> | ||||||
| <!--                            <p class="is-size-4">--> |                             <p class="is-size-4"> | ||||||
| <!--                                Read <span class="icon"><i class="fas fa-chevron-right"></i></span>--> |                                 Read <span class="icon"><i class="fas fa-chevron-right"></i></span> | ||||||
| <!--                            </p>--> |                             </p> | ||||||
| <!--                        </a>--> |                         </a> | ||||||
| <!--                    </div>--> |                     </div> | ||||||
| <!--                </article>--> |                 </article> | ||||||
| <!--            </div>--> |             </div> | ||||||
| <!--        </div>--> |         </div> | ||||||
|         <div class="tile is-ancestor"> |         <div class="tile is-ancestor"> | ||||||
|             <div class="tile is-parent"> |             <div class="tile is-parent"> | ||||||
|                 <article class="tile is-child notification"> |                 <article class="tile is-child notification"> | ||||||
| @@ -107,23 +107,7 @@ | |||||||
|                     </div> |                     </div> | ||||||
|                 </article> |                 </article> | ||||||
|             </div> |             </div> | ||||||
|             <div class="tile is-parent is-vertical"> |  | ||||||
|                 {# |  | ||||||
|                 <article class="tile is-child notification"> |  | ||||||
|                     <p class="title">Import/export</p> |  | ||||||
|                     <p class="subtitle">Learn how to import and export data from the dashboard</p> |  | ||||||
|                     <div class="content has-text-centered"> |  | ||||||
|                         <a class="button is-size-4 is-rounded is-light" href="/help/iemanager"> |  | ||||||
|                             <p class="is-size-4"> |  | ||||||
|                                 Read <span class="icon"><i class="fas fa-chevron-right"></i></span> |  | ||||||
|                             </p> |  | ||||||
|                         </a> |  | ||||||
|                     </div> |  | ||||||
|                 </article> |  | ||||||
|                 #} |  | ||||||
|             </div> |  | ||||||
|             <div class="tile is-parent"> |             <div class="tile is-parent"> | ||||||
|                 {# |  | ||||||
|                 <article class="tile is-child notification"> |                 <article class="tile is-child notification"> | ||||||
|                     <p class="title">Dashboard</p> |                     <p class="title">Dashboard</p> | ||||||
|                     <p class="subtitle">Learn to use the interactive web dashboard</p> |                     <p class="subtitle">Learn to use the interactive web dashboard</p> | ||||||
| @@ -135,7 +119,19 @@ | |||||||
|                         </a> |                         </a> | ||||||
|                     </div> |                     </div> | ||||||
|                 </article> |                 </article> | ||||||
|                 #} |             </div> | ||||||
|  |             <div class="tile is-parent is-vertical"> | ||||||
|  |                 <article class="tile is-child notification"> | ||||||
|  |                     <p class="title">Import/Export</p> | ||||||
|  |                     <p class="subtitle">Learn how to import and export data from the dashboard</p> | ||||||
|  |                     <div class="content has-text-centered"> | ||||||
|  |                         <a class="button is-size-4 is-rounded is-light" href="/help/iemanager"> | ||||||
|  |                             <p class="is-size-4"> | ||||||
|  |                                 Read <span class="icon"><i class="fas fa-chevron-right"></i></span> | ||||||
|  |                             </p> | ||||||
|  |                         </a> | ||||||
|  |                     </div> | ||||||
|  |                 </article> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
| @@ -145,14 +141,14 @@ | |||||||
|             <div class="container has-text-centered"> |             <div class="container has-text-centered"> | ||||||
|                 <p class="title">Need more help?</p> |                 <p class="title">Need more help?</p> | ||||||
|                 <p class="content"> |                 <p class="content"> | ||||||
|                     Please come and ask us! |                     Feel free to come and ask us! | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="hero-foot has-text-centered"> |         <div class="hero-foot has-text-centered"> | ||||||
|             <a class="button is-size-6 is-rounded is-primary" href="https://discord.jellywx.com"> |             <a class="button is-size-6 is-rounded is-primary" href="https://discord.jellywx.com"> | ||||||
|                 <p class="is-size-6"> |                 <p class="is-size-6"> | ||||||
|                     <span>Join Discord</span> <span class="icon"><i class="fas fa-chevron-right"></i></span> |                     Join Discord <span class="icon"><i class="fas fa-chevron-right"></i></span> | ||||||
|                 </p> |                 </p> | ||||||
|             </a> |             </a> | ||||||
|         </div> |         </div> | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ | |||||||
|             <div class="tile is-parent"> |             <div class="tile is-parent"> | ||||||
|                 <article class="tile is-child notification"> |                 <article class="tile is-child notification"> | ||||||
|                     <p class="title">Slash-command Ready <svg aria-hidden="false" width="28" height="28" viewBox="0 0 24 24"><path fill="#777" fill-rule="evenodd" clip-rule="evenodd" d="M5 3C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3H5ZM16.8995 8.41419L15.4853 6.99998L7 15.4853L8.41421 16.8995L16.8995 8.41419Z"></path></svg></p> |                     <p class="title">Slash-command Ready <svg aria-hidden="false" width="28" height="28" viewBox="0 0 24 24"><path fill="#777" fill-rule="evenodd" clip-rule="evenodd" d="M5 3C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3H5ZM16.8995 8.41419L15.4853 6.99998L7 15.4853L8.41421 16.8995L16.8995 8.41419Z"></path></svg></p> | ||||||
|                     <p class="subtitle">Set reminders easily and quickly from anywhere.</p> |                     <p class="subtitle">Set reminders easily and quickly from anywhere</p> | ||||||
|                     <figure class="image"> |                     <figure class="image"> | ||||||
|                         <img class="rounded-corners" src="/static/img/slash-commands.png" alt="Discord slash commands demonstration"> |                         <img class="rounded-corners" src="/static/img/slash-commands.png" alt="Discord slash commands demonstration"> | ||||||
|                     </figure> |                     </figure> | ||||||
| @@ -25,7 +25,7 @@ | |||||||
|             <div class="tile is-parent"> |             <div class="tile is-parent"> | ||||||
|                 <article class="tile is-child notification"> |                 <article class="tile is-child notification"> | ||||||
|                     <p class="title">Advanced Options <span class="icon"><i class="fad fa-palette"></i></span></p> |                     <p class="title">Advanced Options <span class="icon"><i class="fad fa-palette"></i></span></p> | ||||||
|                     <p class="subtitle">Decorate your announcements with our web dashboard.</p> |                     <p class="subtitle">Decorate your announcements with our web dashboard</p> | ||||||
|                     <figure class="image"> |                     <figure class="image"> | ||||||
|                         <img class="rounded-corners" src="/static/img/tournament-demo.png" alt="Advanced options demonstration"> |                         <img class="rounded-corners" src="/static/img/tournament-demo.png" alt="Advanced options demonstration"> | ||||||
|                     </figure> |                     </figure> | ||||||
| @@ -34,62 +34,32 @@ | |||||||
|             <div class="tile is-parent is-vertical"> |             <div class="tile is-parent is-vertical"> | ||||||
|                 <article class="tile is-child notification"> |                 <article class="tile is-child notification"> | ||||||
|                     <p class="title">Unlimited Reminders <span class="icon"><i class="far fa-infinity"></i></span></p> |                     <p class="title">Unlimited Reminders <span class="icon"><i class="far fa-infinity"></i></span></p> | ||||||
|                     <p class="subtitle">Never forget a thing.</p> |                     <p class="subtitle">Never forget a thing</p> | ||||||
|                 </article> |                 </article> | ||||||
|                 <article class="tile is-child notification"> |                 <article class="tile is-child notification"> | ||||||
|                     <p class="title">Repeating Reminders <span class="icon"><i class="fas fa-repeat"></i></span></p> |                     <p class="title">Repeating Reminders <span class="icon"><i class="fas fa-repeat"></i></span></p> | ||||||
|                     <p class="subtitle">Available to <a href="https://patreon.com/jellywx"><span class="patreon-color">Patreon <span class="icon"><i class="fab fa-patreon"></i></span></span></a> subscribers at <strong>$2/month</strong>.</p> |                     <p class="subtitle">Available to <a href="https://patreon.com/jellywx"><span class="patreon-color">Patreon <span class="icon"><i class="fab fa-patreon"></i></span></span></a> subscribers at <strong>$2/month</strong></p> | ||||||
|                 </article> |                 </article> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <section class="hero is-medium"> |     <section class="hero is-small"> | ||||||
|         <div class="hero-body"> |         <div class="hero-body"> | ||||||
|             <div class="columns"> |  | ||||||
|                 <div class="column"> |  | ||||||
|                     <div class="container has-text-centered"> |  | ||||||
|                         <p class="title">Technically-minded?</p> |  | ||||||
|                         <p class="content"> |  | ||||||
|                             Install the bot on your own computer |  | ||||||
|                         </p> |  | ||||||
|                         <a class="button is-size-6 is-rounded is-link" href="https://gitea.jellypro.xyz/jude/reminder-bot"> |  | ||||||
|                             <p class="is-size-6"> |  | ||||||
|                                 <span>Install</span> <span class="icon"><i class="fas fa-chevron-right"></i></span> |  | ||||||
|                             </p> |  | ||||||
|                         </a> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|  |  | ||||||
|                 <div class="column"> |  | ||||||
|             <div class="container has-text-centered"> |             <div class="container has-text-centered"> | ||||||
|                 <p class="title">Ready to go?</p> |                 <p class="title">Ready to go?</p> | ||||||
|                 <p class="content"> |                 <p class="content"> | ||||||
|                             Add the bot to get started |                     Add the bot to get started! | ||||||
|                 </p> |                 </p> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="hero-foot has-text-centered"> | ||||||
|             <a class="button is-size-6 is-rounded is-success" href="https://invite.reminder-bot.com"> |             <a class="button is-size-6 is-rounded is-success" href="https://invite.reminder-bot.com"> | ||||||
|                 <p class="is-size-6"> |                 <p class="is-size-6"> | ||||||
|                                 <span>Add Now</span> <span class="icon"><i class="fas fa-chevron-right"></i></span> |                     Add Now <span class="icon"><i class="fas fa-chevron-right"></i></span> | ||||||
|                 </p> |                 </p> | ||||||
|             </a> |             </a> | ||||||
|         </div> |         </div> | ||||||
|                 </div> |  | ||||||
|  |  | ||||||
|                 <div class="column"> |  | ||||||
|                     <div class="container has-text-centered"> |  | ||||||
|                         <p class="title">Need support?</p> |  | ||||||
|                         <p class="content"> |  | ||||||
|                             Check out our guides, or join our Discord |  | ||||||
|                         </p> |  | ||||||
|                         <a class="button is-size-6 is-rounded is-primary" href="/help"> |  | ||||||
|                             <p class="is-size-6"> |  | ||||||
|                                 <span>Guides</span> <span class="icon"><i class="fas fa-chevron-right"></i></span> |  | ||||||
|                             </p> |  | ||||||
|                         </a> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     </section> |     </section> | ||||||
|  |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ | |||||||
|     <section class="section"> |     <section class="section"> | ||||||
|         <div class="container"> |         <div class="container"> | ||||||
|             <h2 class="title">Who we are</h2> |             <h2 class="title">Who we are</h2> | ||||||
|             <p> |             <p class="is-size-5 pl-6"> | ||||||
|                 Reminder Bot is operated solely by Jude Southworth. You can contact me by email at |                 Reminder Bot is operated solely by Jude Southworth. You can contact me by email at | ||||||
|                 <a href="mailto:jude@jellywx.com">jude@jellywx.com</a>, or via private/public message on Discord at |                 <a href="mailto:jude@jellywx.com">jude@jellywx.com</a>, or via private/public message on Discord at | ||||||
|                 <a href="https://discord.jellywx.com">https://discord.jellywx.com</a>. |                 <a href="https://discord.jellywx.com">https://discord.jellywx.com</a>. | ||||||
| @@ -24,16 +24,12 @@ | |||||||
|     <section class="section"> |     <section class="section"> | ||||||
|         <div class="container"> |         <div class="container"> | ||||||
|             <h2 class="title">What data we collect</h2> |             <h2 class="title">What data we collect</h2> | ||||||
|             <p> |             <p class="is-size-5 pl-6"> | ||||||
|                 Reminder Bot stores limited data necessary for the function of the bot. This data |                 Reminder Bot stores limited data necessary for the function of the bot. This data | ||||||
|                 is your <strong>unique user ID</strong>, <strong>timezone</strong>, and <strong>direct message channel</strong>. |                 is your <strong>unique user ID</strong>, <strong>timezone</strong>, and <strong>direct message channel</strong>. | ||||||
|                 <br> |                 <br> | ||||||
|                 <br> |                 <br> | ||||||
|                 Timezones are provided by the user or the user's browser. |                 Timezones are provided by the user or the user's browser. | ||||||
|                 <br><br> |  | ||||||
|                 Some  additional information is collected by the dashboard for the purpose of debugging.   This is your |  | ||||||
|                 <strong>time spent on the website</strong>, <strong>current URL</strong>, <strong>unique user ID</strong>, |  | ||||||
|                 <strong>unique session token</strong>, <strong>contents of any client errors</strong>. |  | ||||||
|             </p> |             </p> | ||||||
|         </div> |         </div> | ||||||
|     </section> |     </section> | ||||||
| @@ -41,12 +37,10 @@ | |||||||
|     <section class="section"> |     <section class="section"> | ||||||
|         <div class="container"> |         <div class="container"> | ||||||
|             <h2 class="title">Why we collect this data</h2> |             <h2 class="title">Why we collect this data</h2> | ||||||
|             <p> |             <p class="is-size-5 pl-6"> | ||||||
|                 Unique user IDs are stored to <strong>keep track of who sets reminders</strong>. User timezones are |                 Unique user IDs are stored to <strong>keep track of who sets reminders</strong>. User timezones are | ||||||
|                 stored to allow users to set reminders in their local timezone. Direct message channels are stored to |                 stored to allow users to set reminders in their local timezone. Direct message channels are stored to | ||||||
|                 allow the setting of reminders for your direct message channel. |                 allow the setting of reminders for your direct message channel. | ||||||
|                 <br> |  | ||||||
|                 Information collected  by the dashboard is for resolving bugs. |  | ||||||
|             </p> |             </p> | ||||||
|         </div> |         </div> | ||||||
|     </section> |     </section> | ||||||
| @@ -54,7 +48,7 @@ | |||||||
|     <section class="section"> |     <section class="section"> | ||||||
|         <div class="container"> |         <div class="container"> | ||||||
|             <h2 class="title">Who your data is shared with</h2> |             <h2 class="title">Who your data is shared with</h2> | ||||||
|             <p> |             <p class="is-size-5 pl-6"> | ||||||
|                 Your data is also guarded by the privacy policies of <strong>MEGA</strong>, our backup provider, and |                 Your data is also guarded by the privacy policies of <strong>MEGA</strong>, our backup provider, and | ||||||
|                 <strong>Hetzner</strong>, our hosting provider. |                 <strong>Hetzner</strong>, our hosting provider. | ||||||
|             </p> |             </p> | ||||||
| @@ -64,13 +58,17 @@ | |||||||
|     <section class="section"> |     <section class="section"> | ||||||
|         <div class="container"> |         <div class="container"> | ||||||
|             <h2 class="title">Accessing or removing your data</h2> |             <h2 class="title">Accessing or removing your data</h2> | ||||||
|             <p> |             <p class="is-size-5 pl-6"> | ||||||
|                 Your timezone can be removed with the command <strong>/timezone UTC</strong>. Other data can be removed |                 Your timezone can be removed with the command <strong>/timezone UTC</strong>. Other data can be removed | ||||||
|                 on request. Please contact me. |                 on request. Please contact me. | ||||||
|                 <br> |                 <br> | ||||||
|                 <br> |                 <br> | ||||||
|                 Reminders created in a guild/channel will be removed automatically when the bot is removed from the |                 Reminders created in a guild/channel will be removed automatically when the bot is removed from the | ||||||
|                 guild, the guild is deleted, or channel is deleted. Data is otherwise not removed automatically. |                 guild, the guild is deleted, or channel is deleted. Data is otherwise not removed automatically. | ||||||
|  |                 <br> | ||||||
|  |                 <br> | ||||||
|  |                 Reminders deleted with <strong>/del</strong> or via the dashboard are removed from the live database | ||||||
|  |                 instantly, but may persist in backups for up to a year. | ||||||
|             </p> |             </p> | ||||||
|         </div> |         </div> | ||||||
|     </section> |     </section> | ||||||
|   | |||||||
| @@ -1,17 +0,0 @@ | |||||||
| <div class="hero is-fullheight"> |  | ||||||
|     <div class="hero-body"> |  | ||||||
|         <div class="container has-text-centered"> |  | ||||||
|             <p class="title"> |  | ||||||
|                 We couldn't get this server's data |  | ||||||
|             </p> |  | ||||||
|             <p class="subtitle"> |  | ||||||
|                 Please check Reminder Bot is in the server, and has correct permissions. |  | ||||||
|             </p> |  | ||||||
|             <a class="button is-size-4 is-rounded is-success" href="https://invite.reminder-bot.com"> |  | ||||||
|                 <p class="is-size-4"> |  | ||||||
|                     <span>Add to Server</span> <span class="icon"><i class="fas fa-chevron-right"></i></span> |  | ||||||
|                 </p> |  | ||||||
|             </a> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
							
								
								
									
										251
									
								
								web/templates/reminder_dashboard/guild_reminder.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								web/templates/reminder_dashboard/guild_reminder.html.tera
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,251 @@ | |||||||
|  | <div class="columns reminderContent {% if creating %}creator{% endif %}"> | ||||||
|  |     <div class="column discord-frame"> | ||||||
|  |         <article class="media"> | ||||||
|  |             <figure class="media-left"> | ||||||
|  |                 <p class="image is-32x32 customizable"> | ||||||
|  |                     <a> | ||||||
|  |                         <img class="is-rounded discord-avatar" src="/static/img/bg.webp" alt="Image for discord avatar"> | ||||||
|  |                     </a> | ||||||
|  |                 </p> | ||||||
|  |             </figure> | ||||||
|  |             <div class="media-content"> | ||||||
|  |                 <div class="content"> | ||||||
|  |                     <div class="discord-message-header"> | ||||||
|  |                         <label class="is-sr-only">Username Override</label> | ||||||
|  |                         <input class="discord-username message-input" placeholder="Username Override" | ||||||
|  |                                maxlength="32" name="username"> | ||||||
|  |                     </div> | ||||||
|  |                     <label class="is-sr-only">Message</label> | ||||||
|  |                     <textarea class="message-input autoresize discord-content" | ||||||
|  |                               placeholder="Message Content..." | ||||||
|  |                               maxlength="2000" name="content" rows="1"></textarea> | ||||||
|  |  | ||||||
|  |                     <div class="discord-embed"> | ||||||
|  |                         <div class="embed-body"> | ||||||
|  |                             <button class="change-color button is-rounded is-small"> | ||||||
|  |                                 <span class="is-sr-only">Choose embed color</span><i class="fas fa-eye-dropper"></i> | ||||||
|  |                             </button> | ||||||
|  |                             <div class="a"> | ||||||
|  |                                 <div class="embed-author-box"> | ||||||
|  |                                     <div class="a"> | ||||||
|  |                                         <p class="image is-24x24 customizable"> | ||||||
|  |                                             <a> | ||||||
|  |                                                 <img class="is-rounded embed_author_url" src="/static/img/bg.webp" alt="Image for embed author"> | ||||||
|  |                                             </a> | ||||||
|  |                                         </p> | ||||||
|  |                                     </div> | ||||||
|  |  | ||||||
|  |                                     <div class="b"> | ||||||
|  |                                         <label class="is-sr-only" for="embedAuthor">Embed Author</label> | ||||||
|  |                                         <textarea | ||||||
|  |                                                 class="discord-embed-author message-input autoresize" | ||||||
|  |                                                 placeholder="Embed Author..." rows="1" maxlength="256" | ||||||
|  |                                                 name="embed_author"></textarea> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |  | ||||||
|  |                                 <label class="is-sr-only" for="embedTitle">Embed Title</label> | ||||||
|  |                                 <textarea class="discord-title message-input  autoresize" | ||||||
|  |                                           placeholder="Embed Title..." | ||||||
|  |                                           maxlength="256" rows="1" | ||||||
|  |                                           name="embed_title"></textarea> | ||||||
|  |                                 <br> | ||||||
|  |                                 <label class="is-sr-only" for="embedDescription">Embed Description</label> | ||||||
|  |                                 <textarea class="discord-description message-input autoresize " | ||||||
|  |                                           placeholder="Embed Description..." | ||||||
|  |                                           maxlength="4096" name="embed_description" | ||||||
|  |                                           rows="1"></textarea> | ||||||
|  |                                 <br> | ||||||
|  |  | ||||||
|  |                                 <div class="embed-multifield-box"> | ||||||
|  |                                     <div data-inlined="1" class="embed-field-box"> | ||||||
|  |                                         <label class="is-sr-only" for="embedFieldTitle">Field Title</label> | ||||||
|  |                                         <div class="is-flex"> | ||||||
|  |                                             <textarea class="discord-field-title field-input message-input autoresize" | ||||||
|  |                                                       placeholder="Field Title..." rows="1" | ||||||
|  |                                                       maxlength="256" name="embed_field_title[]"></textarea> | ||||||
|  |                                             <button class="button is-small inline-btn"> | ||||||
|  |                                                 <span class="is-sr-only">Toggle field inline</span><i class="fas fa-arrows-h"></i> | ||||||
|  |                                             </button> | ||||||
|  |                                         </div> | ||||||
|  |  | ||||||
|  |                                         <label class="is-sr-only" for="embedFieldValue">Field Value</label> | ||||||
|  |                                         <textarea | ||||||
|  |                                                 class="discord-field-value field-input message-input autoresize " | ||||||
|  |                                                 placeholder="Field Value..." | ||||||
|  |                                                 maxlength="1024" name="embed_field_value[]" | ||||||
|  |                                                 rows="1"></textarea> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |  | ||||||
|  |                             <div class="b"> | ||||||
|  |                                 <p class="image thumbnail customizable"> | ||||||
|  |                                     <a> | ||||||
|  |                                         <img class="embed_thumbnail_url" src="/static/img/bg.webp" alt="Square thumbnail embedded image"> | ||||||
|  |                                     </a> | ||||||
|  |                                 </p> | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |  | ||||||
|  |                         <p class="image is-400x300 customizable"> | ||||||
|  |                             <a> | ||||||
|  |                                 <img class="embed_image_url" src="/static/img/bg.webp" alt="Large embedded image"> | ||||||
|  |                             </a> | ||||||
|  |                         </p> | ||||||
|  |  | ||||||
|  |                         <div class="embed-footer-box"> | ||||||
|  |                             <p class="image is-20x20 customizable"> | ||||||
|  |                                 <a> | ||||||
|  |                                     <img class="is-rounded embed_footer_url" src="/static/img/bg.webp" alt="Footer profile-like image"> | ||||||
|  |                                 </a> | ||||||
|  |                             </p> | ||||||
|  |  | ||||||
|  |                             <label class="is-sr-only" for="embedFooter">Embed Footer text</label> | ||||||
|  |                             <textarea class="discord-embed-footer message-input autoresize " | ||||||
|  |                                       placeholder="Embed Footer..." | ||||||
|  |                                       maxlength="2048" name="embed_footer" rows="1"></textarea> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </article> | ||||||
|  |     </div> | ||||||
|  |     <div class="column settings"> | ||||||
|  |         <div class="columns is-mobile reminder-topbar"> | ||||||
|  |             <div class="column"> | ||||||
|  |                 <div class="field"> | ||||||
|  |                     <div class="control"> | ||||||
|  |                         <label class="label sr-only">Reminder Name</label> | ||||||
|  |                         <input class="input" type="text" name="name" placeholder="Reminder Name"> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |             <div class="column is-narrow"> | ||||||
|  |                 <button class="button is-rounded hide-box"> | ||||||
|  |                     <span class="is-sr-only">Hide reminder</span><i class="fas fa-chevron-down"></i> | ||||||
|  |                 </button> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="columns"> | ||||||
|  |             <div class="column"> | ||||||
|  |                 <div class="field channel-field"> | ||||||
|  |                     <div class="collapses"> | ||||||
|  |                         <label class="label" for="channelOption">Channel*</label> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="control has-icons-left"> | ||||||
|  |                         <div class="select"> | ||||||
|  |                             <select name="channel" class="channel-selector"> | ||||||
|  |                             </select> | ||||||
|  |                         </div> | ||||||
|  |                         <div class="icon is-small is-left"> | ||||||
|  |                             <i class="fas fa-hashtag"></i> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |             <div class="column"> | ||||||
|  |                 <div class="field"> | ||||||
|  |                     <div class="control"> | ||||||
|  |                         <label class="label collapses"> | ||||||
|  |                             Time* | ||||||
|  |                             <input class="input" type="datetime-local" step="1" name="time"> | ||||||
|  |                         </label> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="collapses"> | ||||||
|  |             <div class="patreon-only"> | ||||||
|  |                 <div class="field"> | ||||||
|  |                     <label class="label">Interval <a class="foreground" href="/help/intervals"><i class="fas fa-question-circle"></i></a></label> | ||||||
|  |                     <div class="control intervalSelector" style="min-width: 400px;" > | ||||||
|  |                         <div class="input interval-group"> | ||||||
|  |                             <div class="interval-group-left"> | ||||||
|  |                                 <label> | ||||||
|  |                                     <span class="is-sr-only">Interval months</span> | ||||||
|  |                                     <input class="w2" type="text" pattern="\d*" name="interval_months" maxlength="2" placeholder=""> <span class="half-rem"></span> months, <span class="half-rem"></span> | ||||||
|  |                                 </label> | ||||||
|  |                                 <label> | ||||||
|  |                                     <span class="is-sr-only">Interval days</span> | ||||||
|  |                                     <input class="w3" type="text" pattern="\d*" name="interval_days" maxlength="4" placeholder=""> <span class="half-rem"></span> days, <span class="half-rem"></span> | ||||||
|  |                                 </label> | ||||||
|  |                                 <label> | ||||||
|  |                                     <span class="is-sr-only">Interval hours</span> | ||||||
|  |                                     <input class="w2" type="text" pattern="\d*" name="interval_hours" maxlength="2" placeholder="HH">: | ||||||
|  |                                 </label> | ||||||
|  |                                 <label> | ||||||
|  |                                     <span class="is-sr-only">Interval minutes</span> | ||||||
|  |                                     <input class="w2" type="text" pattern="\d*" name="interval_minutes" maxlength="2" placeholder="MM">: | ||||||
|  |                                 </label> | ||||||
|  |                                 <label> | ||||||
|  |                                     <span class="is-sr-only">Interval seconds</span> | ||||||
|  |                                     <input class="w2" type="text" pattern="\d*" name="interval_seconds" maxlength="2" placeholder="SS"> | ||||||
|  |                                 </label> | ||||||
|  |                             </div> | ||||||
|  |                             <button class="clear"><span class="is-sr-only">Clear interval</span><span class="icon"><i class="fas fa-trash"></i></span></button> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="field"> | ||||||
|  |                     <div class="control"> | ||||||
|  |                         <label class="label"> | ||||||
|  |                             Expiration | ||||||
|  |                             <input class="input" type="datetime-local" step="1" name="expiration"> | ||||||
|  |                         </label> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <div class="columns"> | ||||||
|  |                 <div class="column has-text-centered"> | ||||||
|  |                     <div class="is-boxed"> | ||||||
|  |                         <label class="label">Enable TTS <input type="checkbox" name="tts"></label> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="column has-text-centered"> | ||||||
|  |                     <div class="file is-small is-boxed"> | ||||||
|  |                         <label class="file-label"> | ||||||
|  |                             <input class="file-input" type="file" name="attachment"> | ||||||
|  |                             <span class="file-cta"> | ||||||
|  |                                 <span class="file-label"> | ||||||
|  |                                     Add Attachment | ||||||
|  |                                 </span> | ||||||
|  |                                 <span class="file-icon"> | ||||||
|  |                                     <i class="fas fa-upload"></i> | ||||||
|  |                                 </span> | ||||||
|  |                             </span> | ||||||
|  |                         </label> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <div> | ||||||
|  |                 <span class="pad-left"></span> | ||||||
|  |                 {% if creating %} | ||||||
|  |                     <button class="button is-success" id="createReminder"> | ||||||
|  |                         <span>Create Reminder</span> <span class="icon"><i class="fas fa-sparkles"></i></span> | ||||||
|  |                     </button> | ||||||
|  |                     <button class="button is-success is-outlined" id="createTemplate"> | ||||||
|  |                         <span>Create Template</span> <span class="icon"><i class="fas fa-file-spreadsheet"></i></span> | ||||||
|  |                     </button> | ||||||
|  |                     <button class="button is-outlined show-modal is-pulled-right" data-modal="chooseTemplateModal"> | ||||||
|  |                         Load Template | ||||||
|  |                     </button> | ||||||
|  |                 {% else %} | ||||||
|  |                     <button class="button is-success save-btn"> | ||||||
|  |                         <span>Save</span> <span class="icon"><i class="fas fa-save"></i></span> | ||||||
|  |                     </button> | ||||||
|  |                     <button class="button is-warning disable-enable"> | ||||||
|  |                     </button> | ||||||
|  |                     <button class="button is-danger delete-reminder"> | ||||||
|  |                         Delete | ||||||
|  |                     </button> | ||||||
|  |                 {% endif %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -2,7 +2,7 @@ | |||||||
|     <strong>Create Reminder</strong> |     <strong>Create Reminder</strong> | ||||||
|     <div id="reminderCreator"> |     <div id="reminderCreator"> | ||||||
|         {% set creating = true %} |         {% set creating = true %} | ||||||
|         {% include "reminder_dashboard/templates/guild_reminder" %} |         {% include "reminder_dashboard/guild_reminder" %} | ||||||
|         {% set creating = false %} |         {% set creating = false %} | ||||||
|     </div> |     </div> | ||||||
|     <br> |     <br> | ||||||
| @@ -46,10 +46,6 @@ | |||||||
|     <div id="guildReminders"> |     <div id="guildReminders"> | ||||||
|  |  | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div id="guildErrors"> |  | ||||||
|  |  | ||||||
|     </div> |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <script src="/static/js/sort.js"></script> | <script src="/static/js/sort.js"></script> | ||||||
|   | |||||||
| @@ -1,5 +0,0 @@ | |||||||
| <div id="reminderLog"> |  | ||||||
|  |  | ||||||
| </div> |  | ||||||
|  |  | ||||||
| <script src="/static/js/reminder_errors.js"></script> |  | ||||||
| @@ -1,269 +0,0 @@ | |||||||
| <div class="reminderContent {% if creating %}creator{% endif %}"> |  | ||||||
|     <div class="columns is-mobile column reminder-topbar"> |  | ||||||
|         {% if not creating %} |  | ||||||
|         <div class="invert-collapses channel-bar"> |  | ||||||
|             #channel |  | ||||||
|         </div> |  | ||||||
|         {% endif %} |  | ||||||
|         <div class="name-bar"> |  | ||||||
|             <div class="field"> |  | ||||||
|                 <div class="control"> |  | ||||||
|                     <label class="label sr-only">Reminder Name</label> |  | ||||||
|                     <input class="input" type="text" name="name" placeholder="Reminder Name" maxlength="100"> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div class="hide-button-bar"> |  | ||||||
|             <button class="button hide-box"> |  | ||||||
|                 <span class="is-sr-only">Hide reminder</span><i class="fas fa-chevron-down"></i> |  | ||||||
|             </button> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="columns reminder-settings"> |  | ||||||
|         <div class="column discord-frame"> |  | ||||||
|             <article class="media"> |  | ||||||
|                 <figure class="media-left"> |  | ||||||
|                     <p class="image is-32x32 customizable"> |  | ||||||
|                         <a> |  | ||||||
|                             <img class="is-rounded avatar" src="/static/img/bg.webp" alt="Image for discord avatar"> |  | ||||||
|                         </a> |  | ||||||
|                     </p> |  | ||||||
|                 </figure> |  | ||||||
|                 <div class="media-content"> |  | ||||||
|                     <div class="content"> |  | ||||||
|                         <div class="discord-message-header"> |  | ||||||
|                             <label class="is-sr-only">Username Override</label> |  | ||||||
|                             <input class="discord-username message-input" placeholder="Username Override" |  | ||||||
|                                    maxlength="32" name="username"> |  | ||||||
|                         </div> |  | ||||||
|                         <label class="is-sr-only">Message</label> |  | ||||||
|                         <textarea class="message-input autoresize discord-content" |  | ||||||
|                                   placeholder="Message Content..." |  | ||||||
|                                   maxlength="2000" name="content" rows="1"></textarea> |  | ||||||
|  |  | ||||||
|                         <div class="discord-embed"> |  | ||||||
|                             <div class="embed-body"> |  | ||||||
|                                 <button class="change-color button is-rounded is-small"> |  | ||||||
|                                     <span class="is-sr-only">Choose embed color</span><i class="fas fa-eye-dropper"></i> |  | ||||||
|                                 </button> |  | ||||||
|                                 <div class="a"> |  | ||||||
|                                     <div class="embed-author-box"> |  | ||||||
|                                         <div class="a"> |  | ||||||
|                                             <p class="image is-24x24 customizable"> |  | ||||||
|                                                 <a> |  | ||||||
|                                                     <img class="is-rounded embed_author_url" src="/static/img/bg.webp" alt="Image for embed author"> |  | ||||||
|                                                 </a> |  | ||||||
|                                             </p> |  | ||||||
|                                         </div> |  | ||||||
|  |  | ||||||
|                                         <div class="b"> |  | ||||||
|                                             <label class="is-sr-only" for="embedAuthor">Embed Author</label> |  | ||||||
|                                             <textarea |  | ||||||
|                                                     class="discord-embed-author message-input autoresize" |  | ||||||
|                                                     placeholder="Embed Author..." rows="1" maxlength="256" |  | ||||||
|                                                     name="embed_author"></textarea> |  | ||||||
|                                         </div> |  | ||||||
|                                     </div> |  | ||||||
|  |  | ||||||
|                                     <label class="is-sr-only" for="embedTitle">Embed Title</label> |  | ||||||
|                                     <textarea class="discord-title message-input  autoresize" |  | ||||||
|                                               placeholder="Embed Title..." |  | ||||||
|                                               maxlength="256" rows="1" |  | ||||||
|                                               name="embed_title"></textarea> |  | ||||||
|                                     <br> |  | ||||||
|                                     <label class="is-sr-only" for="embedDescription">Embed Description</label> |  | ||||||
|                                     <textarea class="discord-description message-input autoresize " |  | ||||||
|                                               placeholder="Embed Description..." |  | ||||||
|                                               maxlength="4096" name="embed_description" |  | ||||||
|                                               rows="1"></textarea> |  | ||||||
|                                     <br> |  | ||||||
|  |  | ||||||
|                                     <div class="embed-multifield-box"> |  | ||||||
|                                         <div data-inlined="1" class="embed-field-box"> |  | ||||||
|                                             <label class="is-sr-only" for="embedFieldTitle">Field Title</label> |  | ||||||
|                                             <div class="is-flex"> |  | ||||||
|                                                 <textarea class="discord-field-title field-input message-input autoresize" |  | ||||||
|                                                           placeholder="Field Title..." rows="1" |  | ||||||
|                                                           maxlength="256" name="embed_field_title[]"></textarea> |  | ||||||
|                                                 <button class="button is-small inline-btn"> |  | ||||||
|                                                     <span class="is-sr-only">Toggle field inline</span><i class="fas fa-arrows-h"></i> |  | ||||||
|                                                 </button> |  | ||||||
|                                             </div> |  | ||||||
|  |  | ||||||
|                                             <label class="is-sr-only" for="embedFieldValue">Field Value</label> |  | ||||||
|                                             <textarea |  | ||||||
|                                                     class="discord-field-value field-input message-input autoresize " |  | ||||||
|                                                     placeholder="Field Value..." |  | ||||||
|                                                     maxlength="1024" name="embed_field_value[]" |  | ||||||
|                                                     rows="1"></textarea> |  | ||||||
|                                         </div> |  | ||||||
|                                     </div> |  | ||||||
|                                 </div> |  | ||||||
|  |  | ||||||
|                                 <div class="b"> |  | ||||||
|                                     <p class="image thumbnail customizable"> |  | ||||||
|                                         <a> |  | ||||||
|                                             <img class="embed_thumbnail_url" src="/static/img/bg.webp" alt="Square thumbnail embedded image"> |  | ||||||
|                                         </a> |  | ||||||
|                                     </p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
|  |  | ||||||
|                             <p class="image is-400x300 customizable"> |  | ||||||
|                                 <a> |  | ||||||
|                                     <img class="embed_image_url" src="/static/img/bg.webp" alt="Large embedded image"> |  | ||||||
|                                 </a> |  | ||||||
|                             </p> |  | ||||||
|  |  | ||||||
|                             <div class="embed-footer-box"> |  | ||||||
|                                 <p class="image is-20x20 customizable"> |  | ||||||
|                                     <a> |  | ||||||
|                                         <img class="is-rounded embed_footer_url" src="/static/img/bg.webp" alt="Footer profile-like image"> |  | ||||||
|                                     </a> |  | ||||||
|                                 </p> |  | ||||||
|  |  | ||||||
|                                 <label class="is-sr-only" for="embedFooter">Embed Footer text</label> |  | ||||||
|                                 <textarea class="discord-embed-footer message-input autoresize " |  | ||||||
|                                           placeholder="Embed Footer..." |  | ||||||
|                                           maxlength="2048" name="embed_footer" rows="1"></textarea> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </article> |  | ||||||
|         </div> |  | ||||||
|         <div class="column settings"> |  | ||||||
|             <div class="field channel-field"> |  | ||||||
|                 <div class="collapses"> |  | ||||||
|                     <label class="label" for="channelOption">Channel*</label> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="control has-icons-left"> |  | ||||||
|                     <div class="select"> |  | ||||||
|                         <select name="channel" class="channel-selector"> |  | ||||||
|                         </select> |  | ||||||
|                     </div> |  | ||||||
|                     <div class="icon is-small is-left"> |  | ||||||
|                         <i class="fas fa-hashtag"></i> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="field"> |  | ||||||
|                 <div class="control"> |  | ||||||
|                     <label class="label collapses"> |  | ||||||
|                         Time* |  | ||||||
|                         <input class="input prefill-now" type="datetime-local" step="1" name="time"> |  | ||||||
|                     </label> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="collapses split-controls"> |  | ||||||
|                 <div> |  | ||||||
|                     <div class="patreon-only"> |  | ||||||
|                         <div class="patreon-invert foreground"> |  | ||||||
|                             Intervals available on <a href="https://patreon.com/jellywx">Patreon</a> or <a href="https://gitea.jellypro.xyz/jude/reminder-bot">self-hosting</a> |  | ||||||
|                         </div> |  | ||||||
|                         <div class="field"> |  | ||||||
|                             <label class="label">Interval <a class="foreground" href="/help/intervals"><i class="fas fa-question-circle"></i></a></label> |  | ||||||
|                             <div class="control intervalSelector"> |  | ||||||
|                                 <div class="input interval-group"> |  | ||||||
|                                     <div class="interval-group-left"> |  | ||||||
|                                         <span class="no-break"> |  | ||||||
|                                             <label> |  | ||||||
|                                                 <span class="is-sr-only">Interval months</span> |  | ||||||
|                                                 <input class="w2" type="text" pattern="\d*" name="interval_months" maxlength="2" placeholder=""> <span class="half-rem"></span> months, <span class="half-rem"></span> |  | ||||||
|                                             </label> |  | ||||||
|                                             <label> |  | ||||||
|                                                 <span class="is-sr-only">Interval days</span> |  | ||||||
|                                                 <input class="w3" type="text" pattern="\d*" name="interval_days" maxlength="4" placeholder=""> <span class="half-rem"></span> days, <span class="half-rem"></span> |  | ||||||
|                                             </label> |  | ||||||
|                                         </span> |  | ||||||
|                                         <span class="no-break"> |  | ||||||
|                                             <label> |  | ||||||
|                                                 <span class="is-sr-only">Interval hours</span> |  | ||||||
|                                                 <input class="w2" type="text" pattern="\d*" name="interval_hours" maxlength="2" placeholder="HH">: |  | ||||||
|                                             </label> |  | ||||||
|                                             <label> |  | ||||||
|                                                 <span class="is-sr-only">Interval minutes</span> |  | ||||||
|                                                 <input class="w2" type="text" pattern="\d*" name="interval_minutes" maxlength="2" placeholder="MM">: |  | ||||||
|                                             </label> |  | ||||||
|                                             <label> |  | ||||||
|                                                 <span class="is-sr-only">Interval seconds</span> |  | ||||||
|                                                 <input class="w2" type="text" pattern="\d*" name="interval_seconds" maxlength="2" placeholder="SS"> |  | ||||||
|                                             </label> |  | ||||||
|                                         </span> |  | ||||||
|                                     </div> |  | ||||||
|                                     <button class="clear"><span class="is-sr-only">Clear interval</span><span class="icon"><i class="fas fa-trash"></i></span></button> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|  |  | ||||||
|                         <div class="field"> |  | ||||||
|                             <div class="control"> |  | ||||||
|                                 <label class="label"> |  | ||||||
|                                     Expiration |  | ||||||
|                                     <input class="input" type="datetime-local" step="1" name="expiration"> |  | ||||||
|                                 </label> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <div class="columns is-mobile tts-row"> |  | ||||||
|                         <div class="column has-text-centered"> |  | ||||||
|                             <div class="is-boxed"> |  | ||||||
|                                 <label class="label">Enable TTS <input type="checkbox" name="tts"></label> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                         <div class="column has-text-centered"> |  | ||||||
|                             <div class="file is-small is-boxed"> |  | ||||||
|                                 <label class="file-label"> |  | ||||||
|                                     <input class="file-input" type="file" name="attachment"> |  | ||||||
|                                     <span class="file-cta"> |  | ||||||
|                                         <span class="file-label"> |  | ||||||
|                                             Add Attachment |  | ||||||
|                                         </span> |  | ||||||
|                                         <span class="file-icon"> |  | ||||||
|                                             <i class="fas fa-upload"></i> |  | ||||||
|                                         </span> |  | ||||||
|                                     </span> |  | ||||||
|                                 </label> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     {% if creating %} |  | ||||||
|         <div class="button-row"> |  | ||||||
|             <div class="button-row-reminder"> |  | ||||||
|                 <button class="button is-success" id="createReminder"> |  | ||||||
|                     <span>Create Reminder</span> <span class="icon"><i class="fas fa-sparkles"></i></span> |  | ||||||
|                 </button> |  | ||||||
|             </div> |  | ||||||
|             <div class="button-row-template"> |  | ||||||
|                 <div> |  | ||||||
|                     <button class="button is-success is-outlined" id="createTemplate"> |  | ||||||
|                         <span>Create Template</span> <span class="icon"><i class="fas fa-file-spreadsheet"></i></span> |  | ||||||
|                     </button> |  | ||||||
|                 </div> |  | ||||||
|                 <div> |  | ||||||
|                     <button class="button is-outlined show-modal is-pulled-right" data-modal="chooseTemplateModal"> |  | ||||||
|                         Load Template |  | ||||||
|                     </button> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     {% else %} |  | ||||||
|         <div class="button-row-edit"> |  | ||||||
|             <button class="button is-success save-btn"> |  | ||||||
|                 <span>Save</span> <span class="icon"><i class="fas fa-save"></i></span> |  | ||||||
|             </button> |  | ||||||
|             <button class="button is-warning disable-enable"> |  | ||||||
|             </button> |  | ||||||
|             <button class="button is-danger delete-reminder"> |  | ||||||
|                 Delete |  | ||||||
|             </button> |  | ||||||
|         </div> |  | ||||||
|     {% endif %} |  | ||||||
| </div> |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| <div class="reminderError" data-case="success"> |  | ||||||
|     <div class="errorHead"> |  | ||||||
|         <div class="errorIcon"> |  | ||||||
|             <span class="icon"> |  | ||||||
|                 <i class="fas fa-trash"></i> |  | ||||||
|                 <i class="fas fa-check"></i> |  | ||||||
|                 <i class="fas fa-exclamation-triangle"></i> |  | ||||||
|             </span> |  | ||||||
|         </div> |  | ||||||
|         <div class="reminderName"> |  | ||||||
|             Reminder |  | ||||||
|         </div> |  | ||||||
|         <div class="reminderMessage"> |  | ||||||
|  |  | ||||||
|         </div> |  | ||||||
|         <div class="reminderTime"> |  | ||||||
|  |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| <div class="hero is-fullheight"> |  | ||||||
|     <div class="hero-body"> |  | ||||||
|         <div class="container has-text-centered"> |  | ||||||
|             <p class="title"> |  | ||||||
|                 You do not have permissions for this server |  | ||||||
|             </p> |  | ||||||
|             <p class="subtitle"> |  | ||||||
|                 Ask an admin to grant you the "Manage Messages" permission. |  | ||||||
|             </p> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| @@ -28,10 +28,7 @@ | |||||||
|             <div class="container"> |             <div class="container"> | ||||||
|                 <p class="title">Create reminders via the dashboard</p> |                 <p class="title">Create reminders via the dashboard</p> | ||||||
|                 <p class="content"> |                 <p class="content"> | ||||||
|                     Reminders can also be created on the dashboard. The dashboard offers more options for configuring |                     Reminders can also be created on the dashboard. | ||||||
|                     reminders, and offers templates for quick recreation of reminders. |  | ||||||
|  |  | ||||||
|                     <a href="/dashboard">Access the dashboard.</a> |  | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
|     <section class="hero is-small"> |     <section class="hero is-small"> | ||||||
|         <div class="hero-body"> |         <div class="hero-body"> | ||||||
|             <div class="container"> |             <div class="container"> | ||||||
|                 <p class="title">Export data</p> |                 <p class="title">Export your data</p> | ||||||
|                 <p class="content"> |                 <p class="content"> | ||||||
|                     You can export data associated with your server from the dashboard. The data will export as a CSV |                     You can export data associated with your server from the dashboard. The data will export as a CSV | ||||||
|                     file. The CSV file can then be edited and imported to bulk edit server data. |                     file. The CSV file can then be edited and imported to bulk edit server data. | ||||||
| @@ -26,7 +26,8 @@ | |||||||
|             <div class="container"> |             <div class="container"> | ||||||
|                 <p class="title">Import data</p> |                 <p class="title">Import data</p> | ||||||
|                 <p class="content"> |                 <p class="content"> | ||||||
|                     You can import previous exports or modified exports. When importing a file, the new data will be added alongside existing data. |                     You can import previous exports or modified exports. When importing a file, <strong>existing data | ||||||
|  |                     will be overwritten</strong>. | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
| @@ -54,7 +55,7 @@ | |||||||
|                         </figure> |                         </figure> | ||||||
|                     </li> |                     </li> | ||||||
|                     <li> |                     <li> | ||||||
|                         Make edits to the spreadsheet. You can add, edit, and remove rows for reminders. Don't remove the top-most (title) row. |                         Make edits to the spreadsheet. You can add, edit, and remove rows for reminders. Don't remove the title row. | ||||||
|                         <figure> |                         <figure> | ||||||
|                             <img src="/static/img/support/iemanager/edit_spreadsheet.png" alt="Editing spreadsheet"> |                             <img src="/static/img/support/iemanager/edit_spreadsheet.png" alt="Editing spreadsheet"> | ||||||
|                         </figure> |                         </figure> | ||||||
| @@ -69,7 +70,7 @@ | |||||||
|                 Other spreadsheet tools can also be used to edit exports, as long as they are properly configured: |                 Other spreadsheet tools can also be used to edit exports, as long as they are properly configured: | ||||||
|                 <ul> |                 <ul> | ||||||
|                     <li> |                     <li> | ||||||
|                         <strong>Google Sheets</strong>: Create a new blank spreadsheet. Select <strong>File > Import > Upload > export.csv</strong>. |                         <strong>Google Sheets</strong>: Create a new blank spreadsheet. Select <strong>File >> Import >> Upload >> export.csv</strong>. | ||||||
|                         Use the following import settings: |                         Use the following import settings: | ||||||
|                         <figure> |                         <figure> | ||||||
|                             <img src="/static/img/support/iemanager/sheets_settings.png" alt="Google sheets import settings"> |                             <img src="/static/img/support/iemanager/sheets_settings.png" alt="Google sheets import settings"> | ||||||
|   | |||||||
| @@ -49,7 +49,7 @@ | |||||||
|                 <p class="content"> |                 <p class="content"> | ||||||
|                     Monthly or yearly intervals are configured the same as fixed intervals. Instead of a fixed time |                     Monthly or yearly intervals are configured the same as fixed intervals. Instead of a fixed time | ||||||
|                     interval, these reminders repeat on a certain day each month or each year. This makes them ideal |                     interval, these reminders repeat on a certain day each month or each year. This makes them ideal | ||||||
|                     for marking calendar events. |                     for marking certain dates. | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
| @@ -61,8 +61,7 @@ | |||||||
|                 <p class="title">Interval expiration</p> |                 <p class="title">Interval expiration</p> | ||||||
|                 <p class="content"> |                 <p class="content"> | ||||||
|                     An expiration time can also be specified, both via commands and dashboard, for repeating reminders. |                     An expiration time can also be specified, both via commands and dashboard, for repeating reminders. | ||||||
|                     This is optional, and if omitted, the reminder will repeat indefinitely. Otherwise, the reminder |                     This is optional, and if omitted, the reminder will repeat indefinitely. | ||||||
|                     will be deleted once the expiration date is reached. |  | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|   | |||||||
| @@ -13,8 +13,8 @@ | |||||||
|     <section class="section"> |     <section class="section"> | ||||||
|         <div class="container"> |         <div class="container"> | ||||||
|             <h2 class="title">Outline</h2> |             <h2 class="title">Outline</h2> | ||||||
|             <p class=""> |             <p class="is-size-5 pl-6"> | ||||||
|                 The Terms of Service apply whenever you use the hosted edition of <strong>Reminder Bot</strong> and the |                 The Terms of Service apply whenever you use <strong>Reminder Bot</strong> and the | ||||||
|                 <strong>JellyWX's Home</strong> Discord server. |                 <strong>JellyWX's Home</strong> Discord server. | ||||||
|                 <br> |                 <br> | ||||||
|                 <br> |                 <br> | ||||||
| @@ -25,7 +25,7 @@ | |||||||
|                 <br> |                 <br> | ||||||
|                 <br> |                 <br> | ||||||
|                 The Terms of Service may be updated. Notice will be provided via the Discord server. You |                 The Terms of Service may be updated. Notice will be provided via the Discord server. You | ||||||
|                 should consider the Terms of Service to be a guide for appropriate behaviour. |                 should consider the Terms of Service to be a strong for appropriate behaviour. | ||||||
|             </p> |             </p> | ||||||
|         </div> |         </div> | ||||||
|     </section> |     </section> | ||||||
| @@ -33,43 +33,32 @@ | |||||||
|     <section class="section"> |     <section class="section"> | ||||||
|         <div class="container"> |         <div class="container"> | ||||||
|             <h2 class="title">Reminder Bot</h2> |             <h2 class="title">Reminder Bot</h2> | ||||||
|             <p> |             <ul class="is-size-5 pl-6"> | ||||||
|                 The Terms of Service <strong>do not</strong> apply to self-hosting users who are using the source code |                 <li>Reasonably disclose potential exploits or bugs to me by email or by Discord private message</li> | ||||||
|                 or pre-packaged Debian files to run their own instance of Reminder Bot. |                 <li>Do not use the bot to harass other Discord users</li> | ||||||
|             </p> |                 <li>Do not use the bot to transmit malware or other illegal content</li> | ||||||
|             <br> |                 <li>Do not use the bot to send more than 15 messages during a 60 second period</li> | ||||||
|             <h3 class="subtitle">Your access to Reminder Bot may be restricted if you:</h3> |  | ||||||
|             <ul class="pl-6" style="list-style: disc"> |  | ||||||
|                 <li>Abuse exploits or bugs in Reminder Bot.</li> |  | ||||||
|                 <li>Use the bot to harass other Discord users.</li> |  | ||||||
|                 <li>Use the bot to transmit malware or other illegal content.</li> |  | ||||||
|                 <li>Use the bot to send more than 15 messages during a 60 second period.</li> |  | ||||||
|                 <li> |                 <li> | ||||||
|                     Attempt to circumvent restrictions imposed by the bot or website, including trying to access |                     Do not attempt to circumvent restrictions imposed by the bot or website, including trying to access | ||||||
|                     data of other users, circumvent Patreon restrictions, or uploading files and creating reminders that |                     data of other users, circumvent Patreon restrictions, or uploading files and creating reminders that | ||||||
|                     are too large for the bot to send or process. |                     are too large for the bot to send or process. Some or all of these actions may be illegal in your | ||||||
|  |                     country | ||||||
|                 </li> |                 </li> | ||||||
|             </ul> |             </ul> | ||||||
|             <br> |  | ||||||
|             <p> |  | ||||||
|                 Some or all of these actions may be illegal in your country. |  | ||||||
|             </p> |  | ||||||
|         </div> |         </div> | ||||||
|     </section> |     </section> | ||||||
|  |  | ||||||
|     <section class="section"> |     <section class="section"> | ||||||
|         <div class="container"> |         <div class="container"> | ||||||
|             <h2 class="title">JellyWX's Home</h2> |             <h2 class="title">JellyWX's Home</h2> | ||||||
|             <h3 class="subtitle">Your access to the JellyWX's Home Discord server may be restricted if you:</h3> |             <ul class="is-size-5 pl-6"> | ||||||
|             <ul class="pl-6" style="list-style: disc"> |                 <li>Do not discuss politics, harass other users, or use language intended to upset other users</li> | ||||||
|                 <li>Discuss politics, harass other users, or use language intended to upset other users.</li> |                 <li>Do not share personal information about yourself or any other user. This includes but is not | ||||||
|                 <li>Abuse any exploits.</li> |  | ||||||
|                 <li>Share personal information about yourself or any other user. This includes but is not |  | ||||||
|                     limited to real names<sup>1</sup>, addresses, phone numbers, country of origin<sup>2</sup>, religion, email address, |                     limited to real names<sup>1</sup>, addresses, phone numbers, country of origin<sup>2</sup>, religion, email address, | ||||||
|                     IP address.</li> |                     IP address.</li> | ||||||
|                 <li>Send malicious links or attachments.</li> |                 <li>Do not send malicious links or attachments</li> | ||||||
|                 <li>Advertise without permission.</li> |                 <li>Do not advertise</li> | ||||||
|                 <li>Send unwarranted direct messages.</li> |                 <li>Do not send unwarranted direct messages</li> | ||||||
|             </ul> |             </ul> | ||||||
|             <p class="small"> |             <p class="small"> | ||||||
|                 <sup>1</sup> Some users may use their real name on their account. In this case, do not assert that |                 <sup>1</sup> Some users may use their real name on their account. In this case, do not assert that | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user