Compare commits
	
		
			27 Commits
		
	
	
		
			jellywx/gu
			...
			4b42966284
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4b42966284 | |||
| 523ab7f03a | |||
| 6e831c8253 | |||
|  | 4416e5d175 | ||
|  | 734a39a001 | ||
|  | 98191d29ee | ||
|  | 1c4c4a8b31 | ||
|  | d496c81003 | ||
|  | 094d210f64 | ||
|  | 314c72e132 | ||
|  | 4e0163f2cb | ||
|  | e5b8c418af | ||
|  | 3ef8584189 | ||
|  | df2ad09c86 | ||
|  | d70fb24eb1 | ||
|  | 3150c7267d | ||
|  | 6e65e4ff3d | ||
|  | 67a4db2e9a | ||
|  | e9bcb1973f | ||
|  | 9b87fd4258 | ||
|  | a49a849917 | ||
|  | aa74a7f9a3 | ||
|  | 08e4c6cb57 | ||
|  | 6e087bd2dd | ||
| e9792e6322 | |||
| 130504b964 | |||
| 2a8117d0c1 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -2,6 +2,4 @@ | |||||||
| .env | .env | ||||||
| /venv | /venv | ||||||
| .cargo | .cargo | ||||||
| assets |  | ||||||
| out.json |  | ||||||
| /.idea | /.idea | ||||||
|   | |||||||
							
								
								
									
										1215
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1215
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										34
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -1,20 +1,22 @@ | |||||||
| [package] | [package] | ||||||
| name = "reminder_rs" | name = "reminder-rs" | ||||||
| version = "1.6.6" | version = "1.6.10" | ||||||
| authors = ["jellywx <judesouthworth@pm.me>"] | authors = ["Jude Southworth <judesouthworth@pm.me>"] | ||||||
| edition = "2018" | edition = "2021" | ||||||
|  | license = "AGPL-3.0 only" | ||||||
|  | description = "Reminder Bot for Discord, now in Rust" | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| poise = "0.3" | poise = "0.4" | ||||||
| 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 = "2.3.0" | lazy-regex = "2.3.0" | ||||||
| regex = "1.6" | regex = "1.6" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| env_logger = "0.9" | env_logger = "0.10" | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| chrono-tz = { version = "0.6", features = ["serde"] } | chrono-tz = { version = "0.8", features = ["serde"] } | ||||||
| lazy_static = "1.4" | lazy_static = "1.4" | ||||||
| num-integer = "0.1" | num-integer = "0.1" | ||||||
| serde = "1.0" | serde = "1.0" | ||||||
| @@ -23,7 +25,7 @@ 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.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]} | sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]} | ||||||
| base64 = "0.13" | base64 = "0.13" | ||||||
|  |  | ||||||
| [dependencies.postman] | [dependencies.postman] | ||||||
| @@ -31,3 +33,19 @@ path = "postman" | |||||||
|  |  | ||||||
| [dependencies.reminder_web] | [dependencies.reminder_web] | ||||||
| path = "web" | path = "web" | ||||||
|  |  | ||||||
|  | [package.metadata.deb] | ||||||
|  | depends = "$auto, python3-dateparser" | ||||||
|  | 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/default.env", "600"], | ||||||
|  |     ["web/static/**/*", "var/www/reminder-rs/static", "755"], | ||||||
|  |     ["web/templates/**/*", "var/www/reminder-rs/templates", "755"], | ||||||
|  | #    ["nginx/reminder-rs", "etc/nginx/sites-available/reminder-rs", "755"] | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [package.metadata.deb.systemd-units] | ||||||
|  | unit-scripts = "systemd" | ||||||
|  | start = false | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								Containerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Containerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | FROM ubuntu:20.04 | ||||||
|  |  | ||||||
|  | ENV RUSTUP_HOME=/usr/local/rustup \ | ||||||
|  |     CARGO_HOME=/usr/local/cargo \ | ||||||
|  |     PATH=/usr/local/cargo/bin:$PATH | ||||||
|  |  | ||||||
|  | RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib cmake pkg-config libssl-dev curl mysql-client-8.0 | ||||||
|  | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly | ||||||
|  | RUN cargo install cargo-deb | ||||||
							
								
								
									
										44
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								README.md
									
									
									
									
									
								
							| @@ -7,25 +7,30 @@ reminders are paid on the hosted version of the bot. Keep reading if you want to | |||||||
|  |  | ||||||
| You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust) | You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust) | ||||||
|  |  | ||||||
| ### Compiling | ### Compiling for local target | ||||||
| Install build requirements:  | 1. Install requirements:  | ||||||
| `sudo apt install gcc gcc-multilib cmake libssl-dev build-essential` | `sudo apt install gcc gcc-multilib cmake libssl-dev build-essential python3-dateparser` | ||||||
|  | 2. Install rustup from https://rustup.rs | ||||||
| Install Rust 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`. | ||||||
| Reminder Bot can then be built by running `cargo build --release` in the top level directory. It is necessary to create a  | 5. Install `sqlx-cli`: `cargo install sqlx-cli`. | ||||||
| folder called 'assets' containing an image file with its name specified in the environment as `WEBHOOK_AVATAR`, of  | 6. Run migrations: `sqlx migrate run`. | ||||||
| dimensions 128x128px to be used as the webhook avatar. | 7. Set environment variables: | ||||||
|  |  | ||||||
| #### Compilation environment variables |  | ||||||
| These environment variables must be provided when compiling the bot |  | ||||||
|    * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`) |    * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`) | ||||||
| * `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size** | 8. Build: `cargo build --release` | ||||||
|  |  | ||||||
| ### Setting up Python | ### Compiling for other target | ||||||
| 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 | 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. | ||||||
|  |  | ||||||
| ### Environment Variables | 1. Install container software: `sudo apt install podman`. | ||||||
|  | 2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders` | ||||||
|  | 3. Install SQLx CLI: `cargo install sqlx-cli` | ||||||
|  | 4. From the source code directory, execute `sqlx migrate run` | ||||||
|  | 5. Build container image: `podman build -t reminder-rs .` | ||||||
|  | 6. Build with podman: `podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=mysql://user@localhost/reminders" reminder-rs cargo deb`  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Configuring | ||||||
| Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory. | Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory. | ||||||
|  |  | ||||||
| __Required Variables__ | __Required Variables__ | ||||||
| @@ -37,10 +42,5 @@ __Other Variables__ | |||||||
| * `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor | * `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor | ||||||
| * `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users | * `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users | ||||||
| * `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to | * `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to | ||||||
| * `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else | * `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else | ||||||
| * `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds  | * `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds  | ||||||
| * `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages |  | ||||||
|  |  | ||||||
| ### Todo List |  | ||||||
|  |  | ||||||
| * Convert aliases to macros |  | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								Rocket.toml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Rocket.toml
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| [default] | [default] | ||||||
| address = "0.0.0.0" | address = "0.0.0.0" | ||||||
| port = 5000 | port = 18920 | ||||||
| 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" | ||||||
|  |  | ||||||
| [rsa_sha256.tls] | [debug.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" | ||||||
|  |  | ||||||
| [ecdsa_nistp256_sha256.tls] | [debug.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" | ||||||
|  |  | ||||||
| [ecdsa_nistp384_sha384.tls] | [debug.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" | ||||||
|  |  | ||||||
| [ed25519.tls] | [debug.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" | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								assets/webhook.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/webhook.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										3
									
								
								build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								build.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | fn main() { | ||||||
|  |     println!("cargo:rerun-if-changed=migrations"); | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								conf/default.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								conf/default.env
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | DATABASE_URL= | ||||||
|  |  | ||||||
|  | DISCORD_TOKEN= | ||||||
|  | PATREON_GUILD_ID= | ||||||
|  | PATREON_ROLE_ID= | ||||||
|  |  | ||||||
|  | LOCAL_TIMEZONE= | ||||||
|  | MIN_INTERVAL= | ||||||
|  | PYTHON_LOCATION=/usr/bin/python3 | ||||||
|  | DONTRUN=web | ||||||
|  | SECRET_KEY= | ||||||
|  |  | ||||||
|  | REMIND_INTERVAL= | ||||||
|  | OAUTH2_DISCORD_CALLBACK= | ||||||
|  | OAUTH2_CLIENT_ID= | ||||||
|  | OAUTH2_CLIENT_SECRET= | ||||||
							
								
								
									
										13
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | #!/bin/bash | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | id -u reminder &>/dev/null || useradd -r -M reminder | ||||||
|  |  | ||||||
|  | if [ ! -f /etc/reminder-rs/config.env ]; then | ||||||
|  |   cp /etc/reminder-rs/default.env /etc/reminder-rs/config.env | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | chown reminder /etc/reminder-rs/config.env | ||||||
|  |  | ||||||
|  | #DEBHELPER# | ||||||
							
								
								
									
										11
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | #!/bin/bash | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | id -u reminder &>/dev/null || userdel reminder | ||||||
|  |  | ||||||
|  | if [ -f /etc/reminder-rs/config.env ]; then | ||||||
|  |   rm /etc/reminder-rs/config.env | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | #DEBHELPER# | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| 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,92 +0,0 @@ | |||||||
| SET foreign_key_checks = 0; |  | ||||||
|  |  | ||||||
| START TRANSACTION; |  | ||||||
|  |  | ||||||
| -- drop existing constraints |  | ||||||
| ALTER TABLE channels DROP FOREIGN KEY `channels_ibfk_1`; |  | ||||||
| ALTER TABLE command_aliases DROP FOREIGN KEY `command_aliases_ibfk_1`; |  | ||||||
| ALTER TABLE events DROP FOREIGN KEY `events_ibfk_1`; |  | ||||||
| ALTER TABLE guild_users DROP FOREIGN KEY `guild_users_ibfk_1`; |  | ||||||
| ALTER TABLE macro DROP FOREIGN KEY `macro_ibfk_1`; |  | ||||||
| ALTER TABLE roles DROP FOREIGN KEY `roles_ibfk_1`; |  | ||||||
| ALTER TABLE todos DROP FOREIGN KEY `todos_ibfk_2`; |  | ||||||
| ALTER TABLE reminder_template DROP FOREIGN KEY `reminder_template_ibfk_1`; |  | ||||||
|  |  | ||||||
| -- update foreign key types |  | ||||||
| ALTER TABLE channels MODIFY `guild_id` BIGINT UNSIGNED; |  | ||||||
| ALTER TABLE command_aliases MODIFY `guild_id` BIGINT UNSIGNED; |  | ||||||
| ALTER TABLE events MODIFY `guild_id` BIGINT UNSIGNED; |  | ||||||
| ALTER TABLE guild_users MODIFY `guild` BIGINT UNSIGNED; |  | ||||||
| ALTER TABLE macro MODIFY `guild_id` BIGINT UNSIGNED; |  | ||||||
| ALTER TABLE roles MODIFY `guild_id` BIGINT UNSIGNED; |  | ||||||
| ALTER TABLE todos MODIFY `guild_id` BIGINT UNSIGNED; |  | ||||||
| ALTER TABLE reminder_template MODIFY `guild_id` BIGINT UNSIGNED; |  | ||||||
|  |  | ||||||
| -- update foreign key values |  | ||||||
| UPDATE channels SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`); |  | ||||||
| UPDATE command_aliases SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`); |  | ||||||
| UPDATE events SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`); |  | ||||||
| UPDATE guild_users SET `guild` = (SELECT `guild` FROM guilds WHERE guilds.`id` = guild_users.`guild`); |  | ||||||
| UPDATE macro SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`); |  | ||||||
| UPDATE roles SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`); |  | ||||||
| UPDATE todos SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`); |  | ||||||
| UPDATE reminder_template SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`); |  | ||||||
|  |  | ||||||
| -- update guilds table |  | ||||||
| ALTER TABLE guilds MODIFY `id` BIGINT UNSIGNED NOT NULL; |  | ||||||
| UPDATE guilds SET `id` = `guild`; |  | ||||||
| ALTER TABLE guilds DROP COLUMN `guild`; |  | ||||||
| ALTER TABLE guilds ADD COLUMN `default_channel` BIGINT UNSIGNED; |  | ||||||
| ALTER TABLE guilds ADD CONSTRAINT `default_channel_fk` |  | ||||||
|     FOREIGN KEY (`default_channel`) |  | ||||||
|         REFERENCES channels(`channel`) |  | ||||||
|         ON DELETE SET NULL |  | ||||||
|         ON UPDATE CASCADE; |  | ||||||
|  |  | ||||||
| -- re-add constraints |  | ||||||
| ALTER TABLE channels ADD CONSTRAINT |  | ||||||
|     FOREIGN KEY (`guild_id`) |  | ||||||
|         REFERENCES guilds(`id`) |  | ||||||
|         ON DELETE CASCADE |  | ||||||
|         ON UPDATE CASCADE; |  | ||||||
|  |  | ||||||
| ALTER TABLE command_aliases ADD CONSTRAINT |  | ||||||
|     FOREIGN KEY (`guild_id`) |  | ||||||
|         REFERENCES guilds(`id`) |  | ||||||
|         ON DELETE CASCADE |  | ||||||
|         ON UPDATE CASCADE; |  | ||||||
|  |  | ||||||
| ALTER TABLE events ADD CONSTRAINT |  | ||||||
|     FOREIGN KEY (`guild_id`) |  | ||||||
|         REFERENCES guilds(`id`) |  | ||||||
|         ON DELETE CASCADE |  | ||||||
|         ON UPDATE CASCADE; |  | ||||||
|  |  | ||||||
| ALTER TABLE guild_users ADD CONSTRAINT |  | ||||||
|     FOREIGN KEY (`guild`) |  | ||||||
|         REFERENCES guilds(`id`) |  | ||||||
|         ON DELETE CASCADE |  | ||||||
|         ON UPDATE CASCADE; |  | ||||||
|  |  | ||||||
| ALTER TABLE macro ADD CONSTRAINT |  | ||||||
|     FOREIGN KEY (`guild_id`) |  | ||||||
|         REFERENCES guilds(`id`) |  | ||||||
|         ON DELETE CASCADE |  | ||||||
|         ON UPDATE CASCADE; |  | ||||||
|  |  | ||||||
| ALTER TABLE roles ADD CONSTRAINT |  | ||||||
|     FOREIGN KEY (`guild_id`) |  | ||||||
|         REFERENCES guilds(`id`) |  | ||||||
|         ON DELETE CASCADE |  | ||||||
|         ON UPDATE CASCADE; |  | ||||||
|  |  | ||||||
| ALTER TABLE todos ADD CONSTRAINT |  | ||||||
|     FOREIGN KEY (`guild_id`) |  | ||||||
|         REFERENCES guilds(`id`) |  | ||||||
|         ON DELETE CASCADE |  | ||||||
|         ON UPDATE CASCADE; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| COMMIT; |  | ||||||
|  |  | ||||||
| SET foreign_key_checks = 1; |  | ||||||
| @@ -1,10 +1,6 @@ | |||||||
| CREATE DATABASE IF NOT EXISTS reminders; |  | ||||||
| 
 |  | ||||||
| SET FOREIGN_KEY_CHECKS=0; | SET FOREIGN_KEY_CHECKS=0; | ||||||
| 
 | 
 | ||||||
| USE reminders; | CREATE TABLE guilds ( | ||||||
| 
 |  | ||||||
| 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, | ||||||
| 
 | 
 | ||||||
| @@ -18,10 +14,10 @@ CREATE TABLE reminders.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 reminders.channels(id) ON DELETE SET NULL |     FOREIGN KEY (default_channel_id) REFERENCES channels(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.channels ( | CREATE TABLE 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, | ||||||
| 
 | 
 | ||||||
| @@ -39,10 +35,10 @@ CREATE TABLE reminders.channels ( | |||||||
|     guild_id INT UNSIGNED, |     guild_id INT UNSIGNED, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.users ( | CREATE TABLE 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, | ||||||
| 
 | 
 | ||||||
| @@ -59,10 +55,10 @@ CREATE TABLE reminders.users ( | |||||||
|     patreon BOOLEAN NOT NULL DEFAULT 0, |     patreon BOOLEAN NOT NULL DEFAULT 0, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (dm_channel) REFERENCES reminders.channels(id) ON DELETE RESTRICT |     FOREIGN KEY (dm_channel) REFERENCES channels(id) ON DELETE RESTRICT | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.roles ( | CREATE TABLE 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, | ||||||
| 
 | 
 | ||||||
| @@ -71,10 +67,10 @@ CREATE TABLE reminders.roles ( | |||||||
|     guild_id INT UNSIGNED NOT NULL, |     guild_id INT UNSIGNED NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.embeds ( | CREATE TABLE 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 '', | ||||||
| @@ -91,7 +87,7 @@ CREATE TABLE reminders.embeds ( | |||||||
|     PRIMARY KEY (id) |     PRIMARY KEY (id) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.embed_fields ( | CREATE TABLE 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 '', | ||||||
| @@ -100,10 +96,10 @@ CREATE TABLE reminders.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 reminders.embeds(id) ON DELETE CASCADE |     FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.messages ( | CREATE TABLE 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 '', | ||||||
| @@ -114,10 +110,10 @@ CREATE TABLE reminders.messages ( | |||||||
|     attachment_name VARCHAR(260), |     attachment_name VARCHAR(260), | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE SET NULL |     FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.reminders ( | CREATE TABLE 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, | ||||||
| 
 | 
 | ||||||
| @@ -140,20 +136,20 @@ CREATE TABLE reminders.reminders ( | |||||||
|     set_by INT UNSIGNED, |     set_by INT UNSIGNED, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (message_id) REFERENCES reminders.messages(id) ON DELETE RESTRICT, |     FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT, | ||||||
|     FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE CASCADE, |     FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (set_by) REFERENCES reminders.users(id) ON DELETE SET NULL |     FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TRIGGER message_cleanup AFTER DELETE ON reminders.reminders | CREATE TRIGGER message_cleanup AFTER DELETE ON reminders | ||||||
| FOR EACH ROW | FOR EACH ROW | ||||||
|     DELETE FROM reminders.messages WHERE id = OLD.message_id; |     DELETE FROM messages WHERE id = OLD.message_id; | ||||||
| 
 | 
 | ||||||
| CREATE TRIGGER embed_cleanup AFTER DELETE ON reminders.messages | CREATE TRIGGER embed_cleanup AFTER DELETE ON messages | ||||||
| FOR EACH ROW | FOR EACH ROW | ||||||
|     DELETE FROM reminders.embeds WHERE id = OLD.embed_id; |     DELETE FROM embeds WHERE id = OLD.embed_id; | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.todos ( | CREATE TABLE 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, | ||||||
| @@ -161,23 +157,23 @@ CREATE TABLE reminders.todos ( | |||||||
|     value VARCHAR(2000) NOT NULL, |     value VARCHAR(2000) NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL, |     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, | ||||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL |     FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.command_restrictions ( | CREATE TABLE 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 reminders.roles(id) ON DELETE CASCADE, |     FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, | ||||||
|     UNIQUE KEY (`role_id`, `command`) |     UNIQUE KEY (`role_id`, `command`) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.timers ( | CREATE TABLE 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, | ||||||
| @@ -186,7 +182,7 @@ CREATE TABLE reminders.timers ( | |||||||
|     PRIMARY KEY (id) |     PRIMARY KEY (id) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.events ( | CREATE TABLE 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(), | ||||||
| 
 | 
 | ||||||
| @@ -198,12 +194,12 @@ CREATE TABLE reminders.events ( | |||||||
|     reminder_id INT UNSIGNED, |     reminder_id INT UNSIGNED, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL, |     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, | ||||||
|     FOREIGN KEY (reminder_id) REFERENCES reminders.reminders(id) ON DELETE SET NULL |     FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.command_aliases ( | CREATE TABLE 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, | ||||||
| @@ -212,22 +208,22 @@ CREATE TABLE reminders.command_aliases ( | |||||||
|     command VARCHAR(2048) NOT NULL, |     command VARCHAR(2048) NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, | ||||||
|     UNIQUE KEY (`guild_id`, `name`) |     UNIQUE KEY (`guild_id`, `name`) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.guild_users ( | CREATE TABLE 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 reminders.guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (user) REFERENCES reminders.users(id) ON DELETE CASCADE, |     FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE, | ||||||
|     UNIQUE KEY (guild, user) |     UNIQUE KEY (guild, user) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE EVENT reminders.event_cleanup | CREATE EVENT 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 reminders.events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY); | DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY); | ||||||
| @@ -1,5 +1,3 @@ | |||||||
| 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,5 +1,3 @@ | |||||||
| 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, | ||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`; | ||||||
|  | ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL; | ||||||
| @@ -1,5 +1,3 @@ | |||||||
| 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
									
								
								migrations/20221210000000_reminder_daily_intervals.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								migrations/20221210000000_reminder_daily_intervals.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | ALTER TABLE reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL; | ||||||
							
								
								
									
										2
									
								
								migrations/20230511125236_reminder_threads.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								migrations/20230511125236_reminder_threads.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | -- Add migration script here | ||||||
|  | ALTER TABLE reminders ADD COLUMN `thread_id` BIGINT DEFAULT NULL; | ||||||
							
								
								
									
										41
									
								
								nginx/reminder-rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								nginx/reminder-rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | server { | ||||||
|  |         server_name www.reminder-bot.com; | ||||||
|  |  | ||||||
|  |         return 301 $scheme://reminder-bot.com$request_uri; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | server { | ||||||
|  |         listen 80; | ||||||
|  |         server_name reminder-bot.com; | ||||||
|  |  | ||||||
|  | 	    return 301 https://reminder-bot.com$request_uri; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | server { | ||||||
|  |         listen 443 ssl; | ||||||
|  |         server_name reminder-bot.com; | ||||||
|  |  | ||||||
|  |         ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem; | ||||||
|  |         ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem; | ||||||
|  |  | ||||||
|  |         access_log /var/log/nginx/access.log; | ||||||
|  |         error_log /var/log/nginx/error.log; | ||||||
|  |  | ||||||
|  |         proxy_buffer_size 128k; | ||||||
|  |         proxy_buffers 4 256k; | ||||||
|  |         proxy_busy_buffers_size 256k; | ||||||
|  |  | ||||||
|  |         location / { | ||||||
|  |                 proxy_pass http://localhost:18920; | ||||||
|  |                 proxy_redirect off; | ||||||
|  |                 proxy_set_header Host $host; | ||||||
|  |                 proxy_set_header X-Real-IP $remote_addr; | ||||||
|  |                 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||||||
|  | 		        proxy_set_header X-Forwarded-Proto $scheme; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         location /static { | ||||||
|  |                 alias /var/www/reminder-rs/static; | ||||||
|  |                 expires 30d; | ||||||
|  |         } | ||||||
|  | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| 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}; | ||||||
| @@ -62,7 +62,8 @@ 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) { | ||||||
|             let dt = NaiveDateTime::from_timestamp(final_time, 0); |             match NaiveDateTime::from_timestamp_opt(final_time, 0) { | ||||||
|  |                 Some(dt) => { | ||||||
|                     let now = Utc::now().naive_utc(); |                     let now = Utc::now().naive_utc(); | ||||||
|  |  | ||||||
|                     let difference = { |                     let difference = { | ||||||
| @@ -74,6 +75,10 @@ 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() | ||||||
|         } |         } | ||||||
| @@ -243,11 +248,12 @@ pub struct Reminder { | |||||||
|     attachment: Option<Vec<u8>>, |     attachment: Option<Vec<u8>>, | ||||||
|     attachment_name: Option<String>, |     attachment_name: Option<String>, | ||||||
|  |  | ||||||
|     utc_time: NaiveDateTime, |     utc_time: DateTime<Utc>, | ||||||
|     timezone: String, |     timezone: String, | ||||||
|     restartable: bool, |     restartable: bool, | ||||||
|     expires: Option<NaiveDateTime>, |     expires: Option<DateTime<Utc>>, | ||||||
|     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>, | ||||||
| @@ -281,6 +287,7 @@ 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, | ||||||
| @@ -330,9 +337,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 WHERE channel = ?", | ||||||
| UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ? |  | ||||||
|             ", |  | ||||||
|             self.channel_id |             self.channel_id | ||||||
|         ) |         ) | ||||||
|         .execute(pool) |         .execute(pool) | ||||||
| @@ -341,55 +346,43 @@ UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ? | |||||||
|  |  | ||||||
|     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() || self.interval_months.is_some() { |         if self.interval_seconds.is_some() || self.interval_months.is_some() { | ||||||
|             let now = Utc::now().naive_local(); |             let now = Utc::now(); | ||||||
|             let mut updated_reminder_time = self.utc_time; |             let mut updated_reminder_time = | ||||||
|  |                 self.utc_time.with_timezone(&self.timezone.parse().unwrap_or(Tz::UTC)); | ||||||
|  |  | ||||||
|  |             while updated_reminder_time < now { | ||||||
|                 if let Some(interval) = self.interval_months { |                 if let Some(interval) = self.interval_months { | ||||||
|                 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!("Could not add months to a reminder"); | ||||||
|                     interval |  | ||||||
|                 ) |  | ||||||
|                 .fetch_one(pool) |  | ||||||
|                 .await |  | ||||||
|                 { |  | ||||||
|                     Ok(row) => match row.new_time { |  | ||||||
|                         Some(datetime) => { |  | ||||||
|                             updated_reminder_time = datetime; |  | ||||||
|                         } |  | ||||||
|                         None => { |  | ||||||
|                             warn!("Could not update interval by months: got NULL"); |  | ||||||
|  |  | ||||||
|                             updated_reminder_time += Duration::days(30); |                             updated_reminder_time | ||||||
|  |                         }); | ||||||
|                 } |                 } | ||||||
|                     }, |  | ||||||
|  |  | ||||||
|                     Err(e) => { |                 if let Some(interval) = self.interval_days { | ||||||
|                         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"); | ||||||
|  |  | ||||||
|                         // naively fallback to adding 30 days |                             updated_reminder_time | ||||||
|                         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 = | ||||||
|                     updated_reminder_time += Duration::seconds(interval as i64); |                         updated_reminder_time + Duration::seconds(interval as i64); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if self.expires.map_or(false, |expires| { |             if self.expires.map_or(false, |expires| updated_reminder_time > expires) { | ||||||
|                 NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires |  | ||||||
|             }) { |  | ||||||
|                 self.force_delete(pool).await; |                 self.force_delete(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) | ||||||
| @@ -402,12 +395,7 @@ UPDATE reminders SET `utc_time` = ? WHERE `id` = ? | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn force_delete(&self, pool: impl Executor<'_, Database = Database> + Copy) { |     async fn force_delete(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||||
|         sqlx::query!( |         sqlx::query!("DELETE FROM reminders WHERE `id` = ?", self.id) | ||||||
|             " |  | ||||||
| DELETE FROM reminders WHERE `id` = ? |  | ||||||
|             ", |  | ||||||
|             self.id |  | ||||||
|         ) |  | ||||||
|             .execute(pool) |             .execute(pool) | ||||||
|             .await |             .await | ||||||
|             .expect(&format!("Could not delete Reminder {}", self.id)); |             .expect(&format!("Could not delete Reminder {}", self.id)); | ||||||
| @@ -504,8 +492,10 @@ DELETE FROM reminders WHERE `id` = ? | |||||||
|                     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); | ||||||
| @@ -548,9 +538,7 @@ DELETE FROM reminders WHERE `id` = ? | |||||||
|                     .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` SET paused = 0, paused_until = NULL WHERE `channel` = ?", | ||||||
| UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ? |  | ||||||
|                 ", |  | ||||||
|                 self.channel_id |                 self.channel_id | ||||||
|             ) |             ) | ||||||
|             .execute(pool) |             .execute(pool) | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ pub async fn macro_name_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<Str | |||||||
| SELECT name | SELECT name | ||||||
| FROM macro | FROM macro | ||||||
| WHERE | WHERE | ||||||
|     guild_id = ? |     guild_id = (SELECT id FROM guilds WHERE guild = ?) | ||||||
|     AND name LIKE CONCAT(?, '%')", |     AND name LIKE CONCAT(?, '%')", | ||||||
|         ctx.guild_id().unwrap().0, |         ctx.guild_id().unwrap().0, | ||||||
|         partial, |         partial, | ||||||
| @@ -37,20 +37,6 @@ WHERE | |||||||
|     .collect() |     .collect() | ||||||
| } | } | ||||||
|  |  | ||||||
| pub 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() }, |  | ||||||
|         ] |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub async fn time_hint_autocomplete( | pub async fn time_hint_autocomplete( | ||||||
|     ctx: Context<'_>, |     ctx: Context<'_>, | ||||||
|     partial: &str, |     partial: &str, | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ pub async fn delete_macro( | |||||||
| ) -> Result<(), Error> { | ) -> Result<(), Error> { | ||||||
|     match sqlx::query!( |     match sqlx::query!( | ||||||
|         " |         " | ||||||
| SELECT id FROM macro WHERE guild_id = ? AND name = ?", | SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?", | ||||||
|         ctx.guild_id().unwrap().0, |         ctx.guild_id().unwrap().0, | ||||||
|         name |         name | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|  |  | ||||||
|     let aliases = sqlx::query_as!( |     let aliases = sqlx::query_as!( | ||||||
|         Alias, |         Alias, | ||||||
|         "SELECT name, command FROM command_aliases WHERE guild_id = ?", |         "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) | ||||||
| @@ -36,7 +36,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|         match parse_text_command(guild_id, alias.name, &alias.command) { |         match parse_text_command(guild_id, alias.name, &alias.command) { | ||||||
|             Some(cmd_macro) => { |             Some(cmd_macro) => { | ||||||
|                 sqlx::query!( |                 sqlx::query!( | ||||||
|                     "INSERT INTO macro (guild_id, name, description, commands) VALUES (?, ?, ?, ?)", |                     "INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)", | ||||||
|                     cmd_macro.guild_id.0, |                     cmd_macro.guild_id.0, | ||||||
|                     cmd_macro.name, |                     cmd_macro.name, | ||||||
|                     cmd_macro.description, |                     cmd_macro.description, | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ pub async fn record_macro( | |||||||
|  |  | ||||||
|     let row = sqlx::query!( |     let row = sqlx::query!( | ||||||
|         " |         " | ||||||
| SELECT 1 as _e FROM macro WHERE guild_id = ? AND name = ?", | SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?", | ||||||
|         guild_id.0, |         guild_id.0, | ||||||
|         name |         name | ||||||
|     ) |     ) | ||||||
| @@ -121,7 +121,7 @@ pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|             let json = serde_json::to_string(&command_macro.commands).unwrap(); |             let json = serde_json::to_string(&command_macro.commands).unwrap(); | ||||||
|  |  | ||||||
|             sqlx::query!( |             sqlx::query!( | ||||||
|                 "INSERT INTO macro (guild_id, name, description, commands) VALUES (?, ?, ?, ?)", |                 "INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)", | ||||||
|                 command_macro.guild_id.0, |                 command_macro.guild_id.0, | ||||||
|                 command_macro.name, |                 command_macro.name, | ||||||
|                 command_macro.description, |                 command_macro.description, | ||||||
|   | |||||||
| @@ -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}; | use crate::{models::command_macro::guild_command_macro, Context, Data, Error, THEME_COLOR}; | ||||||
|  |  | ||||||
| /// Run a recorded macro | /// Run a recorded macro | ||||||
| #[poise::command( | #[poise::command( | ||||||
| @@ -17,7 +17,17 @@ 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) => { | ||||||
|             ctx.defer_response(false).await?; |             Context::Application(ctx) | ||||||
|  |                 .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 { | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ 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 log::warn; | ||||||
| use poise::serenity_prelude::{ChannelId, Mentionable}; |  | ||||||
|  |  | ||||||
| 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}; | ||||||
| @@ -149,51 +148,11 @@ pub async fn unset_allowed_dm(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Set defaults for commands |  | ||||||
| #[poise::command( |  | ||||||
|     slash_command, |  | ||||||
|     identifying_name = "default", |  | ||||||
|     default_member_permissions = "MANAGE_GUILD" |  | ||||||
| )] |  | ||||||
| pub async fn default(_ctx: Context<'_>) -> Result<(), Error> { |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set a default channel for reminders to be sent to |  | ||||||
| #[poise::command( |  | ||||||
|     slash_command, |  | ||||||
|     guild_only = true, |  | ||||||
|     identifying_name = "default_channel", |  | ||||||
|     default_member_permissions = "MANAGE_GUILD" |  | ||||||
| )] |  | ||||||
| pub async fn default_channel( |  | ||||||
|     ctx: Context<'_>, |  | ||||||
|     #[description = "Channel to send reminders to by default"] channel: Option<ChannelId>, |  | ||||||
| ) -> Result<(), Error> { |  | ||||||
|     if let Some(mut guild_data) = ctx.guild_data().await { |  | ||||||
|         guild_data.default_channel = channel.map(|c| c.0); |  | ||||||
|  |  | ||||||
|         guild_data.commit_changes(&ctx.data().database).await?; |  | ||||||
|  |  | ||||||
|         if let Some(channel) = channel { |  | ||||||
|             ctx.send(|r| { |  | ||||||
|                 r.ephemeral(true).content(format!("Default channel set to {}", channel.mention())) |  | ||||||
|             }) |  | ||||||
|             .await?; |  | ||||||
|         } else { |  | ||||||
|             ctx.send(|r| r.ephemeral(true).content("Default channel unset.")).await?; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// View the webhook being used to send reminders to this channel | /// View the webhook being used to send reminders to this channel | ||||||
| #[poise::command( | #[poise::command( | ||||||
|     slash_command, |     slash_command, | ||||||
|     identifying_name = "webhook_url", |     identifying_name = "webhook_url", | ||||||
|     required_permissions = "ADMINISTRATOR", |     required_permissions = "ADMINISTRATOR" | ||||||
|     default_member_permissions = "ADMINISTRATOR" |  | ||||||
| )] | )] | ||||||
| pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> { | pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> { | ||||||
|     match ctx.channel_data().await { |     match ctx.channel_data().await { | ||||||
|   | |||||||
| @@ -1,10 +1,6 @@ | |||||||
| use std::{ | use std::{collections::HashSet, string::ToString}; | ||||||
|     collections::HashSet, |  | ||||||
|     string::ToString, |  | ||||||
|     time::{SystemTime, UNIX_EPOCH}, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| use chrono::NaiveDateTime; | use chrono::{DateTime, NaiveDateTime, Utc}; | ||||||
| use chrono_tz::Tz; | use chrono_tz::Tz; | ||||||
| use num_integer::Integer; | use num_integer::Integer; | ||||||
| use poise::{ | use poise::{ | ||||||
| @@ -15,9 +11,7 @@ use poise::{ | |||||||
| }; | }; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     commands::autocomplete::{ |     commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete}, | ||||||
|         multiline_autocomplete, time_hint_autocomplete, timezone_autocomplete, |  | ||||||
|     }, |  | ||||||
|     component_models::{ |     component_models::{ | ||||||
|         pager::{DelPager, LookPager, Pager}, |         pager::{DelPager, LookPager, Pager}, | ||||||
|         ComponentDataModel, DelSelector, UndoReminder, |         ComponentDataModel, DelSelector, UndoReminder, | ||||||
| @@ -62,8 +56,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 { | ||||||
|                 let dt = NaiveDateTime::from_timestamp(timestamp, 0); |                 match NaiveDateTime::from_timestamp_opt(timestamp, 0) { | ||||||
|  |                     Some(dt) => { | ||||||
|                         channel.paused = true; |                         channel.paused = true; | ||||||
|                         channel.paused_until = Some(dt); |                         channel.paused_until = Some(dt); | ||||||
|  |  | ||||||
| @@ -74,6 +68,15 @@ 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", | ||||||
| @@ -247,7 +250,7 @@ pub async fn look( | |||||||
|                 char_count < EMBED_DESCRIPTION_MAX_LENGTH |                 char_count < EMBED_DESCRIPTION_MAX_LENGTH | ||||||
|             }) |             }) | ||||||
|             .collect::<Vec<String>>() |             .collect::<Vec<String>>() | ||||||
|             .join("\n"); |             .join(""); | ||||||
|  |  | ||||||
|         let pages = reminders |         let pages = reminders | ||||||
|             .iter() |             .iter() | ||||||
| @@ -434,11 +437,8 @@ pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> Cr | |||||||
|     reply |     reply | ||||||
| } | } | ||||||
|  |  | ||||||
| fn time_difference(start_time: NaiveDateTime) -> String { | fn time_difference(start_time: DateTime<Utc>) -> String { | ||||||
|     let unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; |     let delta = (Utc::now() - start_time).num_seconds(); | ||||||
|     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); | ||||||
| @@ -562,20 +562,17 @@ struct ContentModal { | |||||||
|     content: String, |     content: String, | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Create a reminder. Press "+4 more" for other options. | /// Create a reminder with multi-line content. Press "+4 more" for other options. | ||||||
| #[poise::command( | #[poise::command( | ||||||
|     slash_command, |     slash_command, | ||||||
|     identifying_name = "remind", |     identifying_name = "multiline", | ||||||
|     default_member_permissions = "MANAGE_GUILD" |     default_member_permissions = "MANAGE_GUILD" | ||||||
| )] | )] | ||||||
| pub async fn remind( | pub async fn multiline( | ||||||
|     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"] | ||||||
|     #[autocomplete = "time_hint_autocomplete"] |     #[autocomplete = "time_hint_autocomplete"] | ||||||
|     time: String, |     time: String, | ||||||
|     #[description = "The message content to send"] |  | ||||||
|     #[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>, | ||||||
| @@ -588,8 +585,6 @@ pub async fn remind( | |||||||
|     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(); | ||||||
|  |  | ||||||
|     if content.is_empty() { |  | ||||||
|     let data = ContentModal::execute(ctx).await?; |     let data = ContentModal::execute(ctx).await?; | ||||||
|  |  | ||||||
|     create_reminder( |     create_reminder( | ||||||
| @@ -603,19 +598,35 @@ pub async fn remind( | |||||||
|         tz, |         tz, | ||||||
|     ) |     ) | ||||||
|     .await |     .await | ||||||
|     } else { |  | ||||||
|         create_reminder( |  | ||||||
|             Context::Application(ctx), |  | ||||||
|             time, |  | ||||||
|             content, |  | ||||||
|             channels, |  | ||||||
|             interval, |  | ||||||
|             expires, |  | ||||||
|             tts, |  | ||||||
|             tz, |  | ||||||
|         ) |  | ||||||
|         .await |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// 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( | ||||||
| @@ -653,9 +664,7 @@ async fn create_reminder( | |||||||
|                 let list = channels.map(|arg| parse_mention_list(&arg)).unwrap_or_default(); |                 let list = channels.map(|arg| parse_mention_list(&arg)).unwrap_or_default(); | ||||||
|  |  | ||||||
|                 if list.is_empty() { |                 if list.is_empty() { | ||||||
|                     if let Some(channel_id) = ctx.default_channel().await { |                     if ctx.guild_id().is_some() { | ||||||
|                         vec![ReminderScope::Channel(channel_id.0)] |  | ||||||
|                     } else if ctx.guild_id().is_some() { |  | ||||||
|                         vec![ReminderScope::Channel(ctx.channel_id().0)] |                         vec![ReminderScope::Channel(ctx.channel_id().0)] | ||||||
|                     } else { |                     } else { | ||||||
|                         vec![ReminderScope::User(ctx.author().id.0)] |                         vec![ReminderScope::User(ctx.author().id.0)] | ||||||
|   | |||||||
| @@ -47,7 +47,7 @@ pub async fn todo_guild_add( | |||||||
| ) -> Result<(), Error> { | ) -> Result<(), Error> { | ||||||
|     sqlx::query!( |     sqlx::query!( | ||||||
|         "INSERT INTO todos (guild_id, value) |         "INSERT INTO todos (guild_id, value) | ||||||
| VALUES (?, ?)", | VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)", | ||||||
|         ctx.guild_id().unwrap().0, |         ctx.guild_id().unwrap().0, | ||||||
|         task |         task | ||||||
|     ) |     ) | ||||||
| @@ -70,7 +70,9 @@ VALUES (?, ?)", | |||||||
| )] | )] | ||||||
| pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> { | pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> { | ||||||
|     let values = sqlx::query!( |     let values = sqlx::query!( | ||||||
|         "SELECT todos.id, value FROM todos WHERE guild_id = ?", |         "SELECT todos.id, value FROM todos | ||||||
|  | INNER JOIN guilds ON todos.guild_id = guilds.id | ||||||
|  | WHERE guilds.guild = ?", | ||||||
|         ctx.guild_id().unwrap().0, |         ctx.guild_id().unwrap().0, | ||||||
|     ) |     ) | ||||||
|     .fetch_all(&ctx.data().database) |     .fetch_all(&ctx.data().database) | ||||||
| @@ -120,7 +122,7 @@ pub async fn todo_channel_add( | |||||||
|  |  | ||||||
|     sqlx::query!( |     sqlx::query!( | ||||||
|         "INSERT INTO todos (guild_id, channel_id, value) |         "INSERT INTO todos (guild_id, channel_id, value) | ||||||
| VALUES (?, (SELECT id FROM channels WHERE channel = ?), ?)", | VALUES ((SELECT id FROM guilds WHERE guild = ?), (SELECT id FROM channels WHERE channel = ?), ?)", | ||||||
|         ctx.guild_id().unwrap().0, |         ctx.guild_id().unwrap().0, | ||||||
|         ctx.channel_id().0, |         ctx.channel_id().0, | ||||||
|         task |         task | ||||||
| @@ -338,7 +340,18 @@ 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(disp.split_once(' ').unwrap_or(("", "")).1) |                                         .description({ | ||||||
|  |                                             let c = disp.split_once(' ').unwrap_or(("", "")).1; | ||||||
|  |  | ||||||
|  |                                             if c.len() > 100 { | ||||||
|  |                                                 format!( | ||||||
|  |                                                     "{}...", | ||||||
|  |                                                     c.chars().take(97).collect::<String>() | ||||||
|  |                                                 ) | ||||||
|  |                                             } else { | ||||||
|  |                                                 c.to_string() | ||||||
|  |                                             } | ||||||
|  |                                         }) | ||||||
|                                 }); |                                 }); | ||||||
|                             } |                             } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -113,7 +113,7 @@ impl ComponentDataModel { | |||||||
|                         char_count < EMBED_DESCRIPTION_MAX_LENGTH |                         char_count < EMBED_DESCRIPTION_MAX_LENGTH | ||||||
|                     }) |                     }) | ||||||
|                     .collect::<Vec<String>>() |                     .collect::<Vec<String>>() | ||||||
|                     .join("\n"); |                     .join(""); | ||||||
|  |  | ||||||
|                 let mut embed = CreateEmbed::default(); |                 let mut embed = CreateEmbed::default(); | ||||||
|                 embed |                 embed | ||||||
| @@ -222,7 +222,9 @@ WHERE channels.channel = ?", | |||||||
|                         .collect::<Vec<(usize, String)>>() |                         .collect::<Vec<(usize, String)>>() | ||||||
|                     } else { |                     } else { | ||||||
|                         sqlx::query!( |                         sqlx::query!( | ||||||
|                             "SELECT todos.id, value FROM todos WHERE guild_id = ?", |                             "SELECT todos.id, value FROM todos | ||||||
|  | INNER JOIN guilds ON todos.guild_id = guilds.id | ||||||
|  | WHERE guilds.guild = ?", | ||||||
|                             pager.guild_id, |                             pager.guild_id, | ||||||
|                         ) |                         ) | ||||||
|                         .fetch_all(&data.database) |                         .fetch_all(&data.database) | ||||||
|   | |||||||
| @@ -17,17 +17,13 @@ use regex::Regex; | |||||||
|  |  | ||||||
| lazy_static! { | lazy_static! { | ||||||
|     pub static ref DEFAULT_AVATAR: AttachmentType<'static> = ( |     pub static ref DEFAULT_AVATAR: AttachmentType<'static> = ( | ||||||
|         include_bytes!(concat!( |         include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/webhook.jpg")) as &[u8], | ||||||
|             env!("CARGO_MANIFEST_DIR"), |         "webhook.jpg", | ||||||
|             "/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("SUBSCRIPTION_ROLES") |         env::var("PATREON_ROLE_ID") | ||||||
|             .map(|var| var |             .map(|var| var | ||||||
|                 .split(',') |                 .split(',') | ||||||
|                 .filter_map(|item| { item.parse::<u64>().ok() }) |                 .filter_map(|item| { item.parse::<u64>().ok() }) | ||||||
| @@ -35,7 +31,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("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten(); |         env::var("PATREON_GUILD_ID").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") | ||||||
| @@ -48,5 +44,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(|_| "venv/bin/python3".to_string()); |         env::var("PYTHON_LOCATION").unwrap_or_else(|_| "/usr/bin/python3".to_string()); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,9 +6,7 @@ use poise::{ | |||||||
|     serenity_prelude::{model::application::interaction::Interaction, utils::shard_id}, |     serenity_prelude::{model::application::interaction::Interaction, utils::shard_id}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{component_models::ComponentDataModel, Data, Error, THEME_COLOR}; | ||||||
|     component_models::ComponentDataModel, models::guild_data::GuildData, Data, Error, THEME_COLOR, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| pub async fn listener( | pub async fn listener( | ||||||
|     ctx: &serenity::Context, |     ctx: &serenity::Context, | ||||||
| @@ -29,7 +27,7 @@ pub async fn listener( | |||||||
|             if *is_new { |             if *is_new { | ||||||
|                 let guild_id = guild.id.as_u64().to_owned(); |                 let guild_id = guild.id.as_u64().to_owned(); | ||||||
|  |  | ||||||
|                 sqlx::query!("INSERT IGNORE INTO guilds (id) VALUES (?)", guild_id) |                 sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id) | ||||||
|                     .execute(&data.database) |                     .execute(&data.database) | ||||||
|                     .await?; |                     .await?; | ||||||
|  |  | ||||||
| @@ -63,28 +61,16 @@ To stay up to date on the latest features and fixes, join our [Discord](https:// | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         poise::Event::GuildDelete { incomplete, .. } => { |         poise::Event::GuildDelete { incomplete, .. } => { | ||||||
|             let _ = sqlx::query!("DELETE FROM guilds WHERE id = ?", incomplete.id.0) |             let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.0) | ||||||
|                 .execute(&data.database) |                 .execute(&data.database) | ||||||
|                 .await; |                 .await; | ||||||
|         } |         } | ||||||
|         poise::Event::InteractionCreate { interaction } => { |         poise::Event::InteractionCreate { interaction } => { | ||||||
|             match interaction { |             if let Interaction::MessageComponent(component) = interaction { | ||||||
|                 Interaction::ApplicationCommand(app_command) => { |                 let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id); | ||||||
|                     if let Some(guild_id) = app_command.guild_id { |  | ||||||
|                         // check database guild exists |  | ||||||
|                         GuildData::from_guild(guild_id, &data.database).await?; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 Interaction::MessageComponent(component) => { |  | ||||||
|                     let component_model = |  | ||||||
|                         ComponentDataModel::from_custom_id(&component.data.custom_id); |  | ||||||
|  |  | ||||||
|                 component_model.act(ctx, data, component).await; |                 component_model.act(ctx, data, component).await; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|                 _ => {} |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|         _ => {} |         _ => {} | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								src/hooks.rs
									
									
									
									
									
								
							| @@ -53,19 +53,22 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool { | |||||||
|             .member_permissions(&ctx.discord(), user_id) |             .member_permissions(&ctx.discord(), user_id) | ||||||
|             .await |             .await | ||||||
|             .map_or(false, |p| p.manage_webhooks()); |             .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_cached(&ctx.discord()) |             .to_channel(&ctx.discord()) | ||||||
|  |             .await | ||||||
|  |             .ok() | ||||||
|             .and_then(|c| { |             .and_then(|c| { | ||||||
|                 if let Channel::Guild(channel) = c { |                 if let Channel::Guild(channel) = c { | ||||||
|                     channel.permissions_for_user(&ctx.discord(), user_id).ok() |                     let perms = channel.permissions_for_user(&ctx.discord(), user_id).ok()?; | ||||||
|  |  | ||||||
|  |                     Some((perms.view_channel(), perms.send_messages(), perms.embed_links())) | ||||||
|                 } else { |                 } else { | ||||||
|                     None |                     None | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
|             .map_or((false, false, false), |p| { |             .unwrap_or((false, false, false)); | ||||||
|                 (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 | ||||||
| @@ -81,8 +84,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 manage_webhooks { "✅" } else { "❌" }, |  | ||||||
|                         if embed_links { "✅" } else { "❌" }, |                         if embed_links { "✅" } else { "❌" }, | ||||||
|  |                         if manage_webhooks { "✅" } else { "❌" }, | ||||||
|                     )) |                     )) | ||||||
|                 }) |                 }) | ||||||
|                 .await; |                 .await; | ||||||
|   | |||||||
| @@ -110,13 +110,14 @@ 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), |     current: (u64, u64, u64, u64), | ||||||
| } | } | ||||||
|  |  | ||||||
| impl<'a> Parser<'a> { | impl<'a> Parser<'a> { | ||||||
| @@ -140,17 +141,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 sec, nsec) = match &self.src[start..end] { |         let (mut month, mut day, mut sec, nsec) = match &self.src[start..end] { | ||||||
|             "nanos" | "nsec" | "ns" => (0u64, 0u64, n), |             "nanos" | "nsec" | "ns" => (0, 0u64, 0u64, n), | ||||||
|             "usec" | "us" => (0, 0u64, n.mul(1000)?), |             "usec" | "us" => (0, 0, 0u64, n.mul(1000)?), | ||||||
|             "millis" | "msec" | "ms" => (0, 0u64, n.mul(1_000_000)?), |             "millis" | "msec" | "ms" => (0, 0, 0u64, n.mul(1_000_000)?), | ||||||
|             "seconds" | "second" | "secs" | "sec" | "s" => (0, n, 0), |             "seconds" | "second" | "secs" | "sec" | "s" => (0, 0, n, 0), | ||||||
|             "minutes" | "minute" | "min" | "mins" | "m" => (0, n.mul(60)?, 0), |             "minutes" | "minute" | "min" | "mins" | "m" => (0, 0, n.mul(60)?, 0), | ||||||
|             "hours" | "hour" | "hr" | "hrs" | "h" => (0, n.mul(3600)?, 0), |             "hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0), | ||||||
|             "days" | "day" | "d" => (0, n.mul(86400)?, 0), |             "days" | "day" | "d" => (0, n, 0, 0), | ||||||
|             "weeks" | "week" | "w" => (0, n.mul(86400 * 7)?, 0), |             "weeks" | "week" | "w" => (0, n.mul(7)?, 0, 0), | ||||||
|             "months" | "month" | "M" => (n, 0, 0), |             "months" | "month" | "M" => (n, 0, 0, 0), | ||||||
|             "years" | "year" | "y" => (12, 0, 0), |             "years" | "year" | "y" => (n.mul(12)?, 0, 0, 0), | ||||||
|             _ => { |             _ => { | ||||||
|                 return Err(Error::UnknownUnit { |                 return Err(Error::UnknownUnit { | ||||||
|                     start, |                     start, | ||||||
| @@ -160,15 +161,16 @@ impl<'a> Parser<'a> { | |||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|         let mut nsec = self.current.2 + nsec; |         let mut nsec = self.current.3 + 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.1; |         sec += self.current.2; | ||||||
|  |         day += self.current.1; | ||||||
|         month += self.current.0; |         month += self.current.0; | ||||||
|  |  | ||||||
|         self.current = (month, sec, nsec); |         self.current = (month, day, sec, nsec); | ||||||
|  |  | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| @@ -215,7 +217,13 @@ 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 => return Ok(Interval { month: self.current.0, sec: self.current.1 }), |                 None => { | ||||||
|  |                     return Ok(Interval { | ||||||
|  |                         month: self.current.0, | ||||||
|  |                         day: self.current.1, | ||||||
|  |                         sec: self.current.2, | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -247,5 +255,73 @@ 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) }.parse() |     Parser { iter: s.chars(), src: s, current: (0, 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); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								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] | #[tokio::main(flavor = "multi_thread")] | ||||||
| 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,7 +88,11 @@ 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(); | ||||||
|  |  | ||||||
|     dotenv()?; |     if Path::new("/etc/reminder-rs/config.env").exists() { | ||||||
|  |         dotenv::from_path("/etc/reminder-rs/config.env")?; | ||||||
|  |     } else { | ||||||
|  |         dotenv::from_path(".env")?; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     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"); | ||||||
|  |  | ||||||
| @@ -120,10 +124,6 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | |||||||
|                 ], |                 ], | ||||||
|                 ..command_macro::macro_base() |                 ..command_macro::macro_base() | ||||||
|             }, |             }, | ||||||
|             poise::Command { |  | ||||||
|                 subcommands: vec![moderation_cmds::default_channel()], |  | ||||||
|                 ..moderation_cmds::default() |  | ||||||
|             }, |  | ||||||
|             reminder_cmds::pause(), |             reminder_cmds::pause(), | ||||||
|             reminder_cmds::offset(), |             reminder_cmds::offset(), | ||||||
|             reminder_cmds::nudge(), |             reminder_cmds::nudge(), | ||||||
| @@ -137,6 +137,7 @@ 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![ | ||||||
| @@ -171,6 +172,8 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> { | |||||||
|     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 IFNULL(timezone, 'UTC') AS timezone | ||||||
|         FROM users |         FROM users | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u | |||||||
|  |  | ||||||
|             sqlx::query!( |             sqlx::query!( | ||||||
|                 " |                 " | ||||||
| INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, ?) | INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?)) | ||||||
|                 ", |                 ", | ||||||
|                 channel_id, |                 channel_id, | ||||||
|                 channel_name, |                 channel_name, | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ pub async fn guild_command_macro( | |||||||
| ) -> Option<CommandMacro<Data, Error>> { | ) -> Option<CommandMacro<Data, Error>> { | ||||||
|     let row = sqlx::query!( |     let row = sqlx::query!( | ||||||
|         " |         " | ||||||
| SELECT * FROM macro WHERE guild_id = ? AND name = ? | SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ? | ||||||
|         ", |         ", | ||||||
|         ctx.guild_id().unwrap().0, |         ctx.guild_id().unwrap().0, | ||||||
|         name |         name | ||||||
|   | |||||||
| @@ -1,52 +0,0 @@ | |||||||
| use sqlx::MySqlPool; |  | ||||||
|  |  | ||||||
| use crate::GuildId; |  | ||||||
|  |  | ||||||
| pub struct GuildData { |  | ||||||
|     pub id: u64, |  | ||||||
|     pub default_channel: Option<u64>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl GuildData { |  | ||||||
|     pub async fn from_guild(guild: GuildId, pool: &MySqlPool) -> Result<Self, sqlx::Error> { |  | ||||||
|         let guild_id = guild.0; |  | ||||||
|  |  | ||||||
|         if let Ok(row) = sqlx::query_as_unchecked!( |  | ||||||
|             Self, |  | ||||||
|             " |  | ||||||
| SELECT id, default_channel FROM guilds WHERE id = ? |  | ||||||
|             ", |  | ||||||
|             guild_id |  | ||||||
|         ) |  | ||||||
|         .fetch_one(pool) |  | ||||||
|         .await |  | ||||||
|         { |  | ||||||
|             Ok(row) |  | ||||||
|         } else { |  | ||||||
|             sqlx::query!( |  | ||||||
|                 " |  | ||||||
| INSERT IGNORE INTO guilds (id) VALUES (?) |  | ||||||
|                 ", |  | ||||||
|                 guild_id |  | ||||||
|             ) |  | ||||||
|             .execute(&pool.clone()) |  | ||||||
|             .await?; |  | ||||||
|  |  | ||||||
|             Ok(Self { id: guild_id, default_channel: None }) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub async fn commit_changes(&self, pool: &MySqlPool) -> Result<(), sqlx::Error> { |  | ||||||
|         sqlx::query!( |  | ||||||
|             " |  | ||||||
| UPDATE guilds SET default_channel = ? WHERE id = ? |  | ||||||
|             ", |  | ||||||
|             self.default_channel, |  | ||||||
|             self.id |  | ||||||
|         ) |  | ||||||
|         .execute(pool) |  | ||||||
|         .await?; |  | ||||||
|  |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,28 +1,28 @@ | |||||||
| 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 log::warn; | use poise::serenity_prelude::{async_trait, model::id::UserId, ChannelType}; | ||||||
| use poise::serenity_prelude::{async_trait, model::id::UserId, ChannelId}; |  | ||||||
|  |  | ||||||
| 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, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #[async_trait] | #[async_trait] | ||||||
| pub trait CtxData { | pub trait CtxData { | ||||||
|     async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>; |     async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>; | ||||||
|  |  | ||||||
|     async fn author_data(&self) -> Result<UserData, Error>; |     async fn author_data(&self) -> Result<UserData, 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>; | ||||||
|     async fn guild_data(&self) -> Option<GuildData>; |  | ||||||
|     async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>; |     async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>; | ||||||
|     async fn default_channel(&self) -> Option<ChannelId>; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[async_trait] | #[async_trait] | ||||||
| @@ -43,7 +43,20 @@ 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>> { | ||||||
|         let channel = self.channel_id().to_channel_cached(&self.discord()).unwrap(); |         // If we're in a thread, get the parent channel. | ||||||
|  |         let recv_channel = self.channel_id().to_channel(&self.discord()).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.discord()).unwrap() | ||||||
|  |                 } else { | ||||||
|  |                     self.channel_id().to_channel_cached(&self.discord()).unwrap() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             None => self.channel_id().to_channel_cached(&self.discord()).unwrap(), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|         ChannelData::from_channel(&channel, &self.data().database).await |         ChannelData::from_channel(&channel, &self.data().database).await | ||||||
|     } |     } | ||||||
| @@ -51,55 +64,24 @@ impl CtxData for Context<'_> { | |||||||
|     async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> { |     async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> { | ||||||
|         self.data().command_macros(self.guild_id().unwrap()).await |         self.data().command_macros(self.guild_id().unwrap()).await | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn default_channel(&self) -> Option<ChannelId> { |  | ||||||
|         match self.guild_id() { |  | ||||||
|             Some(guild_id) => { |  | ||||||
|                 let guild_data = GuildData::from_guild(guild_id, &self.data().database).await; |  | ||||||
|  |  | ||||||
|                 match guild_data { |  | ||||||
|                     Ok(data) => data.default_channel.map(|c| ChannelId(c)), |  | ||||||
|  |  | ||||||
|                     Err(e) => { |  | ||||||
|                         warn!("SQL error: {:?}", e); |  | ||||||
|  |  | ||||||
|                         None |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             None => None, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn guild_data(&self) -> Option<GuildData> { |  | ||||||
|         match self.guild_id() { |  | ||||||
|             Some(guild_id) => GuildData::from_guild(guild_id, &self.data().database).await.ok(), |  | ||||||
|  |  | ||||||
|             None => None, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Data { | impl Data { | ||||||
|     pub async fn command_macros( |     pub(crate) async fn command_macros( | ||||||
|         &self, |         &self, | ||||||
|         guild_id: GuildId, |         guild_id: GuildId, | ||||||
|     ) -> Result<Vec<CommandMacro<Data, Error>>, Error> { |     ) -> Result<Vec<CommandMacro<Data, Error>>, Error> { | ||||||
|         let rows = sqlx::query!( |         let rows = sqlx::query!( | ||||||
|             "SELECT name, description, commands FROM macro WHERE guild_id = ?", |             "SELECT name, description, commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||||
|             guild_id.0 |             guild_id.0 | ||||||
|         ) |         ) | ||||||
|         .fetch_all(&self.database) |         .fetch_all(&self.database) | ||||||
|         .await? |         .await?.iter().map(|row| CommandMacro { | ||||||
|         .iter() |  | ||||||
|         .map(|row| CommandMacro { |  | ||||||
|             guild_id, |             guild_id, | ||||||
|             name: row.name.clone(), |             name: row.name.clone(), | ||||||
|             description: row.description.clone(), |             description: row.description.clone(), | ||||||
|             commands: serde_json::from_str(&row.commands).unwrap(), |             commands: serde_json::from_str(&row.commands).unwrap(), | ||||||
|         }) |         }).collect(); | ||||||
|         .collect(); |  | ||||||
|  |  | ||||||
|         Ok(rows) |         Ok(rows) | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ use poise::serenity_prelude::{ | |||||||
|         id::{ChannelId, GuildId, UserId}, |         id::{ChannelId, GuildId, UserId}, | ||||||
|         webhook::Webhook, |         webhook::Webhook, | ||||||
|     }, |     }, | ||||||
|     Result as SerenityResult, |     ChannelType, Result as SerenityResult, | ||||||
| }; | }; | ||||||
| use sqlx::MySqlPool; | use sqlx::MySqlPool; | ||||||
|  |  | ||||||
| @@ -51,9 +51,11 @@ pub struct ReminderBuilder { | |||||||
|     pool: MySqlPool, |     pool: MySqlPool, | ||||||
|     uid: String, |     uid: String, | ||||||
|     channel: u32, |     channel: u32, | ||||||
|  |     thread_id: Option<u64>, | ||||||
|     utc_time: NaiveDateTime, |     utc_time: NaiveDateTime, | ||||||
|     timezone: String, |     timezone: String, | ||||||
|     interval_secs: Option<i64>, |     interval_seconds: 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,6 +89,7 @@ INSERT INTO reminders ( | |||||||
|     `utc_time`, |     `utc_time`, | ||||||
|     `timezone`, |     `timezone`, | ||||||
|     `interval_seconds`, |     `interval_seconds`, | ||||||
|  |     `interval_days`, | ||||||
|     `interval_months`, |     `interval_months`, | ||||||
|     `expires`, |     `expires`, | ||||||
|     `content`, |     `content`, | ||||||
| @@ -106,6 +109,7 @@ INSERT INTO reminders ( | |||||||
|     ?, |     ?, | ||||||
|     ?, |     ?, | ||||||
|     ?, |     ?, | ||||||
|  |     ?, | ||||||
|     ? |     ? | ||||||
| ) | ) | ||||||
|             ", |             ", | ||||||
| @@ -113,7 +117,8 @@ INSERT INTO reminders ( | |||||||
|                         self.channel, |                         self.channel, | ||||||
|                         utc_time, |                         utc_time, | ||||||
|                         self.timezone, |                         self.timezone, | ||||||
|                         self.interval_secs, |                         self.interval_seconds, | ||||||
|  |                         self.interval_days, | ||||||
|                         self.interval_months, |                         self.interval_months, | ||||||
|                         self.expires, |                         self.expires, | ||||||
|                         self.content, |                         self.content, | ||||||
| @@ -175,17 +180,15 @@ 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 { | ||||||
|         self.utc_time = NaiveDateTime::from_timestamp(time.into(), 0); |         if let Some(utc_time) = NaiveDateTime::from_timestamp_opt(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 { | ||||||
|         if let Some(t) = time { |         self.expires = time.map(|t| NaiveDateTime::from_timestamp_opt(t.into(), 0)).flatten(); | ||||||
|             self.expires = Some(NaiveDateTime::from_timestamp(t.into(), 0)); |  | ||||||
|         } else { |  | ||||||
|             self.expires = None; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         self |         self | ||||||
|     } |     } | ||||||
| @@ -212,13 +215,19 @@ impl<'a> MultiReminderBuilder<'a> { | |||||||
|  |  | ||||||
|         let mut ok_locs = HashSet::new(); |         let mut ok_locs = HashSet::new(); | ||||||
|  |  | ||||||
|         if self.interval.map_or(false, |i| ((i.sec + i.month * 30 * DAY) as i64) < *MIN_INTERVAL) { |         if self | ||||||
|  |             .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.interval.map_or(false, |i| ((i.sec + i.month * 30 * DAY) as i64) > *MAX_TIME) |         } else if self | ||||||
|  |             .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.discord()).await { |                         if let Ok(user) = UserId(user_id).to_user(&self.ctx.discord()).await { | ||||||
| @@ -251,14 +260,29 @@ impl<'a> MultiReminderBuilder<'a> { | |||||||
|                         let channel = |                         let channel = | ||||||
|                             ChannelId(channel_id).to_channel(&self.ctx.discord()).await.unwrap(); |                             ChannelId(channel_id).to_channel(&self.ctx.discord()).await.unwrap(); | ||||||
|  |  | ||||||
|                         if let Some(guild_channel) = channel.clone().guild() { |                         if let Some(mut 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 = |                                 let mut channel_data = if guild_channel.kind | ||||||
|                                     ChannelData::from_channel(&channel, &self.ctx.data().database) |                                     == ChannelType::PublicThread | ||||||
|  |                                 { | ||||||
|  |                                     // fixme jesus christ | ||||||
|  |                                     let parent = guild_channel | ||||||
|  |                                         .parent_id | ||||||
|  |                                         .unwrap() | ||||||
|  |                                         .to_channel(&self.ctx.discord()) | ||||||
|                                         .await |                                         .await | ||||||
|                                         .unwrap(); |                                         .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) | ||||||
|  |                                         .await | ||||||
|  |                                         .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() | ||||||
| @@ -300,9 +324,11 @@ impl<'a> MultiReminderBuilder<'a> { | |||||||
|                             pool: self.ctx.data().database.clone(), |                             pool: self.ctx.data().database.clone(), | ||||||
|                             uid: generate_uid(), |                             uid: generate_uid(), | ||||||
|                             channel: c, |                             channel: c, | ||||||
|  |                             thread_id, | ||||||
|                             utc_time: self.utc_time, |                             utc_time: self.utc_time, | ||||||
|                             timezone: self.timezone.to_string(), |                             timezone: self.timezone.to_string(), | ||||||
|                             interval_secs: self.interval.map(|i| i.sec as i64), |                             interval_seconds: 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::{NaiveDateTime, TimeZone}; | use chrono::{DateTime, NaiveDateTime, Utc}; | ||||||
| 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,8 +24,9 @@ pub struct Reminder { | |||||||
|     pub id: u32, |     pub id: u32, | ||||||
|     pub uid: String, |     pub uid: String, | ||||||
|     pub channel: u64, |     pub channel: u64, | ||||||
|     pub utc_time: NaiveDateTime, |     pub utc_time: DateTime<Utc>, | ||||||
|     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, | ||||||
| @@ -59,6 +60,7 @@ 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, | ||||||
| @@ -95,6 +97,7 @@ 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, | ||||||
| @@ -138,6 +141,7 @@ 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, | ||||||
| @@ -195,6 +199,7 @@ 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, | ||||||
| @@ -228,6 +233,7 @@ 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, | ||||||
| @@ -245,7 +251,7 @@ LEFT JOIN | |||||||
| ON | ON | ||||||
|     reminders.set_by = users.id |     reminders.set_by = users.id | ||||||
| WHERE | WHERE | ||||||
|     channels.guild_id = ? |     channels.guild_id = (SELECT id FROM guilds WHERE guild = ?) | ||||||
|                 ", |                 ", | ||||||
|                     guild_id.as_u64() |                     guild_id.as_u64() | ||||||
|                 ) |                 ) | ||||||
| @@ -262,6 +268,7 @@ 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, | ||||||
| @@ -310,30 +317,32 @@ WHERE | |||||||
|             count + 1, |             count + 1, | ||||||
|             self.display_content(), |             self.display_content(), | ||||||
|             self.channel, |             self.channel, | ||||||
|             timezone.timestamp(self.utc_time.timestamp(), 0).format("%Y-%m-%d %H:%M:%S") |             self.utc_time.with_timezone(timezone).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 => timezone |             TimeDisplayType::Absolute => { | ||||||
|                 .timestamp(self.utc_time.timestamp(), 0) |                 self.utc_time.with_timezone(timezone).format("%Y-%m-%d %H:%M:%S").to_string() | ||||||
|                 .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() || self.interval_months.is_some() { |         if self.interval_seconds.is_some() | ||||||
|  |             || self.interval_days.is_some() | ||||||
|  |             || self.interval_months.is_some() | ||||||
|  |         { | ||||||
|             format!( |             format!( | ||||||
|                 "'{}' *occurs next at* **{}**, repeating (set by {})", |                 "'{}' *occurs next at* **{}**, repeating (set by {})\n", | ||||||
|                 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 {})", |                 "'{}' *occurs next at* **{}** (set by {})\n", | ||||||
|                 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::NaiveDateTime; | use chrono::{DateTime, Utc}; | ||||||
| use sqlx::MySqlPool; | use sqlx::MySqlPool; | ||||||
|  |  | ||||||
| pub struct Timer { | pub struct Timer { | ||||||
|     pub name: String, |     pub name: String, | ||||||
|     pub start_time: NaiveDateTime, |     pub start_time: DateTime<Utc>, | ||||||
|     pub owner: u64, |     pub owner: u64, | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								systemd/reminder-rs.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								systemd/reminder-rs.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | [Unit] | ||||||
|  | Description=Reminder Bot | ||||||
|  |  | ||||||
|  | [Service] | ||||||
|  | User=reminder | ||||||
|  | Type=simple | ||||||
|  | ExecStart=/usr/bin/reminder-rs | ||||||
|  | Restart=always | ||||||
|  | RestartSec=4 | ||||||
|  | # Environment="RUST_LOG=warn,reminder_rs=info,postman=info" | ||||||
|  |  | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
| @@ -31,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("SUBSCRIPTION_ROLES") |         env::var("PATREON_ROLE_ID") | ||||||
|             .map(|var| var |             .map(|var| var | ||||||
|                 .split(',') |                 .split(',') | ||||||
|                 .filter_map(|item| { item.parse::<u64>().ok() }) |                 .filter_map(|item| { item.parse::<u64>().ok() }) | ||||||
| @@ -39,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("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten(); |         env::var("PATREON_GUILD_ID").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()) | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ pub async fn initialize( | |||||||
|     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' not supplied"); |     env::var("PATREON_GUILD_ID").expect("`PATREON_GUILD_ID' not supplied"); | ||||||
|     info!("Done!"); |     info!("Done!"); | ||||||
|  |  | ||||||
|     let oauth2_client = BasicClient::new( |     let oauth2_client = BasicClient::new( | ||||||
|   | |||||||
| @@ -58,6 +58,7 @@ 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, | ||||||
| @@ -159,6 +160,7 @@ 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, | ||||||
| @@ -318,13 +320,6 @@ 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,10 +16,12 @@ 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::dashboard::{ |     routes::dashboard::{ | ||||||
|         create_database_channel, create_reminder, template_name_default, DeleteReminder, |         create_database_channel, create_reminder, template_name_default, DeleteReminder, | ||||||
| @@ -247,9 +249,9 @@ pub async fn create_reminder_template( | |||||||
|             Ok(json!({})) |             Ok(json!({})) | ||||||
|         } |         } | ||||||
|         Err(e) => { |         Err(e) => { | ||||||
|             warn!("Could not fetch templates from {}: {:?}", id, e); |             warn!("Could not create template for {}: {:?}", id, e); | ||||||
|  |  | ||||||
|             json_err!("Could not get templates") |             json_err!("Could not create template") | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -339,6 +341,7 @@ pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySq | |||||||
|                  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, | ||||||
| @@ -374,35 +377,109 @@ 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), | ||||||
|  |             } + 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), | ||||||
|  |             } + 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 { | ||||||
| @@ -483,6 +560,7 @@ 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, | ||||||
|   | |||||||
| @@ -8,13 +8,13 @@ use rocket::{ | |||||||
|     serde::json::{json, Value as JsonValue}, |     serde::json::{json, Value as JsonValue}, | ||||||
| }; | }; | ||||||
| use rocket_dyn_templates::Template; | use rocket_dyn_templates::Template; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Deserializer, 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, MySql, Pool}; | use sqlx::{types::Json, Executor}; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     check_guild_subscription, check_subscription, |     check_guild_subscription, check_subscription, | ||||||
| @@ -50,6 +50,18 @@ 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")] | ||||||
| @@ -132,6 +144,7 @@ pub struct Reminder { | |||||||
|     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, | ||||||
| @@ -164,6 +177,7 @@ 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, | ||||||
| @@ -177,10 +191,13 @@ 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")] | ||||||
| @@ -190,6 +207,7 @@ 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>, | ||||||
| @@ -198,10 +216,13 @@ 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>, | ||||||
| @@ -210,10 +231,16 @@ 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)] |     #[serde(default = "interval_default")] | ||||||
|  |     #[serde(deserialize_with = "deserialize_optional_field")] | ||||||
|     interval_seconds: Unset<Option<u32>>, |     interval_seconds: Unset<Option<u32>>, | ||||||
|     #[serde(default)] |     #[serde(default = "interval_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>, | ||||||
| @@ -222,11 +249,36 @@ 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(); | ||||||
|  |  | ||||||
| @@ -301,11 +353,28 @@ pub struct TodoCsv { | |||||||
|  |  | ||||||
| pub async fn create_reminder( | pub async fn create_reminder( | ||||||
|     ctx: &Context, |     ctx: &Context, | ||||||
|     pool: &Pool<MySql>, |     pool: impl sqlx::Executor<'_, Database = Database> + Copy, | ||||||
|     guild_id: GuildId, |     guild_id: GuildId, | ||||||
|     user_id: UserId, |     user_id: UserId, | ||||||
|     reminder: Reminder, |     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(); | ||||||
| @@ -370,8 +439,12 @@ 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() || reminder.interval_months.is_some() { |     if reminder.interval_seconds.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 | ||||||
|         { |         { | ||||||
| @@ -380,7 +453,10 @@ pub async fn create_reminder( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // check patreon if necessary |     // check patreon if necessary | ||||||
|     if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() { |     if reminder.interval_seconds.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 | ||||||
|         { |         { | ||||||
| @@ -391,6 +467,11 @@ 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(); | ||||||
|  |  | ||||||
| @@ -416,13 +497,14 @@ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | ||||||
|         new_uid, |         new_uid, | ||||||
|         attachment_data, |         attachment_data, | ||||||
|         reminder.attachment_name, |         reminder.attachment_name, | ||||||
| @@ -442,11 +524,12 @@ 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, | ||||||
|         reminder.username, |         username, | ||||||
|         reminder.utc_time, |         reminder.utc_time, | ||||||
|     ) |     ) | ||||||
|     .execute(pool) |     .execute(pool) | ||||||
| @@ -473,6 +556,7 @@ 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, | ||||||
|   | |||||||
| @@ -135,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", |                         "Your login request was rejected. The server may be misconfigured. Please retry or alert us in Discord.", | ||||||
|                     )) |                     )) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             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)")) |             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)")) | ||||||
|         } |         } | ||||||
|     } else { |     } else { | ||||||
|         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)")) |         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)")) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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,6 +22,15 @@ 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"); | ||||||
| @@ -33,7 +42,10 @@ 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(2, "0"); |             minutes.value = String(Number(minutes.value) + Number(quotient)).padStart( | ||||||
|  |                 2, | ||||||
|  |                 "0" | ||||||
|  |             ); | ||||||
|         } |         } | ||||||
|         if (minutes.value >= 60) { |         if (minutes.value >= 60) { | ||||||
|             let quotient = Math.floor(minutes.value / 60); |             let quotient = Math.floor(minutes.value / 60); | ||||||
| @@ -42,12 +54,6 @@ 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); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,14 +60,15 @@ function update_select(sel) { | |||||||
|         sel.closest("div.reminderContent").querySelector("img.discord-avatar").src = |         sel.closest("div.reminderContent").querySelector("img.discord-avatar").src = | ||||||
|             sel.selectedOptions[0].dataset["webhookAvatar"]; |             sel.selectedOptions[0].dataset["webhookAvatar"]; | ||||||
|     } else { |     } else { | ||||||
|         sel.closest("div.reminderContent").querySelector("img.discord-avatar").src = ""; |         sel.closest("div.reminderContent").querySelector("img.discord-avatar").src = | ||||||
|  |             "/static/img/icon.png"; | ||||||
|     } |     } | ||||||
|     if (sel.selectedOptions[0].dataset["webhookName"]) { |     if (sel.selectedOptions[0].dataset["webhookName"]) { | ||||||
|         sel.closest("div.reminderContent").querySelector("input.discord-username").value = |         sel.closest("div.reminderContent").querySelector("input.discord-username").value = | ||||||
|             sel.selectedOptions[0].dataset["webhookName"]; |             sel.selectedOptions[0].dataset["webhookName"]; | ||||||
|     } else { |     } else { | ||||||
|         sel.closest("div.reminderContent").querySelector("input.discord-username").value = |         sel.closest("div.reminderContent").querySelector("input.discord-username").value = | ||||||
|             ""; |             "Reminder"; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -319,6 +320,7 @@ 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, | ||||||
| @@ -331,6 +333,9 @@ 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) { | ||||||
| @@ -351,6 +356,8 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     update_interval(frame); | ||||||
|  |  | ||||||
|     const lastChild = frame.querySelector("div.embed-multifield-box .embed-field-box"); |     const lastChild = frame.querySelector("div.embed-multifield-box .embed-field-box"); | ||||||
|  |  | ||||||
|     for (let field of reminder["embed_fields"]) { |     for (let field of reminder["embed_fields"]) { | ||||||
| @@ -497,6 +504,8 @@ 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"]; | ||||||
| @@ -715,6 +724,7 @@ $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; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -832,13 +842,6 @@ $deleteTemplateBtn.addEventListener("click", (ev) => { | |||||||
|         }); |         }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| document.querySelectorAll("textarea.autoresize").forEach((element) => { |  | ||||||
|     element.addEventListener("input", () => { |  | ||||||
|         element.style.height = ""; |  | ||||||
|         element.style.height = element.scrollHeight + 3 + "px"; |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| let $img; | let $img; | ||||||
| const $urlModal = document.querySelector("div#addImageModal"); | const $urlModal = document.querySelector("div#addImageModal"); | ||||||
| const $urlInput = $urlModal.querySelector("input"); | const $urlInput = $urlModal.querySelector("input"); | ||||||
| @@ -894,6 +897,13 @@ 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() { | ||||||
|   | |||||||
| @@ -191,19 +191,8 @@ | |||||||
|                     </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> | ||||||
|   | |||||||
| @@ -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">Creating reminders</p> |                     <p class="title">Create 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,19 +107,6 @@ | |||||||
|                     </div> |                     </div> | ||||||
|                 </article> |                 </article> | ||||||
|             </div> |             </div> | ||||||
|             <div class="tile is-parent"> |  | ||||||
|                 <article class="tile is-child notification"> |  | ||||||
|                     <p class="title">Dashboard</p> |  | ||||||
|                     <p class="subtitle">Learn to use the interactive web dashboard</p> |  | ||||||
|                     <div class="content has-text-centered"> |  | ||||||
|                         <a class="button is-size-4 is-rounded is-light" href="/help/dashboard"> |  | ||||||
|                             <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 is-vertical"> |             <div class="tile is-parent is-vertical"> | ||||||
|                 <article class="tile is-child notification"> |                 <article class="tile is-child notification"> | ||||||
|                     <p class="title">Import/Export</p> |                     <p class="title">Import/Export</p> | ||||||
| @@ -133,6 +120,19 @@ | |||||||
|                     </div> |                     </div> | ||||||
|                 </article> |                 </article> | ||||||
|             </div> |             </div> | ||||||
|  |             <div class="tile is-parent"> | ||||||
|  | <!--                <article class="tile is-child notification">--> | ||||||
|  | <!--                    <p class="title">Dashboard</p>--> | ||||||
|  | <!--                    <p class="subtitle">Learn to use the interactive web dashboard</p>--> | ||||||
|  | <!--                    <div class="content has-text-centered">--> | ||||||
|  | <!--                        <a class="button is-size-4 is-rounded is-light" href="/help/dashboard">--> | ||||||
|  | <!--                            <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> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -28,7 +28,10 @@ | |||||||
|             <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. |                     Reminders can also be created on the dashboard. The dashboard offers more options for configuring | ||||||
|  |                     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 your data</p> |                 <p class="title">Export 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,8 +26,7 @@ | |||||||
|             <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, <strong>existing data |                     You can import previous exports or modified exports. When importing a file, the new data will be added alongside existing data. | ||||||
|                     will be overwritten</strong>. |  | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
| @@ -55,7 +54,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 title row. |                         Make edits to the spreadsheet. You can add, edit, and remove rows for reminders. Don't remove the top-most (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> | ||||||
| @@ -70,7 +69,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 certain dates. |                     for marking calendar events. | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
| @@ -61,7 +61,8 @@ | |||||||
|                 <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. |                     This is optional, and if omitted, the reminder will repeat indefinitely. Otherwise, the reminder | ||||||
|  |                     will be deleted once the expiration date is reached. | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user