Compare commits
	
		
			2 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 32be8a4281 | |||
| e436d9db80 | 
@@ -1,2 +0,0 @@
 | 
				
			|||||||
printWidth = 90
 | 
					 | 
				
			||||||
tabWidth = 4
 | 
					 | 
				
			||||||
							
								
								
									
										2193
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										14
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						@@ -1,13 +1,14 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "reminder_rs"
 | 
					name = "reminder_rs"
 | 
				
			||||||
version = "1.6.0"
 | 
					version = "1.6.0-beta2"
 | 
				
			||||||
authors = ["jellywx <judesouthworth@pm.me>"]
 | 
					authors = ["jellywx <judesouthworth@pm.me>"]
 | 
				
			||||||
edition = "2018"
 | 
					edition = "2018"
 | 
				
			||||||
workspaces = [".", "postman", "web", "entity", "migration"]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
poise = "0.2"
 | 
					songbird = { git = "https://github.com/serenity-rs/songbird", branch = "next" }
 | 
				
			||||||
 | 
					poise = { git = "https://github.com/kangalioo/poise", branch = "master" }
 | 
				
			||||||
dotenv = "0.15"
 | 
					dotenv = "0.15"
 | 
				
			||||||
 | 
					humantime = "2.1"
 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
reqwest = "0.11"
 | 
					reqwest = "0.11"
 | 
				
			||||||
regex = "1.4"
 | 
					regex = "1.4"
 | 
				
			||||||
@@ -23,10 +24,5 @@ serde_repr = "0.1"
 | 
				
			|||||||
rmp-serde = "0.15"
 | 
					rmp-serde = "0.15"
 | 
				
			||||||
rand = "0.7"
 | 
					rand = "0.7"
 | 
				
			||||||
levenshtein = "1.0"
 | 
					levenshtein = "1.0"
 | 
				
			||||||
 | 
					sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]}
 | 
				
			||||||
base64 = "0.13.0"
 | 
					base64 = "0.13.0"
 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies.postman]
 | 
					 | 
				
			||||||
path = "postman"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies.reminder_web]
 | 
					 | 
				
			||||||
path = "web"
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -2,20 +2,13 @@
 | 
				
			|||||||
Reminder Bot for Discord.
 | 
					Reminder Bot for Discord.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## How do I use it?
 | 
					## How do I use it?
 | 
				
			||||||
I offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating 
 | 
					We offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating 
 | 
				
			||||||
reminders are paid on the hosted version of the bot. Keep reading if you want to host it yourself.
 | 
					reminders are paid on the hosted version of the bot. Keep reading if you want to host it yourself.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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
 | 
				
			||||||
Install build requirements: 
 | 
					Reminder Bot can be built by running `cargo build --release` in the top level directory. It is necessary to create a folder called 'assets' containing an image file with its name specified in the environment as `WEBHOOK_AVATAR`, of dimensions 128x128px to be used as the webhook avatar.
 | 
				
			||||||
`sudo apt install gcc gcc-multilib cmake libssl-dev build-essential`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Install Rust from https://rustup.rs
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Reminder Bot can then be built by running `cargo build --release` in the top level directory. It is necessary to create a 
 | 
					 | 
				
			||||||
folder called 'assets' containing an image file with its name specified in the environment as `WEBHOOK_AVATAR`, of 
 | 
					 | 
				
			||||||
dimensions 128x128px to be used as the webhook avatar.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Compilation environment variables
 | 
					#### Compilation environment variables
 | 
				
			||||||
These environment variables must be provided when compiling the bot
 | 
					These environment variables must be provided when compiling the bot
 | 
				
			||||||
@@ -37,10 +30,15 @@ __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
 | 
				
			||||||
 | 
					* `IGNORE_BOTS` - default `1`, if `1`, Reminder Bot will ignore all other bots
 | 
				
			||||||
* `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else
 | 
					* `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else
 | 
				
			||||||
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds 
 | 
					* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds 
 | 
				
			||||||
 | 
					* `SHARD_COUNT` - default `None`, accepts the number of shards that are being ran
 | 
				
			||||||
 | 
					* `SHARD_RANGE` - default `None`, if `SHARD_COUNT` is specified, specifies what range of shards to start on this process 
 | 
				
			||||||
* `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages
 | 
					* `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Todo List
 | 
					### Todo List
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Convert aliases to macros
 | 
					* Convert aliases to macros
 | 
				
			||||||
 | 
					* Help command
 | 
				
			||||||
 | 
					* Test everything
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										28
									
								
								Rocket.toml
									
									
									
									
									
								
							
							
						
						@@ -1,28 +0,0 @@
 | 
				
			|||||||
[default]
 | 
					 | 
				
			||||||
address = "0.0.0.0"
 | 
					 | 
				
			||||||
port = 5000
 | 
					 | 
				
			||||||
template_dir = "web/templates"
 | 
					 | 
				
			||||||
limits = { json = "10MiB" }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[debug]
 | 
					 | 
				
			||||||
secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[debug.tls]
 | 
					 | 
				
			||||||
certs = "web/private/rsa_sha256_cert.pem"
 | 
					 | 
				
			||||||
key = "web/private/rsa_sha256_key.pem"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[rsa_sha256.tls]
 | 
					 | 
				
			||||||
certs = "web/private/rsa_sha256_cert.pem"
 | 
					 | 
				
			||||||
key = "web/private/rsa_sha256_key.pem"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[ecdsa_nistp256_sha256.tls]
 | 
					 | 
				
			||||||
certs = "web/private/ecdsa_nistp256_sha256_cert.pem"
 | 
					 | 
				
			||||||
key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[ecdsa_nistp384_sha384.tls]
 | 
					 | 
				
			||||||
certs = "web/private/ecdsa_nistp384_sha384_cert.pem"
 | 
					 | 
				
			||||||
key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[ed25519.tls]
 | 
					 | 
				
			||||||
certs = "web/private/ed25519_cert.pem"
 | 
					 | 
				
			||||||
key = "eb/private/ed25519_key.pem"
 | 
					 | 
				
			||||||
							
								
								
									
										233
									
								
								migration/00-initial.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,233 @@
 | 
				
			|||||||
 | 
					CREATE DATABASE IF NOT EXISTS reminders;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SET FOREIGN_KEY_CHECKS=0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					USE reminders;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.guilds (
 | 
				
			||||||
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    guild BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    prefix VARCHAR(5) DEFAULT '$' NOT NULL,
 | 
				
			||||||
 | 
					    timezone VARCHAR(32) DEFAULT 'UTC' NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    default_channel_id INT UNSIGNED,
 | 
				
			||||||
 | 
					    default_username VARCHAR(32) DEFAULT 'Reminder' NOT NULL,
 | 
				
			||||||
 | 
					    default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (default_channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.channels (
 | 
				
			||||||
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    channel BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    nudge SMALLINT NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    blacklisted BOOL NOT NULL DEFAULT FALSE,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    webhook_id BIGINT UNSIGNED UNIQUE,
 | 
				
			||||||
 | 
					    webhook_token TEXT,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    paused BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    paused_until TIMESTAMP,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.users (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					    user BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(37) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dm_channel INT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    language VARCHAR(2) DEFAULT 'EN' NOT NULL,
 | 
				
			||||||
 | 
					    timezone VARCHAR(32) DEFAULT 'UTC' NOT NULL,
 | 
				
			||||||
 | 
					    meridian_time BOOLEAN DEFAULT 0 NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    allowed_dm BOOLEAN DEFAULT 1 NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    patreon BOOLEAN NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (dm_channel) REFERENCES reminders.channels(id) ON DELETE RESTRICT
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.roles (
 | 
				
			||||||
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    role BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.embeds (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    description VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    image_url VARCHAR(512),
 | 
				
			||||||
 | 
					    thumbnail_url VARCHAR(512),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    footer VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    footer_icon VARCHAR(512),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    color MEDIUMINT UNSIGNED NOT NULL DEFAULT 0x0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.embed_fields (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    value VARCHAR(1024) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    inline BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    embed_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.messages (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    content VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    tts BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    embed_id INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    attachment MEDIUMBLOB,
 | 
				
			||||||
 | 
					    attachment_name VARCHAR(260),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE SET NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.reminders (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					    uid VARCHAR(64) UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(24) NOT NULL DEFAULT 'Reminder',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    message_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    channel_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `time` INT UNSIGNED DEFAULT 0 NOT NULL,
 | 
				
			||||||
 | 
					    `interval` INT UNSIGNED DEFAULT NULL,
 | 
				
			||||||
 | 
					    expires TIMESTAMP DEFAULT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    enabled BOOLEAN DEFAULT 1 NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    avatar VARCHAR(512),
 | 
				
			||||||
 | 
					    username VARCHAR(32),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    method ENUM('remind', 'natural', 'dashboard', 'todo', 'countdown'),
 | 
				
			||||||
 | 
					    set_at TIMESTAMP DEFAULT NOW(),
 | 
				
			||||||
 | 
					    set_by INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (message_id) REFERENCES reminders.messages(id) ON DELETE RESTRICT,
 | 
				
			||||||
 | 
					    FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    FOREIGN KEY (set_by) REFERENCES reminders.users(id) ON DELETE SET NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TRIGGER message_cleanup AFTER DELETE ON reminders.reminders
 | 
				
			||||||
 | 
					FOR EACH ROW
 | 
				
			||||||
 | 
					    DELETE FROM reminders.messages WHERE id = OLD.message_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TRIGGER embed_cleanup AFTER DELETE ON reminders.messages
 | 
				
			||||||
 | 
					FOR EACH ROW
 | 
				
			||||||
 | 
					    DELETE FROM reminders.embeds WHERE id = OLD.embed_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.todos (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					    user_id INT UNSIGNED,
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED,
 | 
				
			||||||
 | 
					    channel_id INT UNSIGNED,
 | 
				
			||||||
 | 
					    value VARCHAR(2000) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL,
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.command_restrictions (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    role_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (role_id) REFERENCES reminders.roles(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    UNIQUE KEY (`role_id`, `command`)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.timers (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					    start_time TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
				
			||||||
 | 
					    name VARCHAR(32) NOT NULL,
 | 
				
			||||||
 | 
					    owner BIGINT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.events (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					    `time` TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event_name ENUM('edit', 'enable', 'disable', 'delete') NOT NULL,
 | 
				
			||||||
 | 
					    bulk_count INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    user_id INT UNSIGNED,
 | 
				
			||||||
 | 
					    reminder_id INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL,
 | 
				
			||||||
 | 
					    FOREIGN KEY (reminder_id) REFERENCES reminders.reminders(id) ON DELETE SET NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.command_aliases (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    name VARCHAR(12) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    command VARCHAR(2048) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    UNIQUE KEY (`guild_id`, `name`)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders.guild_users (
 | 
				
			||||||
 | 
					    guild INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    user INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    can_access BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    FOREIGN KEY (user) REFERENCES reminders.users(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    UNIQUE KEY (guild, user)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE EVENT reminders.event_cleanup
 | 
				
			||||||
 | 
					ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
 | 
				
			||||||
 | 
					ON COMPLETION PRESERVE
 | 
				
			||||||
 | 
					DO DELETE FROM reminders.events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
 | 
				
			||||||
							
								
								
									
										160
									
								
								migration/01-reminder_message_embed.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										13
									
								
								migration/02-macro.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					USE reminders;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE macro (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100) NOT NULL,
 | 
				
			||||||
 | 
					    description VARCHAR(100),
 | 
				
			||||||
 | 
					    commands TEXT NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										7
									
								
								models/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -1,7 +0,0 @@
 | 
				
			|||||||
# This file is automatically @generated by Cargo.
 | 
					 | 
				
			||||||
# It is not intended for manual editing.
 | 
					 | 
				
			||||||
version = 3
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "models"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
@@ -1,8 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "models"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
@@ -1,8 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "entity"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
chrono-tz = "^0.6"
 | 
					 | 
				
			||||||
sea-orm = { version = "^0.8", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"] }
 | 
					 | 
				
			||||||
@@ -1,60 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "channel")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key, auto_increment = false)]
 | 
					 | 
				
			||||||
    pub id: i64,
 | 
					 | 
				
			||||||
    pub guild_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub nudge: i32,
 | 
					 | 
				
			||||||
    pub webhook_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub webhook_token: Option<String>,
 | 
					 | 
				
			||||||
    pub paused: bool,
 | 
					 | 
				
			||||||
    pub paused_until: Option<DateTimeUtc>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::GuildId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::user::Entity")]
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::reminder::Entity")]
 | 
					 | 
				
			||||||
    Reminder,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::todo::Entity")]
 | 
					 | 
				
			||||||
    Todo,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::guild::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Guild.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::user::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::User.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::reminder::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Reminder.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::todo::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Todo.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,34 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "command_macro")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub guild_id: i64,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub description: Option<String>,
 | 
					 | 
				
			||||||
    pub commands: Option<Json>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::GuildId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::guild::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Guild.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,48 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "guild")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key, auto_increment = false)]
 | 
					 | 
				
			||||||
    pub id: i64,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::reminder_template::Entity")]
 | 
					 | 
				
			||||||
    ReminderTemplate,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::channel::Entity")]
 | 
					 | 
				
			||||||
    Channel,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::todo::Entity")]
 | 
					 | 
				
			||||||
    Todo,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::command_macro::Entity")]
 | 
					 | 
				
			||||||
    CommandMacro,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::reminder_template::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::ReminderTemplate.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::channel::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Channel.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::todo::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Todo.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::command_macro::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::CommandMacro.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1 +0,0 @@
 | 
				
			|||||||
 | 
					 | 
				
			||||||
@@ -1,14 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub mod prelude;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub mod channel;
 | 
					 | 
				
			||||||
pub mod command_macro;
 | 
					 | 
				
			||||||
pub mod guild;
 | 
					 | 
				
			||||||
pub mod reminder;
 | 
					 | 
				
			||||||
pub mod reminder_template;
 | 
					 | 
				
			||||||
pub mod sea_orm_active_enums;
 | 
					 | 
				
			||||||
pub mod seaql_migrations;
 | 
					 | 
				
			||||||
pub mod timer;
 | 
					 | 
				
			||||||
pub mod todo;
 | 
					 | 
				
			||||||
pub mod user;
 | 
					 | 
				
			||||||
@@ -1,8 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub use super::{
 | 
					 | 
				
			||||||
    channel::Entity as Channel, command_macro::Entity as CommandMacro, guild::Entity as Guild,
 | 
					 | 
				
			||||||
    reminder::Entity as Reminder, reminder_template::Entity as ReminderTemplate,
 | 
					 | 
				
			||||||
    seaql_migrations::Entity as SeaqlMigrations, timer::Entity as Timer, todo::Entity as Todo,
 | 
					 | 
				
			||||||
    user::Entity as User,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@@ -1,73 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use super::sea_orm_active_enums::Timezone;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "reminder")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub uid: String,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub channel_id: i64,
 | 
					 | 
				
			||||||
    pub utc_time: DateTimeUtc,
 | 
					 | 
				
			||||||
    pub timezone: Timezone,
 | 
					 | 
				
			||||||
    pub interval_seconds: Option<i32>,
 | 
					 | 
				
			||||||
    pub interval_months: Option<i32>,
 | 
					 | 
				
			||||||
    pub enabled: bool,
 | 
					 | 
				
			||||||
    pub expires: Option<DateTimeUtc>,
 | 
					 | 
				
			||||||
    pub username: Option<String>,
 | 
					 | 
				
			||||||
    pub avatar: Option<String>,
 | 
					 | 
				
			||||||
    pub content: Option<String>,
 | 
					 | 
				
			||||||
    pub tts: bool,
 | 
					 | 
				
			||||||
    pub attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    pub attachment_name: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_title: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_description: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_image_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_thumbnail_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_footer: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_footer_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_author: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_author_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_color: Option<i32>,
 | 
					 | 
				
			||||||
    pub embed_fields: Option<Json>,
 | 
					 | 
				
			||||||
    pub set_at: DateTimeUtc,
 | 
					 | 
				
			||||||
    pub set_by: i64,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::channel::Entity",
 | 
					 | 
				
			||||||
        from = "Column::ChannelId",
 | 
					 | 
				
			||||||
        to = "super::channel::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Channel,
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::user::Entity",
 | 
					 | 
				
			||||||
        from = "Column::SetBy",
 | 
					 | 
				
			||||||
        to = "super::user::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::channel::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Channel.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::user::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::User.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,48 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "reminder_template")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub guild_id: i64,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub username: Option<String>,
 | 
					 | 
				
			||||||
    pub avatar: Option<String>,
 | 
					 | 
				
			||||||
    pub content: Option<String>,
 | 
					 | 
				
			||||||
    pub tts: bool,
 | 
					 | 
				
			||||||
    pub attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    pub attachment_name: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_title: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_description: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_image_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_thumbnail_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_footer: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_footer_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_author: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_author_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_color: Option<i32>,
 | 
					 | 
				
			||||||
    pub embed_fields: Option<Json>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::GuildId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::guild::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Guild.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,22 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "seaql_migrations")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key, auto_increment = false)]
 | 
					 | 
				
			||||||
    pub version: String,
 | 
					 | 
				
			||||||
    pub applied_at: i64,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter)]
 | 
					 | 
				
			||||||
pub enum Relation {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl RelationTrait for Relation {
 | 
					 | 
				
			||||||
    fn def(&self) -> RelationDef {
 | 
					 | 
				
			||||||
        panic!("No RelationDef")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,36 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "timer")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub start_time: DateTimeUtc,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub user_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub guild_id: Option<i64>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::GuildId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild2,
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::UserId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild1,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,62 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "todo")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub user_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub guild_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub channel_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub value: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::channel::Entity",
 | 
					 | 
				
			||||||
        from = "Column::ChannelId",
 | 
					 | 
				
			||||||
        to = "super::channel::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Channel,
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::GuildId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild,
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::user::Entity",
 | 
					 | 
				
			||||||
        from = "Column::UserId",
 | 
					 | 
				
			||||||
        to = "super::user::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::channel::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Channel.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::guild::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Guild.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::user::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::User.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,50 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use super::sea_orm_active_enums::Timezone;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "user")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key, auto_increment = false)]
 | 
					 | 
				
			||||||
    pub id: i64,
 | 
					 | 
				
			||||||
    pub dm_channel: i64,
 | 
					 | 
				
			||||||
    pub timezone: Timezone,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::channel::Entity",
 | 
					 | 
				
			||||||
        from = "Column::DmChannel",
 | 
					 | 
				
			||||||
        to = "super::channel::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Channel,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::reminder::Entity")]
 | 
					 | 
				
			||||||
    Reminder,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::todo::Entity")]
 | 
					 | 
				
			||||||
    Todo,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::channel::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Channel.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::reminder::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Reminder.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::todo::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Todo.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
							
								
								
									
										2400
									
								
								models/migration/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -1,16 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "migration"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
publish = false
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[lib]
 | 
					 | 
				
			||||||
name = "migration"
 | 
					 | 
				
			||||||
path = "src/lib.rs"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
entity = { path = "../entity" }
 | 
					 | 
				
			||||||
chrono-tz = "^0.6"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies.sea-orm-migration]
 | 
					 | 
				
			||||||
version = "^0.8.0"
 | 
					 | 
				
			||||||
@@ -1,37 +0,0 @@
 | 
				
			|||||||
# Running Migrator CLI
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Apply all pending migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- up
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Apply first 10 pending migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- up -n 10
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Rollback last applied migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- down
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Rollback last 10 applied migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- down -n 10
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Drop all tables from the database, then reapply all migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- fresh
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Rollback all applied migrations, then reapply all migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- refresh
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Rollback all applied migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- reset
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Check the status of all migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- status
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
pub use sea_orm_migration::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
mod m20220101_000001_create_table;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct Migrator;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_trait::async_trait]
 | 
					 | 
				
			||||||
impl MigratorTrait for Migrator {
 | 
					 | 
				
			||||||
    fn migrations() -> Vec<Box<dyn MigrationTrait>> {
 | 
					 | 
				
			||||||
        vec![Box::new(m20220101_000001_create_table::Migration)]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,553 +0,0 @@
 | 
				
			|||||||
use chrono_tz::{Tz, TZ_VARIANTS};
 | 
					 | 
				
			||||||
use sea_orm_migration::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::extension::postgres::Type;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct Migration;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl MigrationName for Migration {
 | 
					 | 
				
			||||||
    fn name(&self) -> &str {
 | 
					 | 
				
			||||||
        "m20220101_000001_create_table"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum Guild {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum Channel {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    GuildId,
 | 
					 | 
				
			||||||
    Nudge,
 | 
					 | 
				
			||||||
    WebhookId,
 | 
					 | 
				
			||||||
    WebhookToken,
 | 
					 | 
				
			||||||
    Paused,
 | 
					 | 
				
			||||||
    PausedUntil,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum User {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    DmChannel,
 | 
					 | 
				
			||||||
    Timezone,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum Reminder {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    Uid,
 | 
					 | 
				
			||||||
    Name,
 | 
					 | 
				
			||||||
    ChannelId,
 | 
					 | 
				
			||||||
    UtcTime,
 | 
					 | 
				
			||||||
    Timezone,
 | 
					 | 
				
			||||||
    IntervalSeconds,
 | 
					 | 
				
			||||||
    IntervalMonths,
 | 
					 | 
				
			||||||
    Enabled,
 | 
					 | 
				
			||||||
    Expires,
 | 
					 | 
				
			||||||
    Username,
 | 
					 | 
				
			||||||
    Avatar,
 | 
					 | 
				
			||||||
    Content,
 | 
					 | 
				
			||||||
    Tts,
 | 
					 | 
				
			||||||
    Attachment,
 | 
					 | 
				
			||||||
    AttachmentName,
 | 
					 | 
				
			||||||
    EmbedTitle,
 | 
					 | 
				
			||||||
    EmbedDescription,
 | 
					 | 
				
			||||||
    EmbedImageUrl,
 | 
					 | 
				
			||||||
    EmbedThumbnailUrl,
 | 
					 | 
				
			||||||
    EmbedFooter,
 | 
					 | 
				
			||||||
    EmbedFooterUrl,
 | 
					 | 
				
			||||||
    EmbedAuthor,
 | 
					 | 
				
			||||||
    EmbedAuthorUrl,
 | 
					 | 
				
			||||||
    EmbedColor,
 | 
					 | 
				
			||||||
    EmbedFields,
 | 
					 | 
				
			||||||
    SetAt,
 | 
					 | 
				
			||||||
    SetBy,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum ReminderTemplate {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    GuildId,
 | 
					 | 
				
			||||||
    Name,
 | 
					 | 
				
			||||||
    Username,
 | 
					 | 
				
			||||||
    Avatar,
 | 
					 | 
				
			||||||
    Content,
 | 
					 | 
				
			||||||
    Tts,
 | 
					 | 
				
			||||||
    Attachment,
 | 
					 | 
				
			||||||
    AttachmentName,
 | 
					 | 
				
			||||||
    EmbedTitle,
 | 
					 | 
				
			||||||
    EmbedDescription,
 | 
					 | 
				
			||||||
    EmbedImageUrl,
 | 
					 | 
				
			||||||
    EmbedThumbnailUrl,
 | 
					 | 
				
			||||||
    EmbedFooter,
 | 
					 | 
				
			||||||
    EmbedFooterUrl,
 | 
					 | 
				
			||||||
    EmbedAuthor,
 | 
					 | 
				
			||||||
    EmbedAuthorUrl,
 | 
					 | 
				
			||||||
    EmbedColor,
 | 
					 | 
				
			||||||
    EmbedFields,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum Timer {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    StartTime,
 | 
					 | 
				
			||||||
    Name,
 | 
					 | 
				
			||||||
    UserId,
 | 
					 | 
				
			||||||
    GuildId,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum Todo {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    UserId,
 | 
					 | 
				
			||||||
    GuildId,
 | 
					 | 
				
			||||||
    ChannelId,
 | 
					 | 
				
			||||||
    Value,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum CommandMacro {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    GuildId,
 | 
					 | 
				
			||||||
    Name,
 | 
					 | 
				
			||||||
    Description,
 | 
					 | 
				
			||||||
    Commands,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub enum Timezone {
 | 
					 | 
				
			||||||
    Type,
 | 
					 | 
				
			||||||
    Tz(Tz),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Iden for Timezone {
 | 
					 | 
				
			||||||
    fn unquoted(&self, s: &mut dyn Write) {
 | 
					 | 
				
			||||||
        write!(
 | 
					 | 
				
			||||||
            s,
 | 
					 | 
				
			||||||
            "{}",
 | 
					 | 
				
			||||||
            match self {
 | 
					 | 
				
			||||||
                Self::Type => "timezone".to_string(),
 | 
					 | 
				
			||||||
                Self::Tz(tz) => tz.to_string(),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_trait::async_trait]
 | 
					 | 
				
			||||||
impl MigrationTrait for Migration {
 | 
					 | 
				
			||||||
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_type(
 | 
					 | 
				
			||||||
                Type::create()
 | 
					 | 
				
			||||||
                    .as_enum(Timezone::Type)
 | 
					 | 
				
			||||||
                    .values(TZ_VARIANTS.iter().map(|tz| Timezone::Tz(tz.to_owned())))
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(Guild::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Guild::Id).big_integer().not_null().primary_key())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(Channel::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::Id).big_integer().not_null().primary_key())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::GuildId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::Nudge).integer().not_null().default(0))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::WebhookId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::WebhookToken).string())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::Paused).boolean().not_null().default(false))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::PausedUntil).date_time())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_channel_guild")
 | 
					 | 
				
			||||||
                    .from(Channel::Table, Channel::GuildId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(User::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(User::Id).big_integer().not_null().primary_key())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(User::DmChannel).big_integer().not_null())
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(User::Timezone)
 | 
					 | 
				
			||||||
                            .custom(Timezone::Type)
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .default("UTC"),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_user_channel")
 | 
					 | 
				
			||||||
                    .from(User::Table, User::DmChannel)
 | 
					 | 
				
			||||||
                    .to(Channel::Table, Channel::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(Reminder::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(Reminder::Id)
 | 
					 | 
				
			||||||
                            .integer()
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .auto_increment()
 | 
					 | 
				
			||||||
                            .primary_key(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Uid).string().char_len(64).not_null())
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(Reminder::Name)
 | 
					 | 
				
			||||||
                            .string()
 | 
					 | 
				
			||||||
                            .char_len(24)
 | 
					 | 
				
			||||||
                            .default("Reminder")
 | 
					 | 
				
			||||||
                            .not_null(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::ChannelId).big_integer().not_null())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::UtcTime).date_time().not_null())
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(Reminder::Timezone)
 | 
					 | 
				
			||||||
                            .custom(Timezone::Type)
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .default("UTC"),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::IntervalSeconds).integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::IntervalMonths).integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Enabled).boolean().not_null().default(false))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Expires).date_time())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Username).string_len(32))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Avatar).string_len(512))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Content).string_len(2000))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Tts).boolean().not_null().default(false))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Attachment).binary_len(8 * 1024 * 1024))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::AttachmentName).string_len(260))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedTitle).string_len(256))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedDescription).string_len(4096))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedImageUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedThumbnailUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedFooter).string_len(2048))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedFooterUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedAuthor).string_len(256))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedAuthorUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedColor).integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedFields).json())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::SetAt).date_time().not_null().default("NOW()"))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::SetBy).big_integer().not_null())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_reminder_channel")
 | 
					 | 
				
			||||||
                    .from(Reminder::Table, Reminder::ChannelId)
 | 
					 | 
				
			||||||
                    .to(Channel::Table, Channel::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_reminder_user")
 | 
					 | 
				
			||||||
                    .from(Reminder::Table, Reminder::SetBy)
 | 
					 | 
				
			||||||
                    .to(User::Table, User::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(ReminderTemplate::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(ReminderTemplate::Id)
 | 
					 | 
				
			||||||
                            .integer()
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .auto_increment()
 | 
					 | 
				
			||||||
                            .primary_key(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::GuildId).big_integer().not_null())
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(ReminderTemplate::Name)
 | 
					 | 
				
			||||||
                            .string()
 | 
					 | 
				
			||||||
                            .char_len(24)
 | 
					 | 
				
			||||||
                            .default("Reminder")
 | 
					 | 
				
			||||||
                            .not_null(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::Username).string_len(32))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::Avatar).string_len(512))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::Content).string_len(2000))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::Tts).boolean().not_null().default(false))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::Attachment).binary_len(8 * 1024 * 1024))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::AttachmentName).string_len(260))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedTitle).string_len(256))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedDescription).string_len(4096))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedImageUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedThumbnailUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedFooter).string_len(2048))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedFooterUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedAuthor).string_len(256))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedAuthorUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedColor).integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedFields).json())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_reminder_template_guild")
 | 
					 | 
				
			||||||
                    .from(ReminderTemplate::Table, ReminderTemplate::GuildId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(Timer::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(Timer::Id)
 | 
					 | 
				
			||||||
                            .integer()
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .auto_increment()
 | 
					 | 
				
			||||||
                            .primary_key(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Timer::StartTime).date_time().not_null().default("NOW()"))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Timer::Name).string_len(32).not_null().default("Timer"))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Timer::UserId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Timer::GuildId).big_integer())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_timer_user")
 | 
					 | 
				
			||||||
                    .from(Timer::Table, Timer::UserId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_timer_guild")
 | 
					 | 
				
			||||||
                    .from(Timer::Table, Timer::GuildId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(Todo::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(Todo::Id)
 | 
					 | 
				
			||||||
                            .integer()
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .auto_increment()
 | 
					 | 
				
			||||||
                            .primary_key(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Todo::UserId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Todo::GuildId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Todo::ChannelId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Todo::Value).string_len(2000).not_null())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_todo_user")
 | 
					 | 
				
			||||||
                    .from(Todo::Table, Todo::UserId)
 | 
					 | 
				
			||||||
                    .to(User::Table, User::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_todo_guild")
 | 
					 | 
				
			||||||
                    .from(Todo::Table, Todo::GuildId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_todo_channel")
 | 
					 | 
				
			||||||
                    .from(Todo::Table, Todo::ChannelId)
 | 
					 | 
				
			||||||
                    .to(Channel::Table, Channel::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(CommandMacro::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(CommandMacro::Id)
 | 
					 | 
				
			||||||
                            .integer()
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .auto_increment()
 | 
					 | 
				
			||||||
                            .primary_key(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(CommandMacro::GuildId).big_integer().not_null())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(CommandMacro::Name).string_len(100).not_null())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(CommandMacro::Description).string_len(100))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(CommandMacro::Commands).json())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_command_macro_guild")
 | 
					 | 
				
			||||||
                    .from(CommandMacro::Table, CommandMacro::GuildId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Channel::Table).name("fk_channel_guild").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(User::Table).name("fk_user_channel").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Reminder::Table).name("fk_reminder_channel").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Reminder::Table).name("fk_reminder_user").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop()
 | 
					 | 
				
			||||||
                    .table(ReminderTemplate::Table)
 | 
					 | 
				
			||||||
                    .name("fk_reminder_template_guild")
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Timer::Table).name("fk_timer_user").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Timer::Table).name("fk_timer_guild").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(ForeignKey::drop().table(Todo::Table).name("fk_todo_user").to_owned())
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Todo::Table).name("fk_todo_guild").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Todo::Table).name("fk_todo_channel").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop()
 | 
					 | 
				
			||||||
                    .table(CommandMacro::Table)
 | 
					 | 
				
			||||||
                    .name("fk_command_macro_guild")
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(Guild::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(Channel::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(User::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(Reminder::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(ReminderTemplate::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(Timer::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(Todo::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(CommandMacro::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager.drop_type(Type::drop().name(Timezone::Type).to_owned()).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,6 +0,0 @@
 | 
				
			|||||||
use sea_orm_migration::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_std::main]
 | 
					 | 
				
			||||||
async fn main() {
 | 
					 | 
				
			||||||
    cli::run_cli(migration::Migrator).await;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1 +0,0 @@
 | 
				
			|||||||
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "postman"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					 | 
				
			||||||
regex = "1.4"
 | 
					 | 
				
			||||||
log = "0.4"
 | 
					 | 
				
			||||||
env_logger = "0.8"
 | 
					 | 
				
			||||||
chrono = "0.4"
 | 
					 | 
				
			||||||
chrono-tz = { version = "0.5", features = ["serde"] }
 | 
					 | 
				
			||||||
lazy_static = "1.4"
 | 
					 | 
				
			||||||
num-integer = "0.1"
 | 
					 | 
				
			||||||
serde = "1.0"
 | 
					 | 
				
			||||||
serde_json = "1.0"
 | 
					 | 
				
			||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
 | 
					 | 
				
			||||||
serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
					 | 
				
			||||||
@@ -1,50 +0,0 @@
 | 
				
			|||||||
mod sender;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::env;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use log::{info, warn};
 | 
					 | 
				
			||||||
use serenity::client::Context;
 | 
					 | 
				
			||||||
use sqlx::{Executor, MySql};
 | 
					 | 
				
			||||||
use tokio::{
 | 
					 | 
				
			||||||
    sync::broadcast::Receiver,
 | 
					 | 
				
			||||||
    time::{sleep_until, Duration, Instant},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Database = MySql;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn initialize(
 | 
					 | 
				
			||||||
    mut kill: Receiver<()>,
 | 
					 | 
				
			||||||
    ctx: Context,
 | 
					 | 
				
			||||||
    pool: impl Executor<'_, Database = Database> + Copy,
 | 
					 | 
				
			||||||
) -> Result<(), &'static str> {
 | 
					 | 
				
			||||||
    tokio::select! {
 | 
					 | 
				
			||||||
        output = _initialize(ctx, pool) => Ok(output),
 | 
					 | 
				
			||||||
        _ = kill.recv() => {
 | 
					 | 
				
			||||||
            warn!("Received terminate signal. Goodbye");
 | 
					 | 
				
			||||||
            Err("Received terminate signal. Goodbye")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
					 | 
				
			||||||
    let remind_interval = env::var("REMIND_INTERVAL")
 | 
					 | 
				
			||||||
        .map(|inner| inner.parse::<u64>().ok())
 | 
					 | 
				
			||||||
        .ok()
 | 
					 | 
				
			||||||
        .flatten()
 | 
					 | 
				
			||||||
        .unwrap_or(10);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    loop {
 | 
					 | 
				
			||||||
        let sleep_to = Instant::now() + Duration::from_secs(remind_interval);
 | 
					 | 
				
			||||||
        let reminders = sender::Reminder::fetch_reminders(pool).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if reminders.len() > 0 {
 | 
					 | 
				
			||||||
            info!("Preparing to send {} reminders.", reminders.len());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for reminder in reminders {
 | 
					 | 
				
			||||||
                reminder.send(pool, ctx.clone()).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        sleep_until(sleep_to).await;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,595 +0,0 @@
 | 
				
			|||||||
use chrono::Duration;
 | 
					 | 
				
			||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use lazy_static::lazy_static;
 | 
					 | 
				
			||||||
use log::{error, info, warn};
 | 
					 | 
				
			||||||
use num_integer::Integer;
 | 
					 | 
				
			||||||
use regex::{Captures, Regex};
 | 
					 | 
				
			||||||
use serde::Deserialize;
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    builder::CreateEmbed,
 | 
					 | 
				
			||||||
    http::{CacheHttp, Http, HttpError, StatusCode},
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        channel::{Channel, Embed as SerenityEmbed},
 | 
					 | 
				
			||||||
        id::ChannelId,
 | 
					 | 
				
			||||||
        webhook::Webhook,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Error, Result,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::{
 | 
					 | 
				
			||||||
    types::{
 | 
					 | 
				
			||||||
        chrono::{NaiveDateTime, Utc},
 | 
					 | 
				
			||||||
        Json,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Executor,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::Database;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
lazy_static! {
 | 
					 | 
				
			||||||
    pub static ref TIMEFROM_REGEX: Regex =
 | 
					 | 
				
			||||||
        Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
 | 
					 | 
				
			||||||
    pub static ref TIMENOW_REGEX: Regex =
 | 
					 | 
				
			||||||
        Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn fmt_displacement(format: &str, seconds: u64) -> String {
 | 
					 | 
				
			||||||
    let mut seconds = seconds;
 | 
					 | 
				
			||||||
    let mut days: u64 = 0;
 | 
					 | 
				
			||||||
    let mut hours: u64 = 0;
 | 
					 | 
				
			||||||
    let mut minutes: u64 = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (rep, time_type, div) in
 | 
					 | 
				
			||||||
        [("%d", &mut days, 86400), ("%h", &mut hours, 3600), ("%m", &mut minutes, 60)].iter_mut()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if format.contains(*rep) {
 | 
					 | 
				
			||||||
            let (divided, new_seconds) = seconds.div_rem(&div);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            **time_type = divided;
 | 
					 | 
				
			||||||
            seconds = new_seconds;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    format
 | 
					 | 
				
			||||||
        .replace("%s", &seconds.to_string())
 | 
					 | 
				
			||||||
        .replace("%m", &minutes.to_string())
 | 
					 | 
				
			||||||
        .replace("%h", &hours.to_string())
 | 
					 | 
				
			||||||
        .replace("%d", &days.to_string())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn substitute(string: &str) -> String {
 | 
					 | 
				
			||||||
    let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
 | 
					 | 
				
			||||||
        let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
 | 
					 | 
				
			||||||
        let format = caps.name("format").map(|m| m.as_str());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let (Some(final_time), Some(format)) = (final_time, format) {
 | 
					 | 
				
			||||||
            let dt = NaiveDateTime::from_timestamp(final_time, 0);
 | 
					 | 
				
			||||||
            let now = Utc::now().naive_utc();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let difference = {
 | 
					 | 
				
			||||||
                if now < dt {
 | 
					 | 
				
			||||||
                    dt - Utc::now().naive_utc()
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    Utc::now().naive_utc() - dt
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            fmt_displacement(format, difference.num_seconds() as u64)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            String::new()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    TIMENOW_REGEX
 | 
					 | 
				
			||||||
        .replace(&new, |caps: &Captures| {
 | 
					 | 
				
			||||||
            let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
 | 
					 | 
				
			||||||
            let format = caps.name("format").map(|m| m.as_str());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let (Some(timezone), Some(format)) = (timezone, format) {
 | 
					 | 
				
			||||||
                let now = Utc::now().with_timezone(&timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                now.format(format).to_string()
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                String::new()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .to_string()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Embed {
 | 
					 | 
				
			||||||
    title: String,
 | 
					 | 
				
			||||||
    description: String,
 | 
					 | 
				
			||||||
    image_url: Option<String>,
 | 
					 | 
				
			||||||
    thumbnail_url: Option<String>,
 | 
					 | 
				
			||||||
    footer: String,
 | 
					 | 
				
			||||||
    footer_url: Option<String>,
 | 
					 | 
				
			||||||
    author: String,
 | 
					 | 
				
			||||||
    author_url: Option<String>,
 | 
					 | 
				
			||||||
    color: u32,
 | 
					 | 
				
			||||||
    fields: Json<Vec<EmbedField>>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					 | 
				
			||||||
struct EmbedField {
 | 
					 | 
				
			||||||
    title: String,
 | 
					 | 
				
			||||||
    value: String,
 | 
					 | 
				
			||||||
    inline: bool,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Embed {
 | 
					 | 
				
			||||||
    pub async fn from_id(
 | 
					 | 
				
			||||||
        pool: impl Executor<'_, Database = Database> + Copy,
 | 
					 | 
				
			||||||
        id: u32,
 | 
					 | 
				
			||||||
    ) -> Option<Self> {
 | 
					 | 
				
			||||||
        match sqlx::query_as!(
 | 
					 | 
				
			||||||
            Self,
 | 
					 | 
				
			||||||
            r#"
 | 
					 | 
				
			||||||
            SELECT
 | 
					 | 
				
			||||||
             `embed_title` AS title,
 | 
					 | 
				
			||||||
             `embed_description` AS description,
 | 
					 | 
				
			||||||
             `embed_image_url` AS image_url,
 | 
					 | 
				
			||||||
             `embed_thumbnail_url` AS thumbnail_url,
 | 
					 | 
				
			||||||
             `embed_footer` AS footer,
 | 
					 | 
				
			||||||
             `embed_footer_url` AS footer_url,
 | 
					 | 
				
			||||||
             `embed_author` AS author,
 | 
					 | 
				
			||||||
             `embed_author_url` AS author_url,
 | 
					 | 
				
			||||||
             `embed_color` AS color,
 | 
					 | 
				
			||||||
             IFNULL(`embed_fields`, '[]') AS "fields:_"
 | 
					 | 
				
			||||||
            FROM reminders
 | 
					 | 
				
			||||||
            WHERE `id` = ?"#,
 | 
					 | 
				
			||||||
            id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(mut embed) => {
 | 
					 | 
				
			||||||
                embed.title = substitute(&embed.title);
 | 
					 | 
				
			||||||
                embed.description = substitute(&embed.description);
 | 
					 | 
				
			||||||
                embed.footer = substitute(&embed.footer);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                embed.fields.iter_mut().for_each(|mut field| {
 | 
					 | 
				
			||||||
                    field.title = substitute(&field.title);
 | 
					 | 
				
			||||||
                    field.value = substitute(&field.value);
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if embed.has_content() {
 | 
					 | 
				
			||||||
                    Some(embed)
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    None
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(e) => {
 | 
					 | 
				
			||||||
                warn!("Error loading embed from reminder: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                None
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn has_content(&self) -> bool {
 | 
					 | 
				
			||||||
        if self.title.is_empty()
 | 
					 | 
				
			||||||
            && self.description.is_empty()
 | 
					 | 
				
			||||||
            && self.image_url.is_none()
 | 
					 | 
				
			||||||
            && self.thumbnail_url.is_none()
 | 
					 | 
				
			||||||
            && self.footer.is_empty()
 | 
					 | 
				
			||||||
            && self.footer_url.is_none()
 | 
					 | 
				
			||||||
            && self.author.is_empty()
 | 
					 | 
				
			||||||
            && self.author_url.is_none()
 | 
					 | 
				
			||||||
            && self.fields.0.is_empty()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            false
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            true
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Into<CreateEmbed> for Embed {
 | 
					 | 
				
			||||||
    fn into(self) -> CreateEmbed {
 | 
					 | 
				
			||||||
        let mut c = CreateEmbed::default();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        c.title(&self.title)
 | 
					 | 
				
			||||||
            .description(&self.description)
 | 
					 | 
				
			||||||
            .color(self.color)
 | 
					 | 
				
			||||||
            .author(|a| {
 | 
					 | 
				
			||||||
                a.name(&self.author);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(author_icon) = &self.author_url {
 | 
					 | 
				
			||||||
                    a.icon_url(author_icon);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                a
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .footer(|f| {
 | 
					 | 
				
			||||||
                f.text(&self.footer);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(footer_icon) = &self.footer_url {
 | 
					 | 
				
			||||||
                    f.icon_url(footer_icon);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                f
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for field in &self.fields.0 {
 | 
					 | 
				
			||||||
            c.field(&field.title, &field.value, field.inline);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Some(image_url) = &self.image_url {
 | 
					 | 
				
			||||||
            c.image(image_url);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Some(thumbnail_url) = &self.thumbnail_url {
 | 
					 | 
				
			||||||
            c.thumbnail(thumbnail_url);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        c
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct Reminder {
 | 
					 | 
				
			||||||
    id: u32,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channel_id: u64,
 | 
					 | 
				
			||||||
    webhook_id: Option<u64>,
 | 
					 | 
				
			||||||
    webhook_token: Option<String>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channel_paused: bool,
 | 
					 | 
				
			||||||
    channel_paused_until: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    enabled: bool,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tts: bool,
 | 
					 | 
				
			||||||
    pin: bool,
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
    attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    attachment_name: Option<String>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					 | 
				
			||||||
    timezone: String,
 | 
					 | 
				
			||||||
    restartable: bool,
 | 
					 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    interval_seconds: Option<u32>,
 | 
					 | 
				
			||||||
    interval_months: Option<u32>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    avatar: Option<String>,
 | 
					 | 
				
			||||||
    username: Option<String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Reminder {
 | 
					 | 
				
			||||||
    pub async fn fetch_reminders(pool: impl Executor<'_, Database = Database> + Copy) -> Vec<Self> {
 | 
					 | 
				
			||||||
        match sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Reminder,
 | 
					 | 
				
			||||||
            r#"
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    reminders.`id` AS id,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channels.`channel` AS channel_id,
 | 
					 | 
				
			||||||
    channels.`webhook_id` AS webhook_id,
 | 
					 | 
				
			||||||
    channels.`webhook_token` AS webhook_token,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channels.`paused` AS 'channel_paused',
 | 
					 | 
				
			||||||
    channels.`paused_until` AS 'channel_paused_until',
 | 
					 | 
				
			||||||
    reminders.`enabled` AS 'enabled',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reminders.`tts` AS tts,
 | 
					 | 
				
			||||||
    reminders.`pin` AS pin,
 | 
					 | 
				
			||||||
    reminders.`content` AS content,
 | 
					 | 
				
			||||||
    reminders.`attachment` AS attachment,
 | 
					 | 
				
			||||||
    reminders.`attachment_name` AS attachment_name,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reminders.`utc_time` AS 'utc_time',
 | 
					 | 
				
			||||||
    reminders.`timezone` AS timezone,
 | 
					 | 
				
			||||||
    reminders.`restartable` AS restartable,
 | 
					 | 
				
			||||||
    reminders.`expires` AS 'expires',
 | 
					 | 
				
			||||||
    reminders.`interval_seconds` AS 'interval_seconds',
 | 
					 | 
				
			||||||
    reminders.`interval_months` AS 'interval_months',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reminders.`avatar` AS avatar,
 | 
					 | 
				
			||||||
    reminders.`username` AS username
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
INNER JOIN
 | 
					 | 
				
			||||||
    channels
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.channel_id = channels.id
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    reminders.`utc_time` < NOW()
 | 
					 | 
				
			||||||
LIMIT 25
 | 
					 | 
				
			||||||
            "#,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_all(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(reminders) => reminders
 | 
					 | 
				
			||||||
                .into_iter()
 | 
					 | 
				
			||||||
                .map(|mut rem| {
 | 
					 | 
				
			||||||
                    rem.content = substitute(&rem.content);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    rem
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .collect::<Vec<Self>>(),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(e) => {
 | 
					 | 
				
			||||||
                warn!("Could not fetch reminders: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                vec![]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
					 | 
				
			||||||
        let _ = sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.channel_id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn refresh(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
					 | 
				
			||||||
        if self.interval_seconds.is_some() || self.interval_months.is_some() {
 | 
					 | 
				
			||||||
            let now = Utc::now().naive_local();
 | 
					 | 
				
			||||||
            let mut updated_reminder_time = self.utc_time;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Some(interval) = self.interval_months {
 | 
					 | 
				
			||||||
                match sqlx::query!(
 | 
					 | 
				
			||||||
                    // use the second date_add to force return value to datetime
 | 
					 | 
				
			||||||
                    "SELECT DATE_ADD(DATE_ADD(?, INTERVAL ? MONTH), INTERVAL 0 SECOND) AS new_time",
 | 
					 | 
				
			||||||
                    updated_reminder_time,
 | 
					 | 
				
			||||||
                    interval
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .fetch_one(pool)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    Ok(row) => match row.new_time {
 | 
					 | 
				
			||||||
                        Some(datetime) => {
 | 
					 | 
				
			||||||
                            updated_reminder_time = datetime;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        None => {
 | 
					 | 
				
			||||||
                            warn!("Could not update interval by months: got NULL");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            updated_reminder_time += Duration::days(30);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        warn!("Could not update interval by months: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        // naively fallback to adding 30 days
 | 
					 | 
				
			||||||
                        updated_reminder_time += Duration::days(30);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Some(interval) = self.interval_seconds {
 | 
					 | 
				
			||||||
                while updated_reminder_time < now {
 | 
					 | 
				
			||||||
                    updated_reminder_time += Duration::seconds(interval as i64);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if self.expires.map_or(false, |expires| {
 | 
					 | 
				
			||||||
                NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires
 | 
					 | 
				
			||||||
            }) {
 | 
					 | 
				
			||||||
                self.force_delete(pool).await;
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                sqlx::query!(
 | 
					 | 
				
			||||||
                    "
 | 
					 | 
				
			||||||
UPDATE reminders SET `utc_time` = ? WHERE `id` = ?
 | 
					 | 
				
			||||||
                    ",
 | 
					 | 
				
			||||||
                    updated_reminder_time,
 | 
					 | 
				
			||||||
                    self.id
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .execute(pool)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .expect(&format!("Could not update time on Reminder {}", self.id));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            self.force_delete(pool).await;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn force_delete(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
DELETE FROM reminders WHERE `id` = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .expect(&format!("Could not delete Reminder {}", self.id));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) {
 | 
					 | 
				
			||||||
        let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn send(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        pool: impl Executor<'_, Database = Database> + Copy,
 | 
					 | 
				
			||||||
        cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        async fn send_to_channel(
 | 
					 | 
				
			||||||
            cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
            reminder: &Reminder,
 | 
					 | 
				
			||||||
            embed: Option<CreateEmbed>,
 | 
					 | 
				
			||||||
        ) -> Result<()> {
 | 
					 | 
				
			||||||
            let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            match channel {
 | 
					 | 
				
			||||||
                Ok(Channel::Guild(channel)) => {
 | 
					 | 
				
			||||||
                    match channel
 | 
					 | 
				
			||||||
                        .send_message(&cache_http, |m| {
 | 
					 | 
				
			||||||
                            m.content(&reminder.content).tts(reminder.tts);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let (Some(attachment), Some(name)) =
 | 
					 | 
				
			||||||
                                (&reminder.attachment, &reminder.attachment_name)
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                m.add_file((attachment as &[u8], name.as_str()));
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let Some(embed) = embed {
 | 
					 | 
				
			||||||
                                m.set_embed(embed);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            m
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Ok(m) => {
 | 
					 | 
				
			||||||
                            if reminder.pin {
 | 
					 | 
				
			||||||
                                reminder.pin_message(m.id, cache_http.http()).await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Ok(())
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        Err(e) => Err(e),
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Ok(Channel::Private(channel)) => {
 | 
					 | 
				
			||||||
                    match channel
 | 
					 | 
				
			||||||
                        .send_message(&cache_http.http(), |m| {
 | 
					 | 
				
			||||||
                            m.content(&reminder.content).tts(reminder.tts);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let (Some(attachment), Some(name)) =
 | 
					 | 
				
			||||||
                                (&reminder.attachment, &reminder.attachment_name)
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                m.add_file((attachment as &[u8], name.as_str()));
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let Some(embed) = embed {
 | 
					 | 
				
			||||||
                                m.set_embed(embed);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            m
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Ok(m) => {
 | 
					 | 
				
			||||||
                            if reminder.pin {
 | 
					 | 
				
			||||||
                                reminder.pin_message(m.id, cache_http.http()).await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Ok(())
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        Err(e) => Err(e),
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Err(e) => Err(e),
 | 
					 | 
				
			||||||
                _ => Err(Error::Other("Channel not of valid type")),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        async fn send_to_webhook(
 | 
					 | 
				
			||||||
            cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
            reminder: &Reminder,
 | 
					 | 
				
			||||||
            webhook: Webhook,
 | 
					 | 
				
			||||||
            embed: Option<CreateEmbed>,
 | 
					 | 
				
			||||||
        ) -> Result<()> {
 | 
					 | 
				
			||||||
            match webhook
 | 
					 | 
				
			||||||
                .execute(&cache_http.http(), reminder.pin || reminder.restartable, |w| {
 | 
					 | 
				
			||||||
                    w.content(&reminder.content).tts(reminder.tts);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(username) = &reminder.username {
 | 
					 | 
				
			||||||
                        w.username(username);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(avatar) = &reminder.avatar {
 | 
					 | 
				
			||||||
                        w.avatar_url(avatar);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let (Some(attachment), Some(name)) =
 | 
					 | 
				
			||||||
                        (&reminder.attachment, &reminder.attachment_name)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        w.add_file((attachment as &[u8], name.as_str()));
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(embed) = embed {
 | 
					 | 
				
			||||||
                        w.embeds(vec![SerenityEmbed::fake(|c| {
 | 
					 | 
				
			||||||
                            *c = embed;
 | 
					 | 
				
			||||||
                            c
 | 
					 | 
				
			||||||
                        })]);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    w
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Ok(m) => {
 | 
					 | 
				
			||||||
                    if reminder.pin {
 | 
					 | 
				
			||||||
                        if let Some(message) = m {
 | 
					 | 
				
			||||||
                            reminder.pin_message(message.id, cache_http.http()).await;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Ok(())
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Err(e) => Err(e),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.enabled
 | 
					 | 
				
			||||||
            && !(self.channel_paused
 | 
					 | 
				
			||||||
                && self
 | 
					 | 
				
			||||||
                    .channel_paused_until
 | 
					 | 
				
			||||||
                    .map_or(true, |inner| inner >= Utc::now().naive_local()))
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            let _ = sqlx::query!(
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                self.channel_id
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(pool)
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let embed = Embed::from_id(pool, self.id).await.map(|e| e.into());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let result = if let (Some(webhook_id), Some(webhook_token)) =
 | 
					 | 
				
			||||||
                (self.webhook_id, &self.webhook_token)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                let webhook_res =
 | 
					 | 
				
			||||||
                    cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Ok(webhook) = webhook_res {
 | 
					 | 
				
			||||||
                    send_to_webhook(cache_http, &self, webhook, embed).await
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    warn!("Webhook vanished: {:?}", webhook_res);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    self.reset_webhook(pool).await;
 | 
					 | 
				
			||||||
                    send_to_channel(cache_http, &self, embed).await
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                send_to_channel(cache_http, &self, embed).await
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Err(e) = result {
 | 
					 | 
				
			||||||
                error!("Error sending {:?}: {:?}", self, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Error::Http(error) = e {
 | 
					 | 
				
			||||||
                    if error.status_code() == Some(StatusCode::NOT_FOUND) {
 | 
					 | 
				
			||||||
                        warn!("Seeing channel is deleted. Removing reminder");
 | 
					 | 
				
			||||||
                        self.force_delete(pool).await;
 | 
					 | 
				
			||||||
                    } else if let HttpError::UnsuccessfulRequest(error) = *error {
 | 
					 | 
				
			||||||
                        if error.error.code == 50007 {
 | 
					 | 
				
			||||||
                            warn!("User cannot receive DMs");
 | 
					 | 
				
			||||||
                            self.force_delete(pool).await;
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            self.refresh(pool).await;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    self.refresh(pool).await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                self.refresh(pool).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            info!("Reminder {} is paused", self.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            self.refresh(pool).await;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,11 +1,9 @@
 | 
				
			|||||||
use chrono::offset::Utc;
 | 
					use chrono::offset::Utc;
 | 
				
			||||||
use poise::{serenity_prelude as serenity, serenity_prelude::Mentionable};
 | 
					use poise::serenity::builder::CreateEmbedFooter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{models::CtxData, Context, Error, THEME_COLOR};
 | 
					use crate::{models::CtxData, Context, Error, THEME_COLOR};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn footer(
 | 
					fn footer(ctx: Context<'_>) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					 | 
				
			||||||
) -> impl FnOnce(&mut serenity::CreateEmbedFooter) -> &mut serenity::CreateEmbedFooter {
 | 
					 | 
				
			||||||
    let shard_count = ctx.discord().cache.shard_count();
 | 
					    let shard_count = ctx.discord().cache.shard_count();
 | 
				
			||||||
    let shard = ctx.discord().shard_id;
 | 
					    let shard = ctx.discord().shard_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -24,8 +22,9 @@ fn footer(
 | 
				
			|||||||
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(|m| {
 | 
					    let _ = ctx
 | 
				
			||||||
        m.ephemeral(true).embed(|e| {
 | 
					        .send(|m| {
 | 
				
			||||||
 | 
					            m.embed(|e| {
 | 
				
			||||||
                e.title("Help")
 | 
					                e.title("Help")
 | 
				
			||||||
                    .color(*THEME_COLOR)
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
                    .description(
 | 
					                    .description(
 | 
				
			||||||
@@ -57,7 +56,7 @@ __Advanced Commands__
 | 
				
			|||||||
                    .footer(footer)
 | 
					                    .footer(footer)
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    .await?;
 | 
					        .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -69,9 +68,9 @@ pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    let _ = ctx
 | 
					    let _ = ctx
 | 
				
			||||||
        .send(|m| {
 | 
					        .send(|m| {
 | 
				
			||||||
            m.ephemeral(true).embed(|e| {
 | 
					            m.embed(|e| {
 | 
				
			||||||
                e.title("Info")
 | 
					                e.title("Info")
 | 
				
			||||||
                    .description(
 | 
					                    .description(format!(
 | 
				
			||||||
                        "Help: `/help`
 | 
					                        "Help: `/help`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Welcome to Reminder Bot!**
 | 
					**Welcome to Reminder Bot!**
 | 
				
			||||||
@@ -81,7 +80,7 @@ Find me on https://discord.jellywx.com and on https://github.com/JellyWX :)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Invite the bot: https://invite.reminder-bot.com/
 | 
					Invite the bot: https://invite.reminder-bot.com/
 | 
				
			||||||
Use our dashboard: https://reminder-bot.com/",
 | 
					Use our dashboard: https://reminder-bot.com/",
 | 
				
			||||||
                    )
 | 
					                    ))
 | 
				
			||||||
                    .footer(footer)
 | 
					                    .footer(footer)
 | 
				
			||||||
                    .color(*THEME_COLOR)
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
@@ -96,10 +95,9 @@ Use our dashboard: https://reminder-bot.com/",
 | 
				
			|||||||
pub async fn donate(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn donate(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(|m| m.embed(|e| {
 | 
					    let _ = ctx.send(|m| m.embed(|e| {
 | 
				
			||||||
                e.title("Donate")
 | 
					                e.title("Donate")
 | 
				
			||||||
            .description("Thinking of adding a monthly contribution?
 | 
					                    .description("Thinking of adding a monthly contribution? Click below for my Patreon and official bot server :)
 | 
				
			||||||
Click below for my Patreon and official bot server :)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
**https://www.patreon.com/jellywx/**
 | 
					**https://www.patreon.com/jellywx/**
 | 
				
			||||||
**https://discord.jellywx.com/**
 | 
					**https://discord.jellywx.com/**
 | 
				
			||||||
@@ -118,7 +116,7 @@ Just $2 USD/month!
 | 
				
			|||||||
                    .color(*THEME_COLOR)
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    .await?;
 | 
					        .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -128,20 +126,21 @@ Just $2 USD/month!
 | 
				
			|||||||
pub async fn dashboard(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn dashboard(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(|m| {
 | 
					    let _ = ctx
 | 
				
			||||||
        m.ephemeral(true).embed(|e| {
 | 
					        .send(|m| {
 | 
				
			||||||
 | 
					            m.embed(|e| {
 | 
				
			||||||
                e.title("Dashboard")
 | 
					                e.title("Dashboard")
 | 
				
			||||||
                    .description("**https://reminder-bot.com/dashboard**")
 | 
					                    .description("**https://reminder-bot.com/dashboard**")
 | 
				
			||||||
                    .footer(footer)
 | 
					                    .footer(footer)
 | 
				
			||||||
                    .color(*THEME_COLOR)
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    .await?;
 | 
					        .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// View the current time in your selected timezone
 | 
					/// View the current time in a user's selected timezone
 | 
				
			||||||
#[poise::command(slash_command)]
 | 
					#[poise::command(slash_command)]
 | 
				
			||||||
pub async fn clock(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn clock(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    ctx.defer_ephemeral().await?;
 | 
					    ctx.defer_ephemeral().await?;
 | 
				
			||||||
@@ -156,25 +155,3 @@ pub async fn clock(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
/// View the current time in a user's selected timezone
 | 
					 | 
				
			||||||
#[poise::command(context_menu_command = "View Local Time")]
 | 
					 | 
				
			||||||
pub async fn clock_context_menu(ctx: Context<'_>, user: serenity::User) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    ctx.defer_ephemeral().await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let user_data = ctx.user_data(user.id).await?;
 | 
					 | 
				
			||||||
    let tz = user_data.timezone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let now = Utc::now().with_timezone(&tz);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ctx.send(|m| {
 | 
					 | 
				
			||||||
        m.ephemeral(true).content(format!(
 | 
					 | 
				
			||||||
            "Time in {}'s timezone: `{}`",
 | 
					 | 
				
			||||||
            user.mention(),
 | 
					 | 
				
			||||||
            now.format("%H:%M")
 | 
					 | 
				
			||||||
        ))
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
pub mod info_cmds;
 | 
					pub mod info_cmds;
 | 
				
			||||||
pub mod moderation_cmds;
 | 
					pub mod moderation_cmds;
 | 
				
			||||||
pub mod reminder_cmds;
 | 
					// pub mod reminder_cmds;
 | 
				
			||||||
pub mod todo_cmds;
 | 
					// pub mod todo_cmds;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,13 @@
 | 
				
			|||||||
use std::collections::hash_map::Entry;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::offset::Utc;
 | 
					use chrono::offset::Utc;
 | 
				
			||||||
use chrono_tz::{Tz, TZ_VARIANTS};
 | 
					use chrono_tz::{Tz, TZ_VARIANTS};
 | 
				
			||||||
use levenshtein::levenshtein;
 | 
					use levenshtein::levenshtein;
 | 
				
			||||||
use poise::CreateReply;
 | 
					use poise::CreateReply;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    component_models::pager::{MacroPager, Pager},
 | 
					 | 
				
			||||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
					    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
				
			||||||
 | 
					    hooks::guild_only,
 | 
				
			||||||
    models::{
 | 
					    models::{
 | 
				
			||||||
        command_macro::{guild_command_macro, CommandMacro},
 | 
					        command_macro::{CommandMacro, CommandOptions},
 | 
				
			||||||
        CtxData,
 | 
					        CtxData,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    Context, Data, Error,
 | 
					    Context, Data, Error,
 | 
				
			||||||
@@ -21,7 +19,11 @@ async fn timezone_autocomplete(ctx: Context<'_>, partial: String) -> Vec<String>
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        TZ_VARIANTS
 | 
					        TZ_VARIANTS
 | 
				
			||||||
            .iter()
 | 
					            .iter()
 | 
				
			||||||
            .filter(|tz| tz.to_string().contains(&partial))
 | 
					            .filter(|tz| {
 | 
				
			||||||
 | 
					                partial.contains(&tz.to_string())
 | 
				
			||||||
 | 
					                    || tz.to_string().contains(&partial)
 | 
				
			||||||
 | 
					                    || levenshtein(&tz.to_string(), &partial) < 4
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
            .take(25)
 | 
					            .take(25)
 | 
				
			||||||
            .map(|t| t.to_string())
 | 
					            .map(|t| t.to_string())
 | 
				
			||||||
            .collect::<Vec<String>>()
 | 
					            .collect::<Vec<String>>()
 | 
				
			||||||
@@ -29,7 +31,7 @@ async fn timezone_autocomplete(ctx: Context<'_>, partial: String) -> Vec<String>
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Select your timezone
 | 
					/// Select your timezone
 | 
				
			||||||
#[poise::command(slash_command, identifying_name = "timezone")]
 | 
					#[poise::command(slash_command)]
 | 
				
			||||||
pub async fn timezone(
 | 
					pub async fn timezone(
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"]
 | 
					    #[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"]
 | 
				
			||||||
@@ -54,7 +56,7 @@ pub async fn timezone(
 | 
				
			|||||||
                            .description(format!(
 | 
					                            .description(format!(
 | 
				
			||||||
                                "Timezone has been set to **{}**. Your current time should be `{}`",
 | 
					                                "Timezone has been set to **{}**. Your current time should be `{}`",
 | 
				
			||||||
                                timezone,
 | 
					                                timezone,
 | 
				
			||||||
                                now.format("%H:%M")
 | 
					                                now.format("%H:%M").to_string()
 | 
				
			||||||
                            ))
 | 
					                            ))
 | 
				
			||||||
                            .color(*THEME_COLOR)
 | 
					                            .color(*THEME_COLOR)
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
@@ -77,7 +79,10 @@ pub async fn timezone(
 | 
				
			|||||||
                let fields = filtered_tz.iter().map(|tz| {
 | 
					                let fields = filtered_tz.iter().map(|tz| {
 | 
				
			||||||
                    (
 | 
					                    (
 | 
				
			||||||
                        tz.to_string(),
 | 
					                        tz.to_string(),
 | 
				
			||||||
                        format!("🕗 `{}`", Utc::now().with_timezone(tz).format("%H:%M")),
 | 
					                        format!(
 | 
				
			||||||
 | 
					                            "🕗 `{}`",
 | 
				
			||||||
 | 
					                            Utc::now().with_timezone(tz).format("%H:%M").to_string()
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
                        true,
 | 
					                        true,
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
@@ -97,7 +102,11 @@ pub async fn timezone(
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
 | 
					        let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
 | 
				
			||||||
            (t.to_string(), format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M")), true)
 | 
					            (
 | 
				
			||||||
 | 
					                t.to_string(),
 | 
				
			||||||
 | 
					                format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M").to_string()),
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ctx.send(|m| {
 | 
					        ctx.send(|m| {
 | 
				
			||||||
@@ -137,32 +146,20 @@ WHERE
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    .fetch_all(&ctx.data().database)
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
    .unwrap_or_default()
 | 
					    .unwrap_or(vec![])
 | 
				
			||||||
    .iter()
 | 
					    .iter()
 | 
				
			||||||
    .map(|s| s.name.clone())
 | 
					    .map(|s| s.name.clone())
 | 
				
			||||||
    .collect()
 | 
					    .collect()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Record and replay command sequences
 | 
					/// Record and replay command sequences
 | 
				
			||||||
#[poise::command(
 | 
					#[poise::command(slash_command, rename = "macro", check = "guild_only")]
 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "macro",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					 | 
				
			||||||
    identifying_name = "macro_base"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn macro_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn macro_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Start recording up to 5 commands to replay
 | 
					/// Start recording up to 5 commands to replay
 | 
				
			||||||
#[poise::command(
 | 
					#[poise::command(slash_command, rename = "record", check = "guild_only")]
 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "record",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					 | 
				
			||||||
    identifying_name = "record_macro"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn record_macro(
 | 
					pub async fn record_macro(
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "Name for the new macro"] name: String,
 | 
					    #[description = "Name for the new macro"] name: String,
 | 
				
			||||||
@@ -195,11 +192,14 @@ Please select a unique name for your macro.",
 | 
				
			|||||||
        let okay = {
 | 
					        let okay = {
 | 
				
			||||||
            let mut lock = ctx.data().recording_macros.write().await;
 | 
					            let mut lock = ctx.data().recording_macros.write().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if let Entry::Vacant(e) = lock.entry((guild_id, ctx.author().id)) {
 | 
					            if lock.contains_key(&(guild_id, ctx.author().id)) {
 | 
				
			||||||
                e.insert(CommandMacro { guild_id, name, description, commands: vec![] });
 | 
					 | 
				
			||||||
                true
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                false
 | 
					                false
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                lock.insert(
 | 
				
			||||||
 | 
					                    (guild_id, ctx.author().id),
 | 
				
			||||||
 | 
					                    CommandMacro { guild_id, name, description, commands: vec![] },
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                true
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -237,9 +237,8 @@ Please use `/macro finish` to end this recording before starting another.",
 | 
				
			|||||||
#[poise::command(
 | 
					#[poise::command(
 | 
				
			||||||
    slash_command,
 | 
					    slash_command,
 | 
				
			||||||
    rename = "finish",
 | 
					    rename = "finish",
 | 
				
			||||||
    guild_only = true,
 | 
					    check = "guild_only",
 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					    identifying_name = "macro_finish"
 | 
				
			||||||
    identifying_name = "finish_macro"
 | 
					 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    let key = (ctx.guild_id().unwrap(), ctx.author().id);
 | 
					    let key = (ctx.guild_id().unwrap(), ctx.author().id);
 | 
				
			||||||
@@ -292,15 +291,9 @@ pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// List recorded macros
 | 
					/// List recorded macros
 | 
				
			||||||
#[poise::command(
 | 
					#[poise::command(slash_command, rename = "list", check = "guild_only")]
 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "list",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					 | 
				
			||||||
    identifying_name = "list_macro"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    let macros = ctx.command_macros().await?;
 | 
					    let macros = CommandMacro::from_guild(&ctx.data().database, ctx.guild_id().unwrap()).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let resp = show_macro_page(¯os, 0);
 | 
					    let resp = show_macro_page(¯os, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -313,44 +306,72 @@ pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			|||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn find_command<'a>(
 | 
				
			||||||
 | 
					    commands: &'a [poise::Command<Data, Error>],
 | 
				
			||||||
 | 
					    searching_name: &str,
 | 
				
			||||||
 | 
					    command_options: &CommandOptions,
 | 
				
			||||||
 | 
					) -> Option<&'a poise::Command<Data, Error>> {
 | 
				
			||||||
 | 
					    commands.iter().find_map(|cmd| {
 | 
				
			||||||
 | 
					        if searching_name != cmd.name {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            if let Some(subgroup) = &command_options.subcommand_group {
 | 
				
			||||||
 | 
					                find_command(&cmd.subcommands, &subgroup, &command_options)
 | 
				
			||||||
 | 
					            } else if let Some(subcommand) = &command_options.subcommand {
 | 
				
			||||||
 | 
					                find_command(&cmd.subcommands, &subcommand, &command_options)
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Some(cmd)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Run a recorded macro
 | 
					/// Run a recorded macro
 | 
				
			||||||
#[poise::command(
 | 
					#[poise::command(slash_command, rename = "run", check = "guild_only")]
 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "run",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					 | 
				
			||||||
    identifying_name = "run_macro"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn run_macro(
 | 
					pub async fn run_macro(
 | 
				
			||||||
    ctx: poise::ApplicationContext<'_, Data, Error>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "Name of macro to run"]
 | 
					    #[description = "Name of macro to run"]
 | 
				
			||||||
    #[autocomplete = "macro_name_autocomplete"]
 | 
					    #[autocomplete = "macro_name_autocomplete"]
 | 
				
			||||||
    name: String,
 | 
					    name: String,
 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    match guild_command_macro(&Context::Application(ctx), &name).await {
 | 
					    match sqlx::query!(
 | 
				
			||||||
        Some(command_macro) => {
 | 
					        "
 | 
				
			||||||
            ctx.defer_response(false).await?;
 | 
					SELECT commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
				
			||||||
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
            for command in command_macro.commands {
 | 
					        name
 | 
				
			||||||
                if let Some(action) = command.action {
 | 
					    )
 | 
				
			||||||
                    match (action)(poise::ApplicationContext { args: &command.options, ..ctx })
 | 
					    .fetch_one(&ctx.data().database)
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
                        Ok(()) => {}
 | 
					        Ok(row) => {
 | 
				
			||||||
                        Err(e) => {
 | 
					            ctx.defer().await?;
 | 
				
			||||||
                            println!("{:?}", e);
 | 
					
 | 
				
			||||||
                        }
 | 
					            let commands: Vec<CommandOptions> = serde_json::from_str(&row.commands)?;
 | 
				
			||||||
                    }
 | 
					
 | 
				
			||||||
 | 
					            for command in commands {
 | 
				
			||||||
 | 
					                let cmd =
 | 
				
			||||||
 | 
					                    find_command(&ctx.framework().options().commands, &command.command, &command);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Some(cmd) = cmd {
 | 
				
			||||||
 | 
					                    let mut executing_ctx = ctx.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    executing_ctx.command = cmd;
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    Context::Application(ctx)
 | 
					                    ctx.send(|m| {
 | 
				
			||||||
                        .say(format!("Command \"{}\" not found", command.command_name))
 | 
					                        m.ephemeral(true)
 | 
				
			||||||
 | 
					                            .content(format!("Command `{}` not found", command.command))
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
                    .await?;
 | 
					                    .await?;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        None => {
 | 
					        Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
            Context::Application(ctx).say(format!("Macro \"{}\" not found", name)).await?;
 | 
					            ctx.say(format!("Macro \"{}\" not found", name)).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            panic!("{}", e);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -358,13 +379,7 @@ pub async fn run_macro(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Delete a recorded macro
 | 
					/// Delete a recorded macro
 | 
				
			||||||
#[poise::command(
 | 
					#[poise::command(slash_command, rename = "delete", check = "guild_only")]
 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "delete",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					 | 
				
			||||||
    identifying_name = "delete_macro"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn delete_macro(
 | 
					pub async fn delete_macro(
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "Name of macro to delete"]
 | 
					    #[description = "Name of macro to delete"]
 | 
				
			||||||
@@ -401,7 +416,7 @@ SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AN
 | 
				
			|||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize {
 | 
					pub fn max_macro_page(macros: &[CommandMacro]) -> usize {
 | 
				
			||||||
    let mut skipped_char_count = 0;
 | 
					    let mut skipped_char_count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    macros
 | 
					    macros
 | 
				
			||||||
@@ -425,7 +440,18 @@ pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply {
 | 
					pub fn show_macro_page(macros: &[CommandMacro], page: usize) -> CreateReply {
 | 
				
			||||||
 | 
					    let mut reply = CreateReply::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reply.embed(|e| {
 | 
				
			||||||
 | 
					        e.title("Macros")
 | 
				
			||||||
 | 
					            .description("No Macros Set Up. Use `/macro record` to get started.")
 | 
				
			||||||
 | 
					            .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reply
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /*
 | 
				
			||||||
    let pager = MacroPager::new(page);
 | 
					    let pager = MacroPager::new(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if macros.is_empty() {
 | 
					    if macros.is_empty() {
 | 
				
			||||||
@@ -496,4 +522,5 @@ pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> Crea
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    reply
 | 
					    reply
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
use poise::CreateReply;
 | 
					use regex_command_attr::command;
 | 
				
			||||||
 | 
					use serenity::client::Context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    component_models::{
 | 
					    component_models::{
 | 
				
			||||||
@@ -6,218 +7,134 @@ use crate::{
 | 
				
			|||||||
        ComponentDataModel, TodoSelector,
 | 
					        ComponentDataModel, TodoSelector,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
 | 
					    consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
 | 
				
			||||||
    Context, Error,
 | 
					    framework::{CommandInvoke, CommandOptions, CreateGenericResponse},
 | 
				
			||||||
 | 
					    hooks::CHECK_GUILD_PERMISSIONS_HOOK,
 | 
				
			||||||
 | 
					    SQLPool,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Manage todo lists
 | 
					#[command]
 | 
				
			||||||
#[poise::command(
 | 
					#[description("Manage todo lists")]
 | 
				
			||||||
    slash_command,
 | 
					#[subcommandgroup("server")]
 | 
				
			||||||
    rename = "todo",
 | 
					#[description("Manage the server todo list")]
 | 
				
			||||||
    identifying_name = "todo_base",
 | 
					#[subcommand("add")]
 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD"
 | 
					#[description("Add an item to the server todo list")]
 | 
				
			||||||
 | 
					#[arg(
 | 
				
			||||||
 | 
					    name = "task",
 | 
				
			||||||
 | 
					    description = "The task to add to the todo list",
 | 
				
			||||||
 | 
					    kind = "String",
 | 
				
			||||||
 | 
					    required = true
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
pub async fn todo_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
					#[subcommand("view")]
 | 
				
			||||||
    Ok(())
 | 
					#[description("View and remove from the server todo list")]
 | 
				
			||||||
}
 | 
					#[subcommandgroup("channel")]
 | 
				
			||||||
 | 
					#[description("Manage the channel todo list")]
 | 
				
			||||||
 | 
					#[subcommand("add")]
 | 
				
			||||||
 | 
					#[description("Add to the channel todo list")]
 | 
				
			||||||
 | 
					#[arg(
 | 
				
			||||||
 | 
					    name = "task",
 | 
				
			||||||
 | 
					    description = "The task to add to the todo list",
 | 
				
			||||||
 | 
					    kind = "String",
 | 
				
			||||||
 | 
					    required = true
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[subcommand("view")]
 | 
				
			||||||
 | 
					#[description("View and remove from the channel todo list")]
 | 
				
			||||||
 | 
					#[subcommandgroup("user")]
 | 
				
			||||||
 | 
					#[description("Manage your personal todo list")]
 | 
				
			||||||
 | 
					#[subcommand("add")]
 | 
				
			||||||
 | 
					#[description("Add to your personal todo list")]
 | 
				
			||||||
 | 
					#[arg(
 | 
				
			||||||
 | 
					    name = "task",
 | 
				
			||||||
 | 
					    description = "The task to add to the todo list",
 | 
				
			||||||
 | 
					    kind = "String",
 | 
				
			||||||
 | 
					    required = true
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[subcommand("view")]
 | 
				
			||||||
 | 
					#[description("View and remove from your personal todo list")]
 | 
				
			||||||
 | 
					#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
 | 
				
			||||||
 | 
					async fn todo(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
 | 
				
			||||||
 | 
					    if invoke.guild_id().is_none() && args.subcommand_group != Some("user".to_string()) {
 | 
				
			||||||
 | 
					        let _ = invoke
 | 
				
			||||||
 | 
					            .respond(
 | 
				
			||||||
 | 
					                &ctx,
 | 
				
			||||||
 | 
					                CreateGenericResponse::new().content("Please use `/todo user` in direct messages"),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Manage the server todo list
 | 
					        let keys = match args.subcommand_group.as_ref().unwrap().as_str() {
 | 
				
			||||||
#[poise::command(
 | 
					            "server" => (None, None, invoke.guild_id().map(|g| g.0)),
 | 
				
			||||||
    slash_command,
 | 
					            "channel" => (None, Some(invoke.channel_id().0), invoke.guild_id().map(|g| g.0)),
 | 
				
			||||||
    rename = "server",
 | 
					            _ => (Some(invoke.author_id().0), None, None),
 | 
				
			||||||
    guild_only = true,
 | 
					        };
 | 
				
			||||||
    identifying_name = "todo_guild_base",
 | 
					
 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD"
 | 
					        match args.get("task") {
 | 
				
			||||||
)]
 | 
					            Some(task) => {
 | 
				
			||||||
pub async fn todo_guild_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
					                let task = task.to_string();
 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Add an item to the server todo list
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "add",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    identifying_name = "todo_guild_add",
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn todo_guild_add(
 | 
					 | 
				
			||||||
    ctx: Context<'_>,
 | 
					 | 
				
			||||||
    #[description = "The task to add to the todo list"] task: String,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
                sqlx::query!(
 | 
					                sqlx::query!(
 | 
				
			||||||
        "INSERT INTO todos (guild_id, value)
 | 
					                    "INSERT INTO todos (user_id, channel_id, guild_id, value) VALUES ((SELECT id FROM users WHERE user = ?), (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?), ?)",
 | 
				
			||||||
VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
 | 
					                    keys.0,
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					                    keys.1,
 | 
				
			||||||
 | 
					                    keys.2,
 | 
				
			||||||
                    task
 | 
					                    task
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
    .execute(&ctx.data().database)
 | 
					                .execute(&pool)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .unwrap();
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.say("Item added to todo list").await?;
 | 
					                let _ = invoke
 | 
				
			||||||
 | 
					                    .respond(&ctx, CreateGenericResponse::new().content("Item added to todo list"))
 | 
				
			||||||
    Ok(())
 | 
					                    .await;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            None => {
 | 
				
			||||||
/// View and remove from the server todo list
 | 
					                let values = if let Some(uid) = keys.0 {
 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "view",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    identifying_name = "todo_guild_view",
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let values = sqlx::query!(
 | 
					 | 
				
			||||||
        "SELECT todos.id, value FROM todos
 | 
					 | 
				
			||||||
INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
					 | 
				
			||||||
WHERE guilds.guild = ?",
 | 
					 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_all(&ctx.data().database)
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    .unwrap()
 | 
					 | 
				
			||||||
    .iter()
 | 
					 | 
				
			||||||
    .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
    .collect::<Vec<(usize, String)>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let resp = show_todo_page(&values, 0, None, None, ctx.guild_id().map(|g| g.0));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ctx.send(|r| {
 | 
					 | 
				
			||||||
        *r = resp;
 | 
					 | 
				
			||||||
        r
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Manage the channel todo list
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "channel",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    identifying_name = "todo_channel_base",
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn todo_channel_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Add an item to the channel todo list
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "add",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    identifying_name = "todo_channel_add",
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn todo_channel_add(
 | 
					 | 
				
			||||||
    ctx: Context<'_>,
 | 
					 | 
				
			||||||
    #[description = "The task to add to the todo list"] task: String,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
                    sqlx::query!(
 | 
					                    sqlx::query!(
 | 
				
			||||||
        "INSERT INTO todos (guild_id, channel_id, value)
 | 
					 | 
				
			||||||
VALUES ((SELECT id FROM guilds WHERE guild = ?), (SELECT id FROM channels WHERE channel = ?), ?)",
 | 
					 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					 | 
				
			||||||
        ctx.channel_id().0,
 | 
					 | 
				
			||||||
        task
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .execute(&ctx.data().database)
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ctx.say("Item added to todo list").await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// View and remove from the channel todo list
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "view",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    identifying_name = "todo_channel_view",
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn todo_channel_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let values = sqlx::query!(
 | 
					 | 
				
			||||||
        "SELECT todos.id, value FROM todos
 | 
					 | 
				
			||||||
INNER JOIN channels ON todos.channel_id = channels.id
 | 
					 | 
				
			||||||
WHERE channels.channel = ?",
 | 
					 | 
				
			||||||
        ctx.channel_id().0,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_all(&ctx.data().database)
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    .unwrap()
 | 
					 | 
				
			||||||
    .iter()
 | 
					 | 
				
			||||||
    .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
    .collect::<Vec<(usize, String)>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let resp =
 | 
					 | 
				
			||||||
        show_todo_page(&values, 0, None, Some(ctx.channel_id().0), ctx.guild_id().map(|g| g.0));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ctx.send(|r| {
 | 
					 | 
				
			||||||
        *r = resp;
 | 
					 | 
				
			||||||
        r
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Manage your personal todo list
 | 
					 | 
				
			||||||
#[poise::command(slash_command, rename = "user", identifying_name = "todo_user_base")]
 | 
					 | 
				
			||||||
pub async fn todo_user_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Add an item to your personal todo list
 | 
					 | 
				
			||||||
#[poise::command(slash_command, rename = "add", identifying_name = "todo_user_add")]
 | 
					 | 
				
			||||||
pub async fn todo_user_add(
 | 
					 | 
				
			||||||
    ctx: Context<'_>,
 | 
					 | 
				
			||||||
    #[description = "The task to add to the todo list"] task: String,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    sqlx::query!(
 | 
					 | 
				
			||||||
        "INSERT INTO todos (user_id, value)
 | 
					 | 
				
			||||||
VALUES ((SELECT id FROM users WHERE user = ?), ?)",
 | 
					 | 
				
			||||||
        ctx.author().id.0,
 | 
					 | 
				
			||||||
        task
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .execute(&ctx.data().database)
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ctx.say("Item added to todo list").await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// View and remove from your personal todo list
 | 
					 | 
				
			||||||
#[poise::command(slash_command, rename = "view", identifying_name = "todo_user_view")]
 | 
					 | 
				
			||||||
pub async fn todo_user_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let values = sqlx::query!(
 | 
					 | 
				
			||||||
                        "SELECT todos.id, value FROM todos
 | 
					                        "SELECT todos.id, value FROM todos
 | 
				
			||||||
INNER JOIN users ON todos.user_id = users.id
 | 
					INNER JOIN users ON todos.user_id = users.id
 | 
				
			||||||
WHERE users.user = ?",
 | 
					WHERE users.user = ?",
 | 
				
			||||||
        ctx.author().id.0,
 | 
					                        uid,
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
    .fetch_all(&ctx.data().database)
 | 
					                    .fetch_all(&pool)
 | 
				
			||||||
                    .await
 | 
					                    .await
 | 
				
			||||||
                    .unwrap()
 | 
					                    .unwrap()
 | 
				
			||||||
                    .iter()
 | 
					                    .iter()
 | 
				
			||||||
                    .map(|row| (row.id as usize, row.value.clone()))
 | 
					                    .map(|row| (row.id as usize, row.value.clone()))
 | 
				
			||||||
    .collect::<Vec<(usize, String)>>();
 | 
					                    .collect::<Vec<(usize, String)>>()
 | 
				
			||||||
 | 
					                } else if let Some(cid) = keys.1 {
 | 
				
			||||||
 | 
					                    sqlx::query!(
 | 
				
			||||||
 | 
					                        "SELECT todos.id, value FROM todos
 | 
				
			||||||
 | 
					INNER JOIN channels ON todos.channel_id = channels.id
 | 
				
			||||||
 | 
					WHERE channels.channel = ?",
 | 
				
			||||||
 | 
					                        cid,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .fetch_all(&pool)
 | 
				
			||||||
 | 
					                    .await
 | 
				
			||||||
 | 
					                    .unwrap()
 | 
				
			||||||
 | 
					                    .iter()
 | 
				
			||||||
 | 
					                    .map(|row| (row.id as usize, row.value.clone()))
 | 
				
			||||||
 | 
					                    .collect::<Vec<(usize, String)>>()
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    sqlx::query!(
 | 
				
			||||||
 | 
					                        "SELECT todos.id, value FROM todos
 | 
				
			||||||
 | 
					INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
				
			||||||
 | 
					WHERE guilds.guild = ?",
 | 
				
			||||||
 | 
					                        keys.2,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .fetch_all(&pool)
 | 
				
			||||||
 | 
					                    .await
 | 
				
			||||||
 | 
					                    .unwrap()
 | 
				
			||||||
 | 
					                    .iter()
 | 
				
			||||||
 | 
					                    .map(|row| (row.id as usize, row.value.clone()))
 | 
				
			||||||
 | 
					                    .collect::<Vec<(usize, String)>>()
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let resp = show_todo_page(&values, 0, Some(ctx.author().id.0), None, None);
 | 
					                let resp = show_todo_page(&values, 0, keys.0, keys.1, keys.2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(|r| {
 | 
					                invoke.respond(&ctx, resp).await.unwrap();
 | 
				
			||||||
        *r = resp;
 | 
					            }
 | 
				
			||||||
        r
 | 
					        }
 | 
				
			||||||
    })
 | 
					    }
 | 
				
			||||||
    .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn max_todo_page(todo_values: &[(usize, String)]) -> usize {
 | 
					pub fn max_todo_page(todo_values: &[(usize, String)]) -> usize {
 | 
				
			||||||
@@ -247,7 +164,7 @@ pub fn show_todo_page(
 | 
				
			|||||||
    user_id: Option<u64>,
 | 
					    user_id: Option<u64>,
 | 
				
			||||||
    channel_id: Option<u64>,
 | 
					    channel_id: Option<u64>,
 | 
				
			||||||
    guild_id: Option<u64>,
 | 
					    guild_id: Option<u64>,
 | 
				
			||||||
) -> CreateReply {
 | 
					) -> CreateGenericResponse {
 | 
				
			||||||
    let pager = TodoPager::new(page, user_id, channel_id, guild_id);
 | 
					    let pager = TodoPager::new(page, user_id, channel_id, guild_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let pages = max_todo_page(todo_values);
 | 
					    let pages = max_todo_page(todo_values);
 | 
				
			||||||
@@ -302,23 +219,17 @@ pub fn show_todo_page(
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if todo_ids.is_empty() {
 | 
					    if todo_ids.is_empty() {
 | 
				
			||||||
        let mut reply = CreateReply::default();
 | 
					        CreateGenericResponse::new().embed(|e| {
 | 
				
			||||||
 | 
					 | 
				
			||||||
        reply.embed(|e| {
 | 
					 | 
				
			||||||
            e.title(format!("{} Todo List", title))
 | 
					            e.title(format!("{} Todo List", title))
 | 
				
			||||||
                .description("Todo List Empty!")
 | 
					                .description("Todo List Empty!")
 | 
				
			||||||
                .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
					                .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
        });
 | 
					        })
 | 
				
			||||||
 | 
					 | 
				
			||||||
        reply
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        let todo_selector =
 | 
					        let todo_selector =
 | 
				
			||||||
            ComponentDataModel::TodoSelector(TodoSelector { page, user_id, channel_id, guild_id });
 | 
					            ComponentDataModel::TodoSelector(TodoSelector { page, user_id, channel_id, guild_id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut reply = CreateReply::default();
 | 
					        CreateGenericResponse::new()
 | 
				
			||||||
 | 
					 | 
				
			||||||
        reply
 | 
					 | 
				
			||||||
            .embed(|e| {
 | 
					            .embed(|e| {
 | 
				
			||||||
                e.title(format!("{} Todo List", title))
 | 
					                e.title(format!("{} Todo List", title))
 | 
				
			||||||
                    .description(display)
 | 
					                    .description(display)
 | 
				
			||||||
@@ -336,7 +247,7 @@ pub fn show_todo_page(
 | 
				
			|||||||
                                opt.create_option(|o| {
 | 
					                                opt.create_option(|o| {
 | 
				
			||||||
                                    o.label(format!("Mark {} complete", count + first_num))
 | 
					                                    o.label(format!("Mark {} complete", count + first_num))
 | 
				
			||||||
                                        .value(id)
 | 
					                                        .value(id)
 | 
				
			||||||
                                        .description(disp.split_once(' ').unwrap_or(("", "")).1)
 | 
					                                        .description(disp.split_once(" ").unwrap_or(("", "")).1)
 | 
				
			||||||
                                });
 | 
					                                });
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -344,8 +255,6 @@ pub fn show_todo_page(
 | 
				
			|||||||
                        })
 | 
					                        })
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
            });
 | 
					            })
 | 
				
			||||||
 | 
					 | 
				
			||||||
        reply
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,35 +3,23 @@ pub(crate) mod pager;
 | 
				
			|||||||
use std::io::Cursor;
 | 
					use std::io::Cursor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use log::warn;
 | 
					use poise::serenity::{
 | 
				
			||||||
use poise::{
 | 
					 | 
				
			||||||
    serenity::{
 | 
					 | 
				
			||||||
    builder::CreateEmbed,
 | 
					    builder::CreateEmbed,
 | 
				
			||||||
    client::Context,
 | 
					    client::Context,
 | 
				
			||||||
    model::{
 | 
					    model::{
 | 
				
			||||||
        channel::Channel,
 | 
					        channel::Channel,
 | 
				
			||||||
            interactions::{
 | 
					        interactions::{message_component::MessageComponentInteraction, InteractionResponseType},
 | 
				
			||||||
                message_component::MessageComponentInteraction, InteractionResponseType,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        prelude::InteractionApplicationCommandCallbackDataFlags,
 | 
					        prelude::InteractionApplicationCommandCallbackDataFlags,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    serenity_prelude as serenity,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use rmp_serde::Serializer;
 | 
					use rmp_serde::Serializer;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::{
 | 
					    self,
 | 
				
			||||||
        moderation_cmds::{max_macro_page, show_macro_page},
 | 
					 | 
				
			||||||
        reminder_cmds::{max_delete_page, show_delete_page},
 | 
					 | 
				
			||||||
        todo_cmds::{max_todo_page, show_todo_page},
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager},
 | 
					    component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager},
 | 
				
			||||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
					    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
				
			||||||
    models::reminder::Reminder,
 | 
					    models::{command_macro::CommandMacro, reminder::Reminder},
 | 
				
			||||||
    utils::send_as_initial_response,
 | 
					 | 
				
			||||||
    Data,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Deserialize, Serialize)]
 | 
					#[derive(Deserialize, Serialize)]
 | 
				
			||||||
@@ -44,7 +32,6 @@ pub enum ComponentDataModel {
 | 
				
			|||||||
    DelSelector(DelSelector),
 | 
					    DelSelector(DelSelector),
 | 
				
			||||||
    TodoSelector(TodoSelector),
 | 
					    TodoSelector(TodoSelector),
 | 
				
			||||||
    MacroPager(MacroPager),
 | 
					    MacroPager(MacroPager),
 | 
				
			||||||
    UndoReminder(UndoReminder),
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ComponentDataModel {
 | 
					impl ComponentDataModel {
 | 
				
			||||||
@@ -62,7 +49,7 @@ impl ComponentDataModel {
 | 
				
			|||||||
        rmp_serde::from_read(cur).unwrap()
 | 
					        rmp_serde::from_read(cur).unwrap()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn act(&self, ctx: &Context, data: &Data, component: &MessageComponentInteraction) {
 | 
					    pub async fn act(&self, ctx: &Context, component: MessageComponentInteraction) {
 | 
				
			||||||
        match self {
 | 
					        match self {
 | 
				
			||||||
            ComponentDataModel::LookPager(pager) => {
 | 
					            ComponentDataModel::LookPager(pager) => {
 | 
				
			||||||
                let flags = pager.flags;
 | 
					                let flags = pager.flags;
 | 
				
			||||||
@@ -79,7 +66,7 @@ impl ComponentDataModel {
 | 
				
			|||||||
                    component.channel_id
 | 
					                    component.channel_id
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let reminders = Reminder::from_channel(&data.database, channel_id, &flags).await;
 | 
					                let reminders = Reminder::from_channel(ctx, channel_id, &flags).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let pages = reminders
 | 
					                let pages = reminders
 | 
				
			||||||
                    .iter()
 | 
					                    .iter()
 | 
				
			||||||
@@ -129,7 +116,7 @@ impl ComponentDataModel {
 | 
				
			|||||||
                    .create_interaction_response(&ctx, |r| {
 | 
					                    .create_interaction_response(&ctx, |r| {
 | 
				
			||||||
                        r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
					                        r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
				
			||||||
                            |response| {
 | 
					                            |response| {
 | 
				
			||||||
                                response.set_embeds(vec![embed]).components(|comp| {
 | 
					                                response.embeds(vec![embed]).components(|comp| {
 | 
				
			||||||
                                    pager.create_button_row(pages, comp);
 | 
					                                    pager.create_button_row(pages, comp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                    comp
 | 
					                                    comp
 | 
				
			||||||
@@ -140,60 +127,37 @@ impl ComponentDataModel {
 | 
				
			|||||||
                    .await;
 | 
					                    .await;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ComponentDataModel::DelPager(pager) => {
 | 
					            ComponentDataModel::DelPager(pager) => {
 | 
				
			||||||
                let reminders = Reminder::from_guild(
 | 
					                let reminders =
 | 
				
			||||||
                    &ctx,
 | 
					                    Reminder::from_guild(ctx, component.guild_id, component.user.id).await;
 | 
				
			||||||
                    &data.database,
 | 
					 | 
				
			||||||
                    component.guild_id,
 | 
					 | 
				
			||||||
                    component.user.id,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let max_pages = max_delete_page(&reminders, &pager.timezone);
 | 
					                let max_pages = max_delete_page(&reminders, &pager.timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let resp = show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone);
 | 
					                let resp = show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let _ = component
 | 
					                let mut invoke = CommandInvoke::component(component);
 | 
				
			||||||
                    .create_interaction_response(&ctx, |f| {
 | 
					                let _ = invoke.respond(&ctx, resp).await;
 | 
				
			||||||
                        f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
					 | 
				
			||||||
                            |d| {
 | 
					 | 
				
			||||||
                                send_as_initial_response(resp, d);
 | 
					 | 
				
			||||||
                                d
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .await;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ComponentDataModel::DelSelector(selector) => {
 | 
					            ComponentDataModel::DelSelector(selector) => {
 | 
				
			||||||
 | 
					                let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
				
			||||||
                let selected_id = component.data.values.join(",");
 | 
					                let selected_id = component.data.values.join(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                sqlx::query!("DELETE FROM reminders WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
					                sqlx::query!("DELETE FROM reminders WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
				
			||||||
                    .execute(&data.database)
 | 
					                    .execute(&pool)
 | 
				
			||||||
                    .await
 | 
					                    .await
 | 
				
			||||||
                    .unwrap();
 | 
					                    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let reminders = Reminder::from_guild(
 | 
					                let reminders =
 | 
				
			||||||
                    &ctx,
 | 
					                    Reminder::from_guild(ctx, component.guild_id, component.user.id).await;
 | 
				
			||||||
                    &data.database,
 | 
					 | 
				
			||||||
                    component.guild_id,
 | 
					 | 
				
			||||||
                    component.user.id,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let resp = show_delete_page(&reminders, selector.page, selector.timezone);
 | 
					                let resp = show_delete_page(&reminders, selector.page, selector.timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let _ = component
 | 
					                let mut invoke = CommandInvoke::component(component);
 | 
				
			||||||
                    .create_interaction_response(&ctx, |f| {
 | 
					                let _ = invoke.respond(&ctx, resp).await;
 | 
				
			||||||
                        f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
					 | 
				
			||||||
                            |d| {
 | 
					 | 
				
			||||||
                                send_as_initial_response(resp, d);
 | 
					 | 
				
			||||||
                                d
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .await;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ComponentDataModel::TodoPager(pager) => {
 | 
					            ComponentDataModel::TodoPager(pager) => {
 | 
				
			||||||
                if Some(component.user.id.0) == pager.user_id || pager.user_id.is_none() {
 | 
					                if Some(component.user.id.0) == pager.user_id || pager.user_id.is_none() {
 | 
				
			||||||
 | 
					                    let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    let values = if let Some(uid) = pager.user_id {
 | 
					                    let values = if let Some(uid) = pager.user_id {
 | 
				
			||||||
                        sqlx::query!(
 | 
					                        sqlx::query!(
 | 
				
			||||||
                            "SELECT todos.id, value FROM todos
 | 
					                            "SELECT todos.id, value FROM todos
 | 
				
			||||||
@@ -201,7 +165,7 @@ INNER JOIN users ON todos.user_id = users.id
 | 
				
			|||||||
    WHERE users.user = ?",
 | 
					    WHERE users.user = ?",
 | 
				
			||||||
                            uid,
 | 
					                            uid,
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .fetch_all(&data.database)
 | 
					                        .fetch_all(&pool)
 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .unwrap()
 | 
					                        .unwrap()
 | 
				
			||||||
                        .iter()
 | 
					                        .iter()
 | 
				
			||||||
@@ -214,7 +178,7 @@ INNER JOIN channels ON todos.channel_id = channels.id
 | 
				
			|||||||
    WHERE channels.channel = ?",
 | 
					    WHERE channels.channel = ?",
 | 
				
			||||||
                            cid,
 | 
					                            cid,
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .fetch_all(&data.database)
 | 
					                        .fetch_all(&pool)
 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .unwrap()
 | 
					                        .unwrap()
 | 
				
			||||||
                        .iter()
 | 
					                        .iter()
 | 
				
			||||||
@@ -227,7 +191,7 @@ INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
				
			|||||||
    WHERE guilds.guild = ?",
 | 
					    WHERE guilds.guild = ?",
 | 
				
			||||||
                            pager.guild_id,
 | 
					                            pager.guild_id,
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .fetch_all(&data.database)
 | 
					                        .fetch_all(&pool)
 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .unwrap()
 | 
					                        .unwrap()
 | 
				
			||||||
                        .iter()
 | 
					                        .iter()
 | 
				
			||||||
@@ -245,15 +209,8 @@ WHERE guilds.guild = ?",
 | 
				
			|||||||
                        pager.guild_id,
 | 
					                        pager.guild_id,
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    let _ = component
 | 
					                    let mut invoke = CommandInvoke::component(component);
 | 
				
			||||||
                        .create_interaction_response(&ctx, |f| {
 | 
					                    let _ = invoke.respond(&ctx, resp).await;
 | 
				
			||||||
                            f.kind(InteractionResponseType::UpdateMessage)
 | 
					 | 
				
			||||||
                                .interaction_response_data(|d| {
 | 
					 | 
				
			||||||
                                    send_as_initial_response(resp, d);
 | 
					 | 
				
			||||||
                                    d
 | 
					 | 
				
			||||||
                                })
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    let _ = component
 | 
					                    let _ = component
 | 
				
			||||||
                        .create_interaction_response(&ctx, |r| {
 | 
					                        .create_interaction_response(&ctx, |r| {
 | 
				
			||||||
@@ -270,10 +227,11 @@ WHERE guilds.guild = ?",
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            ComponentDataModel::TodoSelector(selector) => {
 | 
					            ComponentDataModel::TodoSelector(selector) => {
 | 
				
			||||||
                if Some(component.user.id.0) == selector.user_id || selector.user_id.is_none() {
 | 
					                if Some(component.user.id.0) == selector.user_id || selector.user_id.is_none() {
 | 
				
			||||||
 | 
					                    let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
				
			||||||
                    let selected_id = component.data.values.join(",");
 | 
					                    let selected_id = component.data.values.join(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    sqlx::query!("DELETE FROM todos WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
					                    sqlx::query!("DELETE FROM todos WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
				
			||||||
                        .execute(&data.database)
 | 
					                        .execute(&pool)
 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .unwrap();
 | 
					                        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -284,7 +242,7 @@ WHERE guilds.guild = ?",
 | 
				
			|||||||
                    selector.channel_id,
 | 
					                    selector.channel_id,
 | 
				
			||||||
                    selector.guild_id,
 | 
					                    selector.guild_id,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .fetch_all(&data.database)
 | 
					                .fetch_all(&pool)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .unwrap()
 | 
					                .unwrap()
 | 
				
			||||||
                .iter()
 | 
					                .iter()
 | 
				
			||||||
@@ -299,15 +257,8 @@ WHERE guilds.guild = ?",
 | 
				
			|||||||
                        selector.guild_id,
 | 
					                        selector.guild_id,
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    let _ = component
 | 
					                    let mut invoke = CommandInvoke::component(component);
 | 
				
			||||||
                        .create_interaction_response(&ctx, |f| {
 | 
					                    let _ = invoke.respond(&ctx, resp).await;
 | 
				
			||||||
                            f.kind(InteractionResponseType::UpdateMessage)
 | 
					 | 
				
			||||||
                                .interaction_response_data(|d| {
 | 
					 | 
				
			||||||
                                    send_as_initial_response(resp, d);
 | 
					 | 
				
			||||||
                                    d
 | 
					 | 
				
			||||||
                                })
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    let _ = component
 | 
					                    let _ = component
 | 
				
			||||||
                        .create_interaction_response(&ctx, |r| {
 | 
					                        .create_interaction_response(&ctx, |r| {
 | 
				
			||||||
@@ -323,87 +274,15 @@ WHERE guilds.guild = ?",
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ComponentDataModel::MacroPager(pager) => {
 | 
					            ComponentDataModel::MacroPager(pager) => {
 | 
				
			||||||
                let macros = data.command_macros(component.guild_id.unwrap()).await.unwrap();
 | 
					                let mut invoke = CommandInvoke::component(component);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let max_page = max_macro_page(¯os);
 | 
					                let max_page = max_macro_page(¯os);
 | 
				
			||||||
                let page = pager.next_page(max_page);
 | 
					                let page = pager.next_page(max_page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let resp = show_macro_page(¯os, page);
 | 
					                let resp = show_macro_page(¯os, page);
 | 
				
			||||||
 | 
					                let _ = invoke.respond(&ctx, resp).await;
 | 
				
			||||||
                let _ = component
 | 
					 | 
				
			||||||
                    .create_interaction_response(&ctx, |f| {
 | 
					 | 
				
			||||||
                        f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
					 | 
				
			||||||
                            |d| {
 | 
					 | 
				
			||||||
                                send_as_initial_response(resp, d);
 | 
					 | 
				
			||||||
                                d
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            ComponentDataModel::UndoReminder(undo_reminder) => {
 | 
					 | 
				
			||||||
                if component.user.id == undo_reminder.user_id {
 | 
					 | 
				
			||||||
                    let reminder =
 | 
					 | 
				
			||||||
                        Reminder::from_id(&data.database, undo_reminder.reminder_id).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(reminder) = reminder {
 | 
					 | 
				
			||||||
                        match reminder.delete(&data.database).await {
 | 
					 | 
				
			||||||
                            Ok(()) => {
 | 
					 | 
				
			||||||
                                let _ = component
 | 
					 | 
				
			||||||
                                    .create_interaction_response(&ctx, |f| {
 | 
					 | 
				
			||||||
                                        f.kind(InteractionResponseType::UpdateMessage)
 | 
					 | 
				
			||||||
                                            .interaction_response_data(|d| {
 | 
					 | 
				
			||||||
                                                d.embed(|e| {
 | 
					 | 
				
			||||||
                                                    e.title("Reminder Canceled")
 | 
					 | 
				
			||||||
                                                        .description(
 | 
					 | 
				
			||||||
                                                            "This reminder has been canceled.",
 | 
					 | 
				
			||||||
                                                        )
 | 
					 | 
				
			||||||
                                                        .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
                                                })
 | 
					 | 
				
			||||||
                                                .components(|c| c)
 | 
					 | 
				
			||||||
                                            })
 | 
					 | 
				
			||||||
                                    })
 | 
					 | 
				
			||||||
                                    .await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            Err(e) => {
 | 
					 | 
				
			||||||
                                warn!("Error canceling reminder: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                let _ = component
 | 
					 | 
				
			||||||
                                    .create_interaction_response(&ctx, |f| {
 | 
					 | 
				
			||||||
                                        f.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					 | 
				
			||||||
                                            .interaction_response_data(|d| {
 | 
					 | 
				
			||||||
                                                d.content(
 | 
					 | 
				
			||||||
                                                    "The reminder could not be canceled: it may have already been deleted. Check `/del`!")
 | 
					 | 
				
			||||||
                                                    .ephemeral(true)
 | 
					 | 
				
			||||||
                                            })
 | 
					 | 
				
			||||||
                                    })
 | 
					 | 
				
			||||||
                                    .await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        let _ = component
 | 
					 | 
				
			||||||
                            .create_interaction_response(&ctx, |f| {
 | 
					 | 
				
			||||||
                                f.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					 | 
				
			||||||
                                    .interaction_response_data(|d| {
 | 
					 | 
				
			||||||
                                        d.content(
 | 
					 | 
				
			||||||
                                            "The reminder could not be canceled: it may have already been deleted. Check `/del`!")
 | 
					 | 
				
			||||||
                                            .ephemeral(true)
 | 
					 | 
				
			||||||
                                    })
 | 
					 | 
				
			||||||
                            })
 | 
					 | 
				
			||||||
                            .await;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    let _ = component
 | 
					 | 
				
			||||||
                        .create_interaction_response(&ctx, |f| {
 | 
					 | 
				
			||||||
                            f.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					 | 
				
			||||||
                                .interaction_response_data(|d| {
 | 
					 | 
				
			||||||
                                    d.content(
 | 
					 | 
				
			||||||
                                        "Only the user who performed the command can use this button.")
 | 
					 | 
				
			||||||
                                        .ephemeral(true)
 | 
					 | 
				
			||||||
                                })
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -422,9 +301,3 @@ pub struct TodoSelector {
 | 
				
			|||||||
    pub channel_id: Option<u64>,
 | 
					    pub channel_id: Option<u64>,
 | 
				
			||||||
    pub guild_id: Option<u64>,
 | 
					    pub guild_id: Option<u64>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct UndoReminder {
 | 
					 | 
				
			||||||
    pub user_id: serenity::UserId,
 | 
					 | 
				
			||||||
    pub reminder_id: u32,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,14 @@
 | 
				
			|||||||
pub const DAY: u64 = 86_400;
 | 
					pub const DAY: u64 = 86_400;
 | 
				
			||||||
pub const HOUR: u64 = 3_600;
 | 
					pub const HOUR: u64 = 3_600;
 | 
				
			||||||
pub const MINUTE: u64 = 60;
 | 
					pub const MINUTE: u64 = 60;
 | 
				
			||||||
 | 
					 | 
				
			||||||
pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000;
 | 
					pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000;
 | 
				
			||||||
pub const SELECT_MAX_ENTRIES: usize = 25;
 | 
					pub const SELECT_MAX_ENTRIES: usize = 25;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const MACRO_MAX_COMMANDS: usize = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
					pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const THEME_COLOR_FALLBACK: u32 = 0x8fb677;
 | 
					const THEME_COLOR_FALLBACK: u32 = 0x8fb677;
 | 
				
			||||||
pub const MACRO_MAX_COMMANDS: usize = 5;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::{collections::HashSet, env, iter::FromIterator};
 | 
					use std::{collections::HashSet, env, iter::FromIterator};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,11 +36,15 @@ lazy_static! {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
    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("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten();
 | 
				
			||||||
    pub static ref MIN_INTERVAL: i64 =
 | 
					    pub static ref MIN_INTERVAL: i64 = env::var("MIN_INTERVAL")
 | 
				
			||||||
        env::var("MIN_INTERVAL").ok().and_then(|inner| inner.parse::<i64>().ok()).unwrap_or(600);
 | 
					        .ok()
 | 
				
			||||||
 | 
					        .map(|inner| inner.parse::<i64>().ok())
 | 
				
			||||||
 | 
					        .flatten()
 | 
				
			||||||
 | 
					        .unwrap_or(600);
 | 
				
			||||||
    pub static ref MAX_TIME: i64 = env::var("MAX_TIME")
 | 
					    pub static ref MAX_TIME: i64 = env::var("MAX_TIME")
 | 
				
			||||||
        .ok()
 | 
					        .ok()
 | 
				
			||||||
        .and_then(|inner| inner.parse::<i64>().ok())
 | 
					        .map(|inner| inner.parse::<i64>().ok())
 | 
				
			||||||
 | 
					        .flatten()
 | 
				
			||||||
        .unwrap_or(60 * 60 * 24 * 365 * 50);
 | 
					        .unwrap_or(60 * 60 * 24 * 365 * 50);
 | 
				
			||||||
    pub static ref LOCAL_TIMEZONE: String =
 | 
					    pub static ref LOCAL_TIMEZONE: String =
 | 
				
			||||||
        env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());
 | 
					        env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,63 +1,18 @@
 | 
				
			|||||||
use std::{collections::HashMap, env, sync::atomic::Ordering};
 | 
					use std::{collections::HashMap, env};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use log::{error, info, warn};
 | 
					use poise::serenity::{client::Context, model::interactions::Interaction, utils::shard_id};
 | 
				
			||||||
use poise::{
 | 
					 | 
				
			||||||
    serenity::{model::interactions::Interaction, utils::shard_id},
 | 
					 | 
				
			||||||
    serenity_prelude as serenity,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{component_models::ComponentDataModel, Data, Error};
 | 
					use crate::{Data, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn listener(
 | 
					pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> {
 | 
				
			||||||
    ctx: &serenity::Context,
 | 
					 | 
				
			||||||
    event: &poise::Event<'_>,
 | 
					 | 
				
			||||||
    data: &Data,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    match event {
 | 
					    match event {
 | 
				
			||||||
        poise::Event::Ready { .. } => {
 | 
					 | 
				
			||||||
            ctx.set_activity(serenity::Activity::watching("for /remind")).await;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        poise::Event::CacheReady { .. } => {
 | 
					 | 
				
			||||||
            info!("Cache Ready! Preparing extra processes");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if !data.is_loop_running.load(Ordering::Relaxed) {
 | 
					 | 
				
			||||||
                let kill_tx = data.broadcast.clone();
 | 
					 | 
				
			||||||
                let kill_recv = data.broadcast.subscribe();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let ctx1 = ctx.clone();
 | 
					 | 
				
			||||||
                let ctx2 = ctx.clone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let pool1 = data.database.clone();
 | 
					 | 
				
			||||||
                let pool2 = data.database.clone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let run_settings = env::var("DONTRUN").unwrap_or_else(|_| "".to_string());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if !run_settings.contains("postman") {
 | 
					 | 
				
			||||||
                    tokio::spawn(async move {
 | 
					 | 
				
			||||||
                        match postman::initialize(kill_recv, ctx1, &pool1).await {
 | 
					 | 
				
			||||||
                            Ok(_) => {}
 | 
					 | 
				
			||||||
                            Err(e) => {
 | 
					 | 
				
			||||||
                                error!("postman exiting: {}", e);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        };
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    warn!("Not running postman");
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if !run_settings.contains("web") {
 | 
					 | 
				
			||||||
                    tokio::spawn(async move {
 | 
					 | 
				
			||||||
                        reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap();
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    warn!("Not running web");
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                data.is_loop_running.swap(true, Ordering::Relaxed);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        poise::Event::ChannelDelete { channel } => {
 | 
					        poise::Event::ChannelDelete { channel } => {
 | 
				
			||||||
            sqlx::query!("DELETE FROM channels WHERE channel = ?", channel.id.as_u64())
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					DELETE FROM channels WHERE channel = ?
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                channel.id.as_u64()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            .execute(&data.database)
 | 
					            .execute(&data.database)
 | 
				
			||||||
            .await
 | 
					            .await
 | 
				
			||||||
            .unwrap();
 | 
					            .unwrap();
 | 
				
			||||||
@@ -109,19 +64,19 @@ pub async fn listener(
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        poise::Event::GuildDelete { incomplete, .. } => {
 | 
					        poise::Event::GuildDelete { incomplete, full } => {
 | 
				
			||||||
            let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", 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::MessageComponent(component) => {
 | 
				
			||||||
                let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
 | 
					                //let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
 | 
				
			||||||
 | 
					                //component_model.act(&ctx, component).await;
 | 
				
			||||||
                component_model.act(ctx, data, component).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            _ => {}
 | 
					            _ => {}
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        _ => {}
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										56
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						@@ -1,43 +1,62 @@
 | 
				
			|||||||
use poise::serenity::model::channel::Channel;
 | 
					use poise::{serenity::model::channel::Channel, ApplicationCommandOrAutocompleteInteraction};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
 | 
					use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::CommandOptions, Context, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn guild_only(ctx: Context<'_>) -> Result<bool, Error> {
 | 
				
			||||||
 | 
					    if ctx.guild_id().is_some() {
 | 
				
			||||||
 | 
					        Ok(true)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        let _ = ctx.say("This command can only be used in servers").await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(false)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn macro_check(ctx: Context<'_>) -> bool {
 | 
					async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			||||||
    if let Context::Application(app_ctx) = ctx {
 | 
					    if let Context::Application(app_ctx) = ctx {
 | 
				
			||||||
 | 
					        if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(interaction) =
 | 
				
			||||||
 | 
					            app_ctx.interaction
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            if let Some(guild_id) = ctx.guild_id() {
 | 
					            if let Some(guild_id) = ctx.guild_id() {
 | 
				
			||||||
            if ctx.command().identifying_name != "finish_macro" {
 | 
					                if ctx.command().identifying_name != "macro_finish" {
 | 
				
			||||||
                    let mut lock = ctx.data().recording_macros.write().await;
 | 
					                    let mut lock = ctx.data().recording_macros.write().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
 | 
					                    if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
 | 
				
			||||||
                        if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
 | 
					                        if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
 | 
				
			||||||
                            let _ = ctx.send(|m| {
 | 
					                            let _ = ctx.send(|m| {
 | 
				
			||||||
                                m.ephemeral(true).content(
 | 
					                                m.ephemeral(true).content(
 | 
				
			||||||
                                format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
 | 
					                                    "5 commands already recorded. Please use `/macro finish` to end recording.",
 | 
				
			||||||
                                )
 | 
					                                )
 | 
				
			||||||
                            })
 | 
					                            })
 | 
				
			||||||
                            .await;
 | 
					                            .await;
 | 
				
			||||||
                        } else {
 | 
					                        } else {
 | 
				
			||||||
                        let recorded = RecordedCommand {
 | 
					                            let mut command_options = CommandOptions::new(&ctx.command().name);
 | 
				
			||||||
                            action: None,
 | 
					                            command_options.populate(&interaction);
 | 
				
			||||||
                            command_name: ctx.command().identifying_name.clone(),
 | 
					 | 
				
			||||||
                            options: Vec::from(app_ctx.args),
 | 
					 | 
				
			||||||
                        };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        command_macro.commands.push(recorded);
 | 
					                            command_macro.commands.push(command_options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            let _ = ctx
 | 
					                            let _ = ctx
 | 
				
			||||||
                                .send(|m| m.ephemeral(true).content("Command recorded to macro"))
 | 
					                                .send(|m| m.ephemeral(true).content("Command recorded to macro"))
 | 
				
			||||||
                                .await;
 | 
					                                .await;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    return false;
 | 
					                        false
 | 
				
			||||||
                }
 | 
					                    } else {
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        true
 | 
					                        true
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    true
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn check_self_permissions(ctx: Context<'_>) -> bool {
 | 
					async fn check_self_permissions(ctx: Context<'_>) -> bool {
 | 
				
			||||||
    if let Some(guild) = ctx.guild() {
 | 
					    if let Some(guild) = ctx.guild() {
 | 
				
			||||||
@@ -50,15 +69,16 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
 | 
				
			|||||||
        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_cached(&ctx.discord())
 | 
				
			||||||
            .and_then(|c| {
 | 
					            .map(|c| {
 | 
				
			||||||
                if let Channel::Guild(channel) = c {
 | 
					                if let Channel::Guild(channel) = c {
 | 
				
			||||||
                    channel.permissions_for_user(&ctx.discord(), user_id).ok()
 | 
					                    channel.permissions_for_user(&ctx.discord(), user_id).ok()
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    None
 | 
					                    None
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 | 
					            .flatten()
 | 
				
			||||||
            .map_or((false, false, false), |p| {
 | 
					            .map_or((false, false, false), |p| {
 | 
				
			||||||
                (p.view_channel(), p.send_messages(), p.embed_links())
 | 
					                (p.read_messages(), p.send_messages(), p.embed_links())
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if manage_webhooks && send_messages && embed_links {
 | 
					        if manage_webhooks && send_messages && embed_links {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,251 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
With modifications, 2022 Jude Southworth
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Original copyright notice:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Copyright 2021 Paul Colomiets
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 | 
					 | 
				
			||||||
and associated documentation files (the "Software"), to deal in the Software without restriction,
 | 
					 | 
				
			||||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
 | 
					 | 
				
			||||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 | 
					 | 
				
			||||||
furnished to do so, subject to the following conditions:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The above copyright notice and this permission notice shall be included in all copies or
 | 
					 | 
				
			||||||
substantial portions of the Software.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 | 
					 | 
				
			||||||
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 | 
					 | 
				
			||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 | 
					 | 
				
			||||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
					 | 
				
			||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::{error::Error as StdError, fmt, str::Chars};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Error parsing human-friendly duration
 | 
					 | 
				
			||||||
#[derive(Debug, PartialEq, Clone)]
 | 
					 | 
				
			||||||
pub enum Error {
 | 
					 | 
				
			||||||
    /// Invalid character during parsing
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// More specifically anything that is not alphanumeric is prohibited
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// The field is an byte offset of the character in the string.
 | 
					 | 
				
			||||||
    InvalidCharacter(usize),
 | 
					 | 
				
			||||||
    /// Non-numeric value where number is expected
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// This usually means that either time unit is broken into words,
 | 
					 | 
				
			||||||
    /// e.g. `m sec` instead of `msec`, or just number is omitted,
 | 
					 | 
				
			||||||
    /// for example `2 hours min` instead of `2 hours 1 min`
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// The field is an byte offset of the errorneous character
 | 
					 | 
				
			||||||
    /// in the string.
 | 
					 | 
				
			||||||
    NumberExpected(usize),
 | 
					 | 
				
			||||||
    /// Unit in the number is not one of allowed units
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// See documentation of `parse_duration` for the list of supported
 | 
					 | 
				
			||||||
    /// time units.
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// The two fields are start and end (exclusive) of the slice from
 | 
					 | 
				
			||||||
    /// the original string, containing errorneous value
 | 
					 | 
				
			||||||
    UnknownUnit {
 | 
					 | 
				
			||||||
        /// Start of the invalid unit inside the original string
 | 
					 | 
				
			||||||
        start: usize,
 | 
					 | 
				
			||||||
        /// End of the invalid unit inside the original string
 | 
					 | 
				
			||||||
        end: usize,
 | 
					 | 
				
			||||||
        /// The unit verbatim
 | 
					 | 
				
			||||||
        unit: String,
 | 
					 | 
				
			||||||
        /// A number associated with the unit
 | 
					 | 
				
			||||||
        value: u64,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    /// The numeric value is too large
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// Usually this means value is too large to be useful. If user writes
 | 
					 | 
				
			||||||
    /// data in subsecond units, then the maximum is about 3k years. When
 | 
					 | 
				
			||||||
    /// using seconds, or larger units, the limit is even larger.
 | 
					 | 
				
			||||||
    NumberOverflow,
 | 
					 | 
				
			||||||
    /// The value was an empty string (or consists only whitespace)
 | 
					 | 
				
			||||||
    Empty,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl StdError for Error {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl fmt::Display for Error {
 | 
					 | 
				
			||||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Error::InvalidCharacter(offset) => write!(f, "invalid character at {}", offset),
 | 
					 | 
				
			||||||
            Error::NumberExpected(offset) => write!(f, "expected number at {}", offset),
 | 
					 | 
				
			||||||
            Error::UnknownUnit { unit, value, .. } if unit.is_empty() => {
 | 
					 | 
				
			||||||
                write!(f, "time unit needed, for example {0}sec or {0}ms", value,)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            Error::UnknownUnit { unit, .. } => {
 | 
					 | 
				
			||||||
                write!(
 | 
					 | 
				
			||||||
                    f,
 | 
					 | 
				
			||||||
                    "unknown time unit {:?}, \
 | 
					 | 
				
			||||||
                    supported units: ns, us, ms, sec, min, hours, days, \
 | 
					 | 
				
			||||||
                    weeks, months, years (and few variations)",
 | 
					 | 
				
			||||||
                    unit
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            Error::NumberOverflow => write!(f, "number is too large"),
 | 
					 | 
				
			||||||
            Error::Empty => write!(f, "value was empty"),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
trait OverflowOp: Sized {
 | 
					 | 
				
			||||||
    fn mul(self, other: Self) -> Result<Self, Error>;
 | 
					 | 
				
			||||||
    fn add(self, other: Self) -> Result<Self, Error>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl OverflowOp for u64 {
 | 
					 | 
				
			||||||
    fn mul(self, other: Self) -> Result<Self, Error> {
 | 
					 | 
				
			||||||
        self.checked_mul(other).ok_or(Error::NumberOverflow)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    fn add(self, other: Self) -> Result<Self, Error> {
 | 
					 | 
				
			||||||
        self.checked_add(other).ok_or(Error::NumberOverflow)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone)]
 | 
					 | 
				
			||||||
pub struct Interval {
 | 
					 | 
				
			||||||
    pub month: u64,
 | 
					 | 
				
			||||||
    pub sec: u64,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Parser<'a> {
 | 
					 | 
				
			||||||
    iter: Chars<'a>,
 | 
					 | 
				
			||||||
    src: &'a str,
 | 
					 | 
				
			||||||
    current: (u64, u64, u64),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<'a> Parser<'a> {
 | 
					 | 
				
			||||||
    fn off(&self) -> usize {
 | 
					 | 
				
			||||||
        self.src.len() - self.iter.as_str().len()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn parse_first_char(&mut self) -> Result<Option<u64>, Error> {
 | 
					 | 
				
			||||||
        let off = self.off();
 | 
					 | 
				
			||||||
        for c in self.iter.by_ref() {
 | 
					 | 
				
			||||||
            match c {
 | 
					 | 
				
			||||||
                '0'..='9' => {
 | 
					 | 
				
			||||||
                    return Ok(Some(c as u64 - '0' as u64));
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                c if c.is_whitespace() => continue,
 | 
					 | 
				
			||||||
                _ => {
 | 
					 | 
				
			||||||
                    return Err(Error::NumberExpected(off));
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Ok(None)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    fn parse_unit(&mut self, n: u64, start: usize, end: usize) -> Result<(), Error> {
 | 
					 | 
				
			||||||
        let (mut month, mut sec, nsec) = match &self.src[start..end] {
 | 
					 | 
				
			||||||
            "nanos" | "nsec" | "ns" => (0u64, 0u64, n),
 | 
					 | 
				
			||||||
            "usec" | "us" => (0, 0u64, n.mul(1000)?),
 | 
					 | 
				
			||||||
            "millis" | "msec" | "ms" => (0, 0u64, n.mul(1_000_000)?),
 | 
					 | 
				
			||||||
            "seconds" | "second" | "secs" | "sec" | "s" => (0, n, 0),
 | 
					 | 
				
			||||||
            "minutes" | "minute" | "min" | "mins" | "m" => (0, n.mul(60)?, 0),
 | 
					 | 
				
			||||||
            "hours" | "hour" | "hr" | "hrs" | "h" => (0, n.mul(3600)?, 0),
 | 
					 | 
				
			||||||
            "days" | "day" | "d" => (0, n.mul(86400)?, 0),
 | 
					 | 
				
			||||||
            "weeks" | "week" | "w" => (0, n.mul(86400 * 7)?, 0),
 | 
					 | 
				
			||||||
            "months" | "month" | "M" => (n, 0, 0),
 | 
					 | 
				
			||||||
            "years" | "year" | "y" => (12, 0, 0),
 | 
					 | 
				
			||||||
            _ => {
 | 
					 | 
				
			||||||
                return Err(Error::UnknownUnit {
 | 
					 | 
				
			||||||
                    start,
 | 
					 | 
				
			||||||
                    end,
 | 
					 | 
				
			||||||
                    unit: self.src[start..end].to_string(),
 | 
					 | 
				
			||||||
                    value: n,
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        let mut nsec = self.current.2 + nsec;
 | 
					 | 
				
			||||||
        if nsec > 1_000_000_000 {
 | 
					 | 
				
			||||||
            sec += nsec / 1_000_000_000;
 | 
					 | 
				
			||||||
            nsec %= 1_000_000_000;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        sec += self.current.1;
 | 
					 | 
				
			||||||
        month += self.current.0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.current = (month, sec, nsec);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn parse(mut self) -> Result<Interval, Error> {
 | 
					 | 
				
			||||||
        let mut n = self.parse_first_char()?.ok_or(Error::Empty)?;
 | 
					 | 
				
			||||||
        'outer: loop {
 | 
					 | 
				
			||||||
            let mut off = self.off();
 | 
					 | 
				
			||||||
            while let Some(c) = self.iter.next() {
 | 
					 | 
				
			||||||
                match c {
 | 
					 | 
				
			||||||
                    '0'..='9' => {
 | 
					 | 
				
			||||||
                        n = n
 | 
					 | 
				
			||||||
                            .checked_mul(10)
 | 
					 | 
				
			||||||
                            .and_then(|x| x.checked_add(c as u64 - '0' as u64))
 | 
					 | 
				
			||||||
                            .ok_or(Error::NumberOverflow)?;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    c if c.is_whitespace() => {}
 | 
					 | 
				
			||||||
                    'a'..='z' | 'A'..='Z' => {
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    _ => {
 | 
					 | 
				
			||||||
                        return Err(Error::InvalidCharacter(off));
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                off = self.off();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            let start = off;
 | 
					 | 
				
			||||||
            let mut off = self.off();
 | 
					 | 
				
			||||||
            while let Some(c) = self.iter.next() {
 | 
					 | 
				
			||||||
                match c {
 | 
					 | 
				
			||||||
                    '0'..='9' => {
 | 
					 | 
				
			||||||
                        self.parse_unit(n, start, off)?;
 | 
					 | 
				
			||||||
                        n = c as u64 - '0' as u64;
 | 
					 | 
				
			||||||
                        continue 'outer;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    c if c.is_whitespace() => break,
 | 
					 | 
				
			||||||
                    'a'..='z' | 'A'..='Z' => {}
 | 
					 | 
				
			||||||
                    _ => {
 | 
					 | 
				
			||||||
                        return Err(Error::InvalidCharacter(off));
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                off = self.off();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            self.parse_unit(n, start, off)?;
 | 
					 | 
				
			||||||
            n = match self.parse_first_char()? {
 | 
					 | 
				
			||||||
                Some(n) => n,
 | 
					 | 
				
			||||||
                None => return Ok(Interval { month: self.current.0, sec: self.current.1 }),
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Parse duration object `1hour 12min 5s`
 | 
					 | 
				
			||||||
///
 | 
					 | 
				
			||||||
/// The duration object is a concatenation of time spans. Where each time
 | 
					 | 
				
			||||||
/// span is an integer number and a suffix. Supported suffixes:
 | 
					 | 
				
			||||||
///
 | 
					 | 
				
			||||||
/// * `nsec`, `ns` -- nanoseconds
 | 
					 | 
				
			||||||
/// * `usec`, `us` -- microseconds
 | 
					 | 
				
			||||||
/// * `msec`, `ms` -- milliseconds
 | 
					 | 
				
			||||||
/// * `seconds`, `second`, `sec`, `s`
 | 
					 | 
				
			||||||
/// * `minutes`, `minute`, `min`, `m`
 | 
					 | 
				
			||||||
/// * `hours`, `hour`, `hr`, `h`
 | 
					 | 
				
			||||||
/// * `days`, `day`, `d`
 | 
					 | 
				
			||||||
/// * `weeks`, `week`, `w`
 | 
					 | 
				
			||||||
/// * `months`, `month`, `M` -- defined as 30.44 days
 | 
					 | 
				
			||||||
/// * `years`, `year`, `y` -- defined as 365.25 days
 | 
					 | 
				
			||||||
///
 | 
					 | 
				
			||||||
/// # Examples
 | 
					 | 
				
			||||||
///
 | 
					 | 
				
			||||||
/// ```
 | 
					 | 
				
			||||||
/// use std::time::Duration;
 | 
					 | 
				
			||||||
/// use humantime::parse_duration;
 | 
					 | 
				
			||||||
///
 | 
					 | 
				
			||||||
/// assert_eq!(parse_duration("2h 37min"), Ok(Duration::new(9420, 0)));
 | 
					 | 
				
			||||||
/// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000)));
 | 
					 | 
				
			||||||
/// ```
 | 
					 | 
				
			||||||
pub fn parse_duration(s: &str) -> Result<Interval, Error> {
 | 
					 | 
				
			||||||
    Parser { iter: s.chars(), src: s, current: (0, 0, 0) }.parse()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										106
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						@@ -1,37 +1,29 @@
 | 
				
			|||||||
#![feature(int_roundings)]
 | 
					#![feature(int_roundings)]
 | 
				
			||||||
 | 
					 | 
				
			||||||
#[macro_use]
 | 
					#[macro_use]
 | 
				
			||||||
extern crate lazy_static;
 | 
					extern crate lazy_static;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod commands;
 | 
					mod commands;
 | 
				
			||||||
mod component_models;
 | 
					// mod component_models;
 | 
				
			||||||
mod consts;
 | 
					mod consts;
 | 
				
			||||||
mod event_handlers;
 | 
					mod event_handlers;
 | 
				
			||||||
mod hooks;
 | 
					mod hooks;
 | 
				
			||||||
mod interval_parser;
 | 
					 | 
				
			||||||
mod models;
 | 
					mod models;
 | 
				
			||||||
mod time_parser;
 | 
					mod time_parser;
 | 
				
			||||||
mod utils;
 | 
					mod utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::{
 | 
					use std::{collections::HashMap, env};
 | 
				
			||||||
    collections::HashMap,
 | 
					 | 
				
			||||||
    env,
 | 
					 | 
				
			||||||
    error::Error as StdError,
 | 
					 | 
				
			||||||
    fmt::{Debug, Display, Formatter},
 | 
					 | 
				
			||||||
    sync::atomic::AtomicBool,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use dotenv::dotenv;
 | 
					use dotenv::dotenv;
 | 
				
			||||||
use poise::serenity::model::{
 | 
					use poise::serenity::model::{
 | 
				
			||||||
    gateway::GatewayIntents,
 | 
					    gateway::{Activity, GatewayIntents},
 | 
				
			||||||
    id::{GuildId, UserId},
 | 
					    id::{GuildId, UserId},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
use tokio::sync::{broadcast, broadcast::Sender, RwLock};
 | 
					use tokio::sync::RwLock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
 | 
					    commands::{info_cmds, moderation_cmds},
 | 
				
			||||||
    consts::THEME_COLOR,
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
    event_handlers::listener,
 | 
					    event_handlers::listener,
 | 
				
			||||||
    hooks::all_checks,
 | 
					    hooks::all_checks,
 | 
				
			||||||
@@ -41,51 +33,18 @@ use crate::{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type Database = MySql;
 | 
					type Database = MySql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Error = Box<dyn std::error::Error + Send + Sync>;
 | 
					 | 
				
			||||||
type Context<'a> = poise::Context<'a, Data, Error>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct Data {
 | 
					pub struct Data {
 | 
				
			||||||
    database: Pool<Database>,
 | 
					    database: Pool<Database>,
 | 
				
			||||||
    http: reqwest::Client,
 | 
					    http: reqwest::Client,
 | 
				
			||||||
    recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro<Data, Error>>>,
 | 
					    recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro>>,
 | 
				
			||||||
    popular_timezones: Vec<Tz>,
 | 
					    popular_timezones: Vec<Tz>,
 | 
				
			||||||
    is_loop_running: AtomicBool,
 | 
					 | 
				
			||||||
    broadcast: Sender<()>,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Debug for Data {
 | 
					type Error = Box<dyn std::error::Error + Send + Sync>;
 | 
				
			||||||
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
					type Context<'a> = poise::Context<'a, Data, Error>;
 | 
				
			||||||
        write!(f, "Data {{ .. }}")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Ended;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Debug for Ended {
 | 
					 | 
				
			||||||
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
					 | 
				
			||||||
        f.write_str("Process ended.")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Display for Ended {
 | 
					 | 
				
			||||||
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
					 | 
				
			||||||
        f.write_str("Process ended.")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl StdError for Ended {}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tokio::main]
 | 
					#[tokio::main]
 | 
				
			||||||
async fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
					async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
 | 
				
			||||||
    let (tx, mut rx) = broadcast::channel(16);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tokio::select! {
 | 
					 | 
				
			||||||
        output = _main(tx) => output,
 | 
					 | 
				
			||||||
        _ = rx.recv() => Err(Box::new(Ended) as Box<dyn StdError + Send + Sync>)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
					 | 
				
			||||||
    env_logger::init();
 | 
					    env_logger::init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dotenv()?;
 | 
					    dotenv()?;
 | 
				
			||||||
@@ -98,7 +57,6 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
            info_cmds::info(),
 | 
					            info_cmds::info(),
 | 
				
			||||||
            info_cmds::donate(),
 | 
					            info_cmds::donate(),
 | 
				
			||||||
            info_cmds::clock(),
 | 
					            info_cmds::clock(),
 | 
				
			||||||
            info_cmds::clock_context_menu(),
 | 
					 | 
				
			||||||
            info_cmds::dashboard(),
 | 
					            info_cmds::dashboard(),
 | 
				
			||||||
            moderation_cmds::timezone(),
 | 
					            moderation_cmds::timezone(),
 | 
				
			||||||
            poise::Command {
 | 
					            poise::Command {
 | 
				
			||||||
@@ -111,43 +69,6 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
                ..moderation_cmds::macro_base()
 | 
					                ..moderation_cmds::macro_base()
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            reminder_cmds::pause(),
 | 
					 | 
				
			||||||
            reminder_cmds::offset(),
 | 
					 | 
				
			||||||
            reminder_cmds::nudge(),
 | 
					 | 
				
			||||||
            reminder_cmds::look(),
 | 
					 | 
				
			||||||
            reminder_cmds::delete(),
 | 
					 | 
				
			||||||
            poise::Command {
 | 
					 | 
				
			||||||
                subcommands: vec![
 | 
					 | 
				
			||||||
                    reminder_cmds::list_timer(),
 | 
					 | 
				
			||||||
                    reminder_cmds::start_timer(),
 | 
					 | 
				
			||||||
                    reminder_cmds::delete_timer(),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                ..reminder_cmds::timer_base()
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            reminder_cmds::remind(),
 | 
					 | 
				
			||||||
            poise::Command {
 | 
					 | 
				
			||||||
                subcommands: vec![
 | 
					 | 
				
			||||||
                    poise::Command {
 | 
					 | 
				
			||||||
                        subcommands: vec![
 | 
					 | 
				
			||||||
                            todo_cmds::todo_guild_add(),
 | 
					 | 
				
			||||||
                            todo_cmds::todo_guild_view(),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                        ..todo_cmds::todo_guild_base()
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    poise::Command {
 | 
					 | 
				
			||||||
                        subcommands: vec![
 | 
					 | 
				
			||||||
                            todo_cmds::todo_channel_add(),
 | 
					 | 
				
			||||||
                            todo_cmds::todo_channel_view(),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                        ..todo_cmds::todo_channel_base()
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    poise::Command {
 | 
					 | 
				
			||||||
                        subcommands: vec![todo_cmds::todo_user_add(), todo_cmds::todo_user_view()],
 | 
					 | 
				
			||||||
                        ..todo_cmds::todo_user_base()
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                ..todo_cmds::todo_base()
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        allowed_mentions: None,
 | 
					        allowed_mentions: None,
 | 
				
			||||||
        command_check: Some(|ctx| Box::pin(all_checks(ctx))),
 | 
					        command_check: Some(|ctx| Box::pin(all_checks(ctx))),
 | 
				
			||||||
@@ -159,7 +80,8 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
        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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let popular_timezones = sqlx::query!(
 | 
					    let popular_timezones = sqlx::query!(
 | 
				
			||||||
        "SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
 | 
					        "
 | 
				
			||||||
 | 
					SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .fetch_all(&database)
 | 
					    .fetch_all(&database)
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
@@ -172,6 +94,8 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
        .token(discord_token)
 | 
					        .token(discord_token)
 | 
				
			||||||
        .user_data_setup(move |ctx, _bot, framework| {
 | 
					        .user_data_setup(move |ctx, _bot, framework| {
 | 
				
			||||||
            Box::pin(async move {
 | 
					            Box::pin(async move {
 | 
				
			||||||
 | 
					                ctx.set_activity(Activity::watching("for /remind")).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                register_application_commands(
 | 
					                register_application_commands(
 | 
				
			||||||
                    ctx,
 | 
					                    ctx,
 | 
				
			||||||
                    framework,
 | 
					                    framework,
 | 
				
			||||||
@@ -187,13 +111,11 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
                    database,
 | 
					                    database,
 | 
				
			||||||
                    popular_timezones,
 | 
					                    popular_timezones,
 | 
				
			||||||
                    recording_macros: Default::default(),
 | 
					                    recording_macros: Default::default(),
 | 
				
			||||||
                    is_loop_running: AtomicBool::new(false),
 | 
					 | 
				
			||||||
                    broadcast: tx,
 | 
					 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .options(options)
 | 
					        .options(options)
 | 
				
			||||||
        .intents(GatewayIntents::GUILDS)
 | 
					        .client_settings(move |client_builder| client_builder.intents(GatewayIntents::GUILDS))
 | 
				
			||||||
        .run_autosharded()
 | 
					        .run_autosharded()
 | 
				
			||||||
        .await?;
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,69 +1,267 @@
 | 
				
			|||||||
use poise::serenity::model::{
 | 
					use std::collections::HashMap;
 | 
				
			||||||
    id::GuildId, interactions::application_command::ApplicationCommandInteractionDataOption,
 | 
					
 | 
				
			||||||
 | 
					use poise::{
 | 
				
			||||||
 | 
					    serenity::{
 | 
				
			||||||
 | 
					        json::Value,
 | 
				
			||||||
 | 
					        model::{
 | 
				
			||||||
 | 
					            id::{ChannelId, GuildId, RoleId, UserId},
 | 
				
			||||||
 | 
					            interactions::application_command::{
 | 
				
			||||||
 | 
					                ApplicationCommandInteraction, ApplicationCommandInteractionData,
 | 
				
			||||||
 | 
					                ApplicationCommandInteractionDataOption, ApplicationCommandOptionType,
 | 
				
			||||||
 | 
					                ApplicationCommandType,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    ApplicationCommandOrAutocompleteInteraction,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use serde_json::Number;
 | 
				
			||||||
 | 
					use sqlx::Executor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{Context, Data, Error};
 | 
					use crate::Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Func<U, E> = for<'a> fn(
 | 
					pub struct CommandMacro {
 | 
				
			||||||
    poise::ApplicationContext<'a, U, E>,
 | 
					 | 
				
			||||||
) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn default_none<U, E>() -> Option<Func<U, E>> {
 | 
					 | 
				
			||||||
    None
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct RecordedCommand<U, E> {
 | 
					 | 
				
			||||||
    #[serde(skip)]
 | 
					 | 
				
			||||||
    #[serde(default = "default_none::<U, E>")]
 | 
					 | 
				
			||||||
    pub action: Option<Func<U, E>>,
 | 
					 | 
				
			||||||
    pub command_name: String,
 | 
					 | 
				
			||||||
    pub options: Vec<ApplicationCommandInteractionDataOption>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct CommandMacro<U, E> {
 | 
					 | 
				
			||||||
    pub guild_id: GuildId,
 | 
					    pub guild_id: GuildId,
 | 
				
			||||||
    pub name: String,
 | 
					    pub name: String,
 | 
				
			||||||
    pub description: Option<String>,
 | 
					    pub description: Option<String>,
 | 
				
			||||||
    pub commands: Vec<RecordedCommand<U, E>>,
 | 
					    pub commands: Vec<CommandOptions>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn guild_command_macro(
 | 
					impl CommandMacro {
 | 
				
			||||||
    ctx: &Context<'_>,
 | 
					    pub async fn from_guild(
 | 
				
			||||||
    name: &str,
 | 
					        db_pool: impl Executor<'_, Database = Database>,
 | 
				
			||||||
) -> Option<CommandMacro<Data, Error>> {
 | 
					        guild_id: impl Into<GuildId>,
 | 
				
			||||||
    let row = sqlx::query!(
 | 
					    ) -> Vec<Self> {
 | 
				
			||||||
        "
 | 
					        let guild_id = guild_id.into();
 | 
				
			||||||
SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?
 | 
					
 | 
				
			||||||
        ",
 | 
					        sqlx::query!(
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					            "SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
				
			||||||
        name
 | 
					            guild_id.0
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    .fetch_one(&ctx.data().database)
 | 
					        .fetch_all(db_pool)
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
    .ok()?;
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut commands: Vec<RecordedCommand<Data, Error>> =
 | 
					 | 
				
			||||||
        serde_json::from_str(&row.commands).unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for recorded_command in &mut commands {
 | 
					 | 
				
			||||||
        let command = &ctx
 | 
					 | 
				
			||||||
            .framework()
 | 
					 | 
				
			||||||
            .options()
 | 
					 | 
				
			||||||
            .commands
 | 
					 | 
				
			||||||
        .iter()
 | 
					        .iter()
 | 
				
			||||||
            .find(|c| c.identifying_name == recorded_command.command_name);
 | 
					        .map(|row| Self {
 | 
				
			||||||
 | 
					            guild_id,
 | 
				
			||||||
        recorded_command.action = command.map(|c| c.slash_action).flatten();
 | 
					            name: row.name.clone(),
 | 
				
			||||||
 | 
					            description: row.description.clone(),
 | 
				
			||||||
 | 
					            commands: serde_json::from_str(&row.commands).unwrap(),
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .collect::<Vec<Self>>()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let command_macro = CommandMacro {
 | 
					#[derive(Serialize, Deserialize, Clone)]
 | 
				
			||||||
        guild_id: ctx.guild_id().unwrap(),
 | 
					pub enum OptionValue {
 | 
				
			||||||
        name: row.name,
 | 
					    String(String),
 | 
				
			||||||
        description: row.description,
 | 
					    Integer(i64),
 | 
				
			||||||
        commands,
 | 
					    Boolean(bool),
 | 
				
			||||||
    };
 | 
					    User(UserId),
 | 
				
			||||||
 | 
					    Channel(ChannelId),
 | 
				
			||||||
    Some(command_macro)
 | 
					    Role(RoleId),
 | 
				
			||||||
 | 
					    Mentionable(u64),
 | 
				
			||||||
 | 
					    Number(f64),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl OptionValue {
 | 
				
			||||||
 | 
					    pub fn as_i64(&self) -> Option<i64> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            OptionValue::Integer(i) => Some(*i),
 | 
				
			||||||
 | 
					            _ => None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn as_bool(&self) -> Option<bool> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            OptionValue::Boolean(b) => Some(*b),
 | 
				
			||||||
 | 
					            _ => None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn as_channel_id(&self) -> Option<ChannelId> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            OptionValue::Channel(c) => Some(*c),
 | 
				
			||||||
 | 
					            _ => None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn to_string(&self) -> String {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            OptionValue::String(s) => s.to_string(),
 | 
				
			||||||
 | 
					            OptionValue::Integer(i) => i.to_string(),
 | 
				
			||||||
 | 
					            OptionValue::Boolean(b) => b.to_string(),
 | 
				
			||||||
 | 
					            OptionValue::User(u) => u.to_string(),
 | 
				
			||||||
 | 
					            OptionValue::Channel(c) => c.to_string(),
 | 
				
			||||||
 | 
					            OptionValue::Role(r) => r.to_string(),
 | 
				
			||||||
 | 
					            OptionValue::Mentionable(m) => m.to_string(),
 | 
				
			||||||
 | 
					            OptionValue::Number(n) => n.to_string(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn as_value(&self) -> Value {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            OptionValue::String(s) => Value::String(s.to_string()),
 | 
				
			||||||
 | 
					            OptionValue::Integer(i) => Value::Number(i.to_owned().into()),
 | 
				
			||||||
 | 
					            OptionValue::Boolean(b) => Value::Bool(b.to_owned()),
 | 
				
			||||||
 | 
					            OptionValue::User(u) => Value::String(u.to_string()),
 | 
				
			||||||
 | 
					            OptionValue::Channel(c) => Value::String(c.to_string()),
 | 
				
			||||||
 | 
					            OptionValue::Role(r) => Value::String(r.to_string()),
 | 
				
			||||||
 | 
					            OptionValue::Mentionable(m) => Value::String(m.to_string()),
 | 
				
			||||||
 | 
					            OptionValue::Number(n) => Value::Number(Number::from_f64(n.to_owned()).unwrap()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn kind(&self) -> ApplicationCommandOptionType {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            OptionValue::String(_) => ApplicationCommandOptionType::String,
 | 
				
			||||||
 | 
					            OptionValue::Integer(_) => ApplicationCommandOptionType::Integer,
 | 
				
			||||||
 | 
					            OptionValue::Boolean(_) => ApplicationCommandOptionType::Boolean,
 | 
				
			||||||
 | 
					            OptionValue::User(_) => ApplicationCommandOptionType::User,
 | 
				
			||||||
 | 
					            OptionValue::Channel(_) => ApplicationCommandOptionType::Channel,
 | 
				
			||||||
 | 
					            OptionValue::Role(_) => ApplicationCommandOptionType::Role,
 | 
				
			||||||
 | 
					            OptionValue::Mentionable(_) => ApplicationCommandOptionType::Mentionable,
 | 
				
			||||||
 | 
					            OptionValue::Number(_) => ApplicationCommandOptionType::Number,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Clone)]
 | 
				
			||||||
 | 
					pub struct CommandOptions {
 | 
				
			||||||
 | 
					    pub command: String,
 | 
				
			||||||
 | 
					    pub subcommand: Option<String>,
 | 
				
			||||||
 | 
					    pub subcommand_group: Option<String>,
 | 
				
			||||||
 | 
					    pub options: HashMap<String, OptionValue>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Into<ApplicationCommandInteractionData> for CommandOptions {
 | 
				
			||||||
 | 
					    fn into(self) -> ApplicationCommandInteractionData {
 | 
				
			||||||
 | 
					        ApplicationCommandInteractionData {
 | 
				
			||||||
 | 
					            name: self.command,
 | 
				
			||||||
 | 
					            kind: ApplicationCommandType::ChatInput,
 | 
				
			||||||
 | 
					            options: self
 | 
				
			||||||
 | 
					                .options
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .map(|(name, value)| ApplicationCommandInteractionDataOption {
 | 
				
			||||||
 | 
					                    name: name.to_string(),
 | 
				
			||||||
 | 
					                    value: Some(value.as_value()),
 | 
				
			||||||
 | 
					                    kind: value.kind(),
 | 
				
			||||||
 | 
					                    options: vec![],
 | 
				
			||||||
 | 
					                    ..Default::default()
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .collect(),
 | 
				
			||||||
 | 
					            ..Default::default()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl CommandOptions {
 | 
				
			||||||
 | 
					    pub fn new(command: impl ToString) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            command: command.to_string(),
 | 
				
			||||||
 | 
					            subcommand: None,
 | 
				
			||||||
 | 
					            subcommand_group: None,
 | 
				
			||||||
 | 
					            options: Default::default(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn populate(&mut self, interaction: &ApplicationCommandInteraction) {
 | 
				
			||||||
 | 
					        fn match_option(
 | 
				
			||||||
 | 
					            option: ApplicationCommandInteractionDataOption,
 | 
				
			||||||
 | 
					            cmd_opts: &mut CommandOptions,
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            match option.kind {
 | 
				
			||||||
 | 
					                ApplicationCommandOptionType::SubCommand => {
 | 
				
			||||||
 | 
					                    cmd_opts.subcommand = Some(option.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    for opt in option.options {
 | 
				
			||||||
 | 
					                        match_option(opt, cmd_opts);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ApplicationCommandOptionType::SubCommandGroup => {
 | 
				
			||||||
 | 
					                    cmd_opts.subcommand_group = Some(option.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    for opt in option.options {
 | 
				
			||||||
 | 
					                        match_option(opt, cmd_opts);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ApplicationCommandOptionType::String => {
 | 
				
			||||||
 | 
					                    cmd_opts.options.insert(
 | 
				
			||||||
 | 
					                        option.name,
 | 
				
			||||||
 | 
					                        OptionValue::String(option.value.unwrap().as_str().unwrap().to_string()),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ApplicationCommandOptionType::Integer => {
 | 
				
			||||||
 | 
					                    cmd_opts.options.insert(
 | 
				
			||||||
 | 
					                        option.name,
 | 
				
			||||||
 | 
					                        OptionValue::Integer(option.value.map(|m| m.as_i64()).flatten().unwrap()),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ApplicationCommandOptionType::Boolean => {
 | 
				
			||||||
 | 
					                    cmd_opts.options.insert(
 | 
				
			||||||
 | 
					                        option.name,
 | 
				
			||||||
 | 
					                        OptionValue::Boolean(option.value.map(|m| m.as_bool()).flatten().unwrap()),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ApplicationCommandOptionType::User => {
 | 
				
			||||||
 | 
					                    cmd_opts.options.insert(
 | 
				
			||||||
 | 
					                        option.name,
 | 
				
			||||||
 | 
					                        OptionValue::User(UserId(
 | 
				
			||||||
 | 
					                            option
 | 
				
			||||||
 | 
					                                .value
 | 
				
			||||||
 | 
					                                .map(|m| m.as_str().map(|s| s.parse::<u64>().ok()))
 | 
				
			||||||
 | 
					                                .flatten()
 | 
				
			||||||
 | 
					                                .flatten()
 | 
				
			||||||
 | 
					                                .unwrap(),
 | 
				
			||||||
 | 
					                        )),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ApplicationCommandOptionType::Channel => {
 | 
				
			||||||
 | 
					                    cmd_opts.options.insert(
 | 
				
			||||||
 | 
					                        option.name,
 | 
				
			||||||
 | 
					                        OptionValue::Channel(ChannelId(
 | 
				
			||||||
 | 
					                            option
 | 
				
			||||||
 | 
					                                .value
 | 
				
			||||||
 | 
					                                .map(|m| m.as_str().map(|s| s.parse::<u64>().ok()))
 | 
				
			||||||
 | 
					                                .flatten()
 | 
				
			||||||
 | 
					                                .flatten()
 | 
				
			||||||
 | 
					                                .unwrap(),
 | 
				
			||||||
 | 
					                        )),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ApplicationCommandOptionType::Role => {
 | 
				
			||||||
 | 
					                    cmd_opts.options.insert(
 | 
				
			||||||
 | 
					                        option.name,
 | 
				
			||||||
 | 
					                        OptionValue::Role(RoleId(
 | 
				
			||||||
 | 
					                            option
 | 
				
			||||||
 | 
					                                .value
 | 
				
			||||||
 | 
					                                .map(|m| m.as_str().map(|s| s.parse::<u64>().ok()))
 | 
				
			||||||
 | 
					                                .flatten()
 | 
				
			||||||
 | 
					                                .flatten()
 | 
				
			||||||
 | 
					                                .unwrap(),
 | 
				
			||||||
 | 
					                        )),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ApplicationCommandOptionType::Mentionable => {
 | 
				
			||||||
 | 
					                    cmd_opts.options.insert(
 | 
				
			||||||
 | 
					                        option.name,
 | 
				
			||||||
 | 
					                        OptionValue::Mentionable(
 | 
				
			||||||
 | 
					                            option.value.map(|m| m.as_u64()).flatten().unwrap(),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ApplicationCommandOptionType::Number => {
 | 
				
			||||||
 | 
					                    cmd_opts.options.insert(
 | 
				
			||||||
 | 
					                        option.name,
 | 
				
			||||||
 | 
					                        OptionValue::Number(option.value.map(|m| m.as_f64()).flatten().unwrap()),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                _ => {}
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for option in &interaction.data.options {
 | 
				
			||||||
 | 
					            match_option(option.clone(), self)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,20 +9,21 @@ use poise::serenity::{async_trait, model::id::UserId};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    models::{channel_data::ChannelData, user_data::UserData},
 | 
					    models::{channel_data::ChannelData, user_data::UserData},
 | 
				
			||||||
    CommandMacro, Context, Data, Error, GuildId,
 | 
					    Context,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[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, Box<dyn std::error::Error + Sync + Send>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn author_data(&self) -> Result<UserData, Error>;
 | 
					    async fn author_data(&self) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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, Box<dyn std::error::Error + Sync + Send>>;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[async_trait]
 | 
					#[async_trait]
 | 
				
			||||||
@@ -47,29 +48,4 @@ impl CtxData for Context<'_> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        ChannelData::from_channel(&channel, &self.data().database).await
 | 
					        ChannelData::from_channel(&channel, &self.data().database).await
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
					 | 
				
			||||||
        self.data().command_macros(self.guild_id().unwrap()).await
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Data {
 | 
					 | 
				
			||||||
    pub(crate) async fn command_macros(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        guild_id: GuildId,
 | 
					 | 
				
			||||||
    ) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
					 | 
				
			||||||
        let rows = sqlx::query!(
 | 
					 | 
				
			||||||
            "SELECT name, description, commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
					 | 
				
			||||||
            guild_id.0
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_all(&self.database)
 | 
					 | 
				
			||||||
        .await?.iter().map(|row| CommandMacro {
 | 
					 | 
				
			||||||
            guild_id,
 | 
					 | 
				
			||||||
            name: row.name.clone(),
 | 
					 | 
				
			||||||
            description: row.description.clone(),
 | 
					 | 
				
			||||||
            commands: serde_json::from_str(&row.commands).unwrap(),
 | 
					 | 
				
			||||||
        }).collect();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(rows)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,8 +14,7 @@ use poise::serenity::{
 | 
				
			|||||||
use sqlx::MySqlPool;
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    consts::{DAY, DEFAULT_AVATAR, MAX_TIME, MIN_INTERVAL},
 | 
					    consts::{DEFAULT_AVATAR, MAX_TIME, MIN_INTERVAL},
 | 
				
			||||||
    interval_parser::Interval,
 | 
					 | 
				
			||||||
    models::{
 | 
					    models::{
 | 
				
			||||||
        channel_data::ChannelData,
 | 
					        channel_data::ChannelData,
 | 
				
			||||||
        reminder::{content::Content, errors::ReminderError, helper::generate_uid, Reminder},
 | 
					        reminder::{content::Content, errors::ReminderError, helper::generate_uid, Reminder},
 | 
				
			||||||
@@ -53,8 +52,7 @@ pub struct ReminderBuilder {
 | 
				
			|||||||
    channel: u32,
 | 
					    channel: u32,
 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					    utc_time: NaiveDateTime,
 | 
				
			||||||
    timezone: String,
 | 
					    timezone: String,
 | 
				
			||||||
    interval_secs: Option<i64>,
 | 
					    interval: Option<i64>,
 | 
				
			||||||
    interval_months: Option<i64>,
 | 
					 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					    expires: Option<NaiveDateTime>,
 | 
				
			||||||
    content: String,
 | 
					    content: String,
 | 
				
			||||||
    tts: bool,
 | 
					    tts: bool,
 | 
				
			||||||
@@ -86,8 +84,7 @@ INSERT INTO reminders (
 | 
				
			|||||||
    `channel_id`,
 | 
					    `channel_id`,
 | 
				
			||||||
    `utc_time`,
 | 
					    `utc_time`,
 | 
				
			||||||
    `timezone`,
 | 
					    `timezone`,
 | 
				
			||||||
    `interval_seconds`,
 | 
					    `interval`,
 | 
				
			||||||
    `interval_months`,
 | 
					 | 
				
			||||||
    `expires`,
 | 
					    `expires`,
 | 
				
			||||||
    `content`,
 | 
					    `content`,
 | 
				
			||||||
    `tts`,
 | 
					    `tts`,
 | 
				
			||||||
@@ -105,7 +102,6 @@ INSERT INTO reminders (
 | 
				
			|||||||
    ?,
 | 
					    ?,
 | 
				
			||||||
    ?,
 | 
					    ?,
 | 
				
			||||||
    ?,
 | 
					    ?,
 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?
 | 
					    ?
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
@@ -113,8 +109,7 @@ INSERT INTO reminders (
 | 
				
			|||||||
                        self.channel,
 | 
					                        self.channel,
 | 
				
			||||||
                        utc_time,
 | 
					                        utc_time,
 | 
				
			||||||
                        self.timezone,
 | 
					                        self.timezone,
 | 
				
			||||||
                        self.interval_secs,
 | 
					                        self.interval,
 | 
				
			||||||
                        self.interval_months,
 | 
					 | 
				
			||||||
                        self.expires,
 | 
					                        self.expires,
 | 
				
			||||||
                        self.content,
 | 
					                        self.content,
 | 
				
			||||||
                        self.tts,
 | 
					                        self.tts,
 | 
				
			||||||
@@ -126,7 +121,7 @@ INSERT INTO reminders (
 | 
				
			|||||||
                    .await
 | 
					                    .await
 | 
				
			||||||
                    .unwrap();
 | 
					                    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Ok(Reminder::from_uid(&self.pool, &self.uid).await.unwrap())
 | 
					                    Ok(Reminder::from_uid(&self.pool, self.uid).await.unwrap())
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -139,7 +134,7 @@ pub struct MultiReminderBuilder<'a> {
 | 
				
			|||||||
    scopes: Vec<ReminderScope>,
 | 
					    scopes: Vec<ReminderScope>,
 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					    utc_time: NaiveDateTime,
 | 
				
			||||||
    timezone: Tz,
 | 
					    timezone: Tz,
 | 
				
			||||||
    interval: Option<Interval>,
 | 
					    interval: Option<i64>,
 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					    expires: Option<NaiveDateTime>,
 | 
				
			||||||
    content: Content,
 | 
					    content: Content,
 | 
				
			||||||
    set_by: Option<u32>,
 | 
					    set_by: Option<u32>,
 | 
				
			||||||
@@ -148,7 +143,7 @@ pub struct MultiReminderBuilder<'a> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl<'a> MultiReminderBuilder<'a> {
 | 
					impl<'a> MultiReminderBuilder<'a> {
 | 
				
			||||||
    pub fn new(ctx: &'a Context, guild_id: Option<GuildId>) -> Self {
 | 
					    pub fn new(ctx: &'a Context<'a>, guild_id: Option<GuildId>) -> Self {
 | 
				
			||||||
        MultiReminderBuilder {
 | 
					        MultiReminderBuilder {
 | 
				
			||||||
            scopes: vec![],
 | 
					            scopes: vec![],
 | 
				
			||||||
            utc_time: Utc::now().naive_utc(),
 | 
					            utc_time: Utc::now().naive_utc(),
 | 
				
			||||||
@@ -162,12 +157,6 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn timezone(mut self, timezone: Tz) -> Self {
 | 
					 | 
				
			||||||
        self.timezone = timezone;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn content(mut self, content: Content) -> Self {
 | 
					    pub fn content(mut self, content: Content) -> Self {
 | 
				
			||||||
        self.content = content;
 | 
					        self.content = content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -197,7 +186,7 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn interval(mut self, interval: Option<Interval>) -> Self {
 | 
					    pub fn interval(mut self, interval: Option<i64>) -> Self {
 | 
				
			||||||
        self.interval = interval;
 | 
					        self.interval = interval;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
@@ -207,26 +196,23 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
        self.scopes = scopes;
 | 
					        self.scopes = scopes;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn build(self) -> (HashSet<ReminderError>, HashSet<(Reminder, ReminderScope)>) {
 | 
					    pub async fn build(self) -> (HashSet<ReminderError>, HashSet<ReminderScope>) {
 | 
				
			||||||
 | 
					        let pool = self.ctx.data().database.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut errors = HashSet::new();
 | 
					        let mut errors = HashSet::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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 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 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 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 {
 | 
				
			||||||
                            let user_data = UserData::from_user(
 | 
					                            let user_data = UserData::from_user(&user, &self.ctx.discord(), &pool)
 | 
				
			||||||
                                &user,
 | 
					 | 
				
			||||||
                                &self.ctx.discord(),
 | 
					 | 
				
			||||||
                                &self.ctx.data().database,
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                                .await
 | 
					                                .await
 | 
				
			||||||
                                .unwrap();
 | 
					                                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -252,9 +238,7 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
                                Err(ReminderError::InvalidTag)
 | 
					                                Err(ReminderError::InvalidTag)
 | 
				
			||||||
                            } else {
 | 
					                            } else {
 | 
				
			||||||
                                let mut channel_data =
 | 
					                                let mut channel_data =
 | 
				
			||||||
                                    ChannelData::from_channel(&channel, &self.ctx.data().database)
 | 
					                                    ChannelData::from_channel(&channel, &pool).await.unwrap();
 | 
				
			||||||
                                        .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()
 | 
				
			||||||
@@ -271,9 +255,7 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
                                                Some(webhook.id.as_u64().to_owned());
 | 
					                                                Some(webhook.id.as_u64().to_owned());
 | 
				
			||||||
                                            channel_data.webhook_token = webhook.token;
 | 
					                                            channel_data.webhook_token = webhook.token;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                            channel_data
 | 
					                                            channel_data.commit_changes(&pool).await;
 | 
				
			||||||
                                                .commit_changes(&self.ctx.data().database)
 | 
					 | 
				
			||||||
                                                .await;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                            Ok(channel_data.id)
 | 
					                                            Ok(channel_data.id)
 | 
				
			||||||
                                        }
 | 
					                                        }
 | 
				
			||||||
@@ -293,13 +275,12 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
                match db_channel_id {
 | 
					                match db_channel_id {
 | 
				
			||||||
                    Ok(c) => {
 | 
					                    Ok(c) => {
 | 
				
			||||||
                        let builder = ReminderBuilder {
 | 
					                        let builder = ReminderBuilder {
 | 
				
			||||||
                            pool: self.ctx.data().database.clone(),
 | 
					                            pool: pool.clone(),
 | 
				
			||||||
                            uid: generate_uid(),
 | 
					                            uid: generate_uid(),
 | 
				
			||||||
                            channel: c,
 | 
					                            channel: c,
 | 
				
			||||||
                            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: self.interval,
 | 
				
			||||||
                            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(),
 | 
				
			||||||
                            tts: self.content.tts,
 | 
					                            tts: self.content.tts,
 | 
				
			||||||
@@ -309,8 +290,8 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
                        };
 | 
					                        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        match builder.build().await {
 | 
					                        match builder.build().await {
 | 
				
			||||||
                            Ok(r) => {
 | 
					                            Ok(_) => {
 | 
				
			||||||
                                ok_locs.insert((r, scope));
 | 
					                                ok_locs.insert(scope);
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            Err(e) => {
 | 
					                            Err(e) => {
 | 
				
			||||||
                                errors.insert(e);
 | 
					                                errors.insert(e);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,25 @@
 | 
				
			|||||||
 | 
					use num_integer::Integer;
 | 
				
			||||||
use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
					use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::consts::CHARACTERS;
 | 
					use crate::consts::{CHARACTERS, DAY, HOUR, MINUTE};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn longhand_displacement(seconds: u64) -> String {
 | 
				
			||||||
 | 
					    let (days, seconds) = seconds.div_rem(&DAY);
 | 
				
			||||||
 | 
					    let (hours, seconds) = seconds.div_rem(&HOUR);
 | 
				
			||||||
 | 
					    let (minutes, seconds) = seconds.div_rem(&MINUTE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut sections = vec![];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var, name) in
 | 
				
			||||||
 | 
					        [days, hours, minutes, seconds].iter().zip(["days", "hours", "minutes", "seconds"].iter())
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if *var > 0 {
 | 
				
			||||||
 | 
					            sections.push(format!("{} {}", var, name));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sections.join(", ")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn generate_uid() -> String {
 | 
					pub fn generate_uid() -> String {
 | 
				
			||||||
    let mut generator: OsRng = Default::default();
 | 
					    let mut generator: OsRng = Default::default();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,19 +4,17 @@ pub mod errors;
 | 
				
			|||||||
mod helper;
 | 
					mod helper;
 | 
				
			||||||
pub mod look_flags;
 | 
					pub mod look_flags;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::hash::{Hash, Hasher};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::{NaiveDateTime, TimeZone};
 | 
					use chrono::{NaiveDateTime, TimeZone};
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use poise::{
 | 
					use poise::serenity::model::id::{ChannelId, GuildId, UserId};
 | 
				
			||||||
    serenity::model::id::{ChannelId, GuildId, UserId},
 | 
					use sqlx::{Executor, MySqlPool};
 | 
				
			||||||
    serenity_prelude::Cache,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::Executor;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    models::reminder::look_flags::{LookFlags, TimeDisplayType},
 | 
					    models::reminder::{
 | 
				
			||||||
    Database,
 | 
					        helper::longhand_displacement,
 | 
				
			||||||
 | 
					        look_flags::{LookFlags, TimeDisplayType},
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Context, Database,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
@@ -25,8 +23,7 @@ pub struct Reminder {
 | 
				
			|||||||
    pub uid: String,
 | 
					    pub uid: String,
 | 
				
			||||||
    pub channel: u64,
 | 
					    pub channel: u64,
 | 
				
			||||||
    pub utc_time: NaiveDateTime,
 | 
					    pub utc_time: NaiveDateTime,
 | 
				
			||||||
    pub interval_seconds: Option<u32>,
 | 
					    pub interval: Option<u32>,
 | 
				
			||||||
    pub interval_months: Option<u32>,
 | 
					 | 
				
			||||||
    pub expires: Option<NaiveDateTime>,
 | 
					    pub expires: Option<NaiveDateTime>,
 | 
				
			||||||
    pub enabled: bool,
 | 
					    pub enabled: bool,
 | 
				
			||||||
    pub content: String,
 | 
					    pub content: String,
 | 
				
			||||||
@@ -34,22 +31,8 @@ pub struct Reminder {
 | 
				
			|||||||
    pub set_by: Option<u64>,
 | 
					    pub set_by: Option<u64>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Hash for Reminder {
 | 
					 | 
				
			||||||
    fn hash<H: Hasher>(&self, state: &mut H) {
 | 
					 | 
				
			||||||
        self.uid.hash(state);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl PartialEq<Self> for Reminder {
 | 
					 | 
				
			||||||
    fn eq(&self, other: &Self) -> bool {
 | 
					 | 
				
			||||||
        self.uid == other.uid
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Eq for Reminder {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Reminder {
 | 
					impl Reminder {
 | 
				
			||||||
    pub async fn from_uid(pool: impl Executor<'_, Database = Database>, uid: &str) -> Option<Self> {
 | 
					    pub async fn from_uid(pool: &MySqlPool, uid: String) -> Option<Self> {
 | 
				
			||||||
        sqlx::query_as_unchecked!(
 | 
					        sqlx::query_as_unchecked!(
 | 
				
			||||||
            Self,
 | 
					            Self,
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
@@ -58,8 +41,7 @@ SELECT
 | 
				
			|||||||
    reminders.uid,
 | 
					    reminders.uid,
 | 
				
			||||||
    channels.channel,
 | 
					    channels.channel,
 | 
				
			||||||
    reminders.utc_time,
 | 
					    reminders.utc_time,
 | 
				
			||||||
    reminders.interval_seconds,
 | 
					    reminders.interval,
 | 
				
			||||||
    reminders.interval_months,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					    reminders.expires,
 | 
				
			||||||
    reminders.enabled,
 | 
					    reminders.enabled,
 | 
				
			||||||
    reminders.content,
 | 
					    reminders.content,
 | 
				
			||||||
@@ -85,44 +67,8 @@ WHERE
 | 
				
			|||||||
        .ok()
 | 
					        .ok()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn from_id(pool: impl Executor<'_, Database = Database>, id: u32) -> Option<Self> {
 | 
					 | 
				
			||||||
        sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Self,
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    reminders.id,
 | 
					 | 
				
			||||||
    reminders.uid,
 | 
					 | 
				
			||||||
    channels.channel,
 | 
					 | 
				
			||||||
    reminders.utc_time,
 | 
					 | 
				
			||||||
    reminders.interval_seconds,
 | 
					 | 
				
			||||||
    reminders.interval_months,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					 | 
				
			||||||
    reminders.enabled,
 | 
					 | 
				
			||||||
    reminders.content,
 | 
					 | 
				
			||||||
    reminders.embed_description,
 | 
					 | 
				
			||||||
    users.user AS set_by
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
INNER JOIN
 | 
					 | 
				
			||||||
    channels
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.channel_id = channels.id
 | 
					 | 
				
			||||||
LEFT JOIN
 | 
					 | 
				
			||||||
    users
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.set_by = users.id
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    reminders.id = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .ok()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn from_channel<C: Into<ChannelId>>(
 | 
					    pub async fn from_channel<C: Into<ChannelId>>(
 | 
				
			||||||
        pool: impl Executor<'_, Database = Database>,
 | 
					        db_pool: impl Executor<'_, Database = Database>,
 | 
				
			||||||
        channel_id: C,
 | 
					        channel_id: C,
 | 
				
			||||||
        flags: &LookFlags,
 | 
					        flags: &LookFlags,
 | 
				
			||||||
    ) -> Vec<Self> {
 | 
					    ) -> Vec<Self> {
 | 
				
			||||||
@@ -137,8 +83,7 @@ SELECT
 | 
				
			|||||||
    reminders.uid,
 | 
					    reminders.uid,
 | 
				
			||||||
    channels.channel,
 | 
					    channels.channel,
 | 
				
			||||||
    reminders.utc_time,
 | 
					    reminders.utc_time,
 | 
				
			||||||
    reminders.interval_seconds,
 | 
					    reminders.interval,
 | 
				
			||||||
    reminders.interval_months,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					    reminders.expires,
 | 
				
			||||||
    reminders.enabled,
 | 
					    reminders.enabled,
 | 
				
			||||||
    reminders.content,
 | 
					    reminders.content,
 | 
				
			||||||
@@ -163,19 +108,21 @@ ORDER BY
 | 
				
			|||||||
            channel_id.as_u64(),
 | 
					            channel_id.as_u64(),
 | 
				
			||||||
            enabled,
 | 
					            enabled,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .fetch_all(pool)
 | 
					        .fetch_all(db_pool)
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn from_guild(
 | 
					    pub async fn from_guild(
 | 
				
			||||||
        cache: impl AsRef<Cache>,
 | 
					        ctx: &Context<'_>,
 | 
				
			||||||
        pool: impl Executor<'_, Database = Database>,
 | 
					 | 
				
			||||||
        guild_id: Option<GuildId>,
 | 
					        guild_id: Option<GuildId>,
 | 
				
			||||||
        user: UserId,
 | 
					        user: UserId,
 | 
				
			||||||
    ) -> Vec<Self> {
 | 
					    ) -> Vec<Self> {
 | 
				
			||||||
 | 
					        // todo: see if this can be moved to just extract from the context
 | 
				
			||||||
 | 
					        let pool = ctx.data().database.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(guild_id) = guild_id {
 | 
					        if let Some(guild_id) = guild_id {
 | 
				
			||||||
            let guild_opt = guild_id.to_guild_cached(cache);
 | 
					            let guild_opt = guild_id.to_guild_cached(&ctx.discord());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if let Some(guild) = guild_opt {
 | 
					            if let Some(guild) = guild_opt {
 | 
				
			||||||
                let channels = guild
 | 
					                let channels = guild
 | 
				
			||||||
@@ -194,8 +141,7 @@ SELECT
 | 
				
			|||||||
    reminders.uid,
 | 
					    reminders.uid,
 | 
				
			||||||
    channels.channel,
 | 
					    channels.channel,
 | 
				
			||||||
    reminders.utc_time,
 | 
					    reminders.utc_time,
 | 
				
			||||||
    reminders.interval_seconds,
 | 
					    reminders.interval,
 | 
				
			||||||
    reminders.interval_months,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					    reminders.expires,
 | 
				
			||||||
    reminders.enabled,
 | 
					    reminders.enabled,
 | 
				
			||||||
    reminders.content,
 | 
					    reminders.content,
 | 
				
			||||||
@@ -216,7 +162,7 @@ WHERE
 | 
				
			|||||||
                ",
 | 
					                ",
 | 
				
			||||||
                    channels
 | 
					                    channels
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .fetch_all(pool)
 | 
					                .fetch_all(&pool)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                sqlx::query_as_unchecked!(
 | 
					                sqlx::query_as_unchecked!(
 | 
				
			||||||
@@ -227,8 +173,7 @@ SELECT
 | 
				
			|||||||
    reminders.uid,
 | 
					    reminders.uid,
 | 
				
			||||||
    channels.channel,
 | 
					    channels.channel,
 | 
				
			||||||
    reminders.utc_time,
 | 
					    reminders.utc_time,
 | 
				
			||||||
    reminders.interval_seconds,
 | 
					    reminders.interval,
 | 
				
			||||||
    reminders.interval_months,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					    reminders.expires,
 | 
				
			||||||
    reminders.enabled,
 | 
					    reminders.enabled,
 | 
				
			||||||
    reminders.content,
 | 
					    reminders.content,
 | 
				
			||||||
@@ -249,7 +194,7 @@ WHERE
 | 
				
			|||||||
                ",
 | 
					                ",
 | 
				
			||||||
                    guild_id.as_u64()
 | 
					                    guild_id.as_u64()
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .fetch_all(pool)
 | 
					                .fetch_all(&pool)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@@ -261,8 +206,7 @@ SELECT
 | 
				
			|||||||
    reminders.uid,
 | 
					    reminders.uid,
 | 
				
			||||||
    channels.channel,
 | 
					    channels.channel,
 | 
				
			||||||
    reminders.utc_time,
 | 
					    reminders.utc_time,
 | 
				
			||||||
    reminders.interval_seconds,
 | 
					    reminders.interval,
 | 
				
			||||||
    reminders.interval_months,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					    reminders.expires,
 | 
				
			||||||
    reminders.enabled,
 | 
					    reminders.enabled,
 | 
				
			||||||
    reminders.content,
 | 
					    reminders.content,
 | 
				
			||||||
@@ -283,19 +227,12 @@ WHERE
 | 
				
			|||||||
            ",
 | 
					            ",
 | 
				
			||||||
                user.as_u64()
 | 
					                user.as_u64()
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .fetch_all(pool)
 | 
					            .fetch_all(&pool)
 | 
				
			||||||
            .await
 | 
					            .await
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn delete(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        db: impl Executor<'_, Database = Database>,
 | 
					 | 
				
			||||||
    ) -> Result<(), sqlx::Error> {
 | 
					 | 
				
			||||||
        sqlx::query!("DELETE FROM reminders WHERE uid = ?", self.uid).execute(db).await.map(|_| ())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn display_content(&self) -> &str {
 | 
					    pub fn display_content(&self) -> &str {
 | 
				
			||||||
        if self.content.is_empty() {
 | 
					        if self.content.is_empty() {
 | 
				
			||||||
            &self.embed_description
 | 
					            &self.embed_description
 | 
				
			||||||
@@ -310,7 +247,10 @@ 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")
 | 
					            timezone
 | 
				
			||||||
 | 
					                .timestamp(self.utc_time.timestamp(), 0)
 | 
				
			||||||
 | 
					                .format("%Y-%m-%d %H:%M:%S")
 | 
				
			||||||
 | 
					                .to_string()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -324,11 +264,12 @@ WHERE
 | 
				
			|||||||
            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 let Some(interval) = self.interval {
 | 
				
			||||||
            format!(
 | 
					            format!(
 | 
				
			||||||
                "'{}' *occurs next at* **{}**, repeating (set by {})",
 | 
					                "'{}' *occurs next at* **{}**, repeating every **{}** (set by {})",
 | 
				
			||||||
                self.display_content(),
 | 
					                self.display_content(),
 | 
				
			||||||
                time_display,
 | 
					                time_display,
 | 
				
			||||||
 | 
					                longhand_displacement(interval as u64),
 | 
				
			||||||
                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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,7 +71,7 @@ INSERT IGNORE INTO channels (channel) VALUES (?)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                sqlx::query!(
 | 
					                sqlx::query!(
 | 
				
			||||||
                    "
 | 
					                    "
 | 
				
			||||||
INSERT INTO users (name, user, dm_channel, timezone) VALUES ('', ?, (SELECT id FROM channels WHERE channel = ?), ?)
 | 
					INSERT INTO users (user, dm_channel, timezone) VALUES (?, (SELECT id FROM channels WHERE channel = ?), ?)
 | 
				
			||||||
                    ",
 | 
					                    ",
 | 
				
			||||||
                    user_id.0,
 | 
					                    user_id.0,
 | 
				
			||||||
                    dm_channel.id.0,
 | 
					                    dm_channel.id.0,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -211,12 +211,14 @@ pub async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
 | 
				
			|||||||
        .output()
 | 
					        .output()
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .ok()
 | 
					        .ok()
 | 
				
			||||||
        .and_then(|inner| {
 | 
					        .map(|inner| {
 | 
				
			||||||
            if inner.status.success() {
 | 
					            if inner.status.success() {
 | 
				
			||||||
                Some(from_utf8(&*inner.stdout).unwrap().parse::<i64>().unwrap())
 | 
					                Some(from_utf8(&*inner.stdout).unwrap().parse::<i64>().unwrap())
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                None
 | 
					                None
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .and_then(|inner| if inner < 0 { None } else { Some(inner) })
 | 
					        .flatten()
 | 
				
			||||||
 | 
					        .map(|inner| if inner < 0 { None } else { Some(inner) })
 | 
				
			||||||
 | 
					        .flatten()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										42
									
								
								src/utils.rs
									
									
									
									
									
								
							
							
						
						@@ -1,10 +1,7 @@
 | 
				
			|||||||
use poise::{
 | 
					use poise::serenity::{
 | 
				
			||||||
    serenity::{
 | 
					 | 
				
			||||||
    builder::CreateApplicationCommands,
 | 
					    builder::CreateApplicationCommands,
 | 
				
			||||||
    http::CacheHttp,
 | 
					    http::CacheHttp,
 | 
				
			||||||
    model::id::{GuildId, UserId},
 | 
					    model::id::{GuildId, UserId},
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    serenity_prelude as serenity,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
@@ -68,40 +65,3 @@ pub async fn check_guild_subscription(
 | 
				
			|||||||
        false
 | 
					        false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Sends the message, specified via [`crate::CreateReply`], to the interaction initial response
 | 
					 | 
				
			||||||
/// endpoint
 | 
					 | 
				
			||||||
pub fn send_as_initial_response(
 | 
					 | 
				
			||||||
    data: poise::CreateReply<'_>,
 | 
					 | 
				
			||||||
    f: &mut serenity::CreateInteractionResponseData,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    let poise::CreateReply {
 | 
					 | 
				
			||||||
        content,
 | 
					 | 
				
			||||||
        embeds,
 | 
					 | 
				
			||||||
        attachments: _, // serenity doesn't support attachments in initial response yet
 | 
					 | 
				
			||||||
        components,
 | 
					 | 
				
			||||||
        ephemeral,
 | 
					 | 
				
			||||||
        allowed_mentions,
 | 
					 | 
				
			||||||
        reference_message: _, // can't reply to a message in interactions
 | 
					 | 
				
			||||||
    } = data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if let Some(content) = content {
 | 
					 | 
				
			||||||
        f.content(content);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    f.set_embeds(embeds);
 | 
					 | 
				
			||||||
    if let Some(allowed_mentions) = allowed_mentions {
 | 
					 | 
				
			||||||
        f.allowed_mentions(|f| {
 | 
					 | 
				
			||||||
            *f = allowed_mentions.clone();
 | 
					 | 
				
			||||||
            f
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if let Some(components) = components {
 | 
					 | 
				
			||||||
        f.components(|f| {
 | 
					 | 
				
			||||||
            f.0 = components.0;
 | 
					 | 
				
			||||||
            f
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if ephemeral {
 | 
					 | 
				
			||||||
        f.flags(serenity::InteractionApplicationCommandCallbackDataFlags::EPHEMERAL);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "reminder_web"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
authors = ["jellywx <judesouthworth@pm.me>"]
 | 
					 | 
				
			||||||
edition = "2018"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] }
 | 
					 | 
				
			||||||
rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] }
 | 
					 | 
				
			||||||
serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
					 | 
				
			||||||
oauth2 = "4"
 | 
					 | 
				
			||||||
log = "0.4"
 | 
					 | 
				
			||||||
reqwest = "0.11"
 | 
					 | 
				
			||||||
serde = { version = "1.0", features = ["derive"] }
 | 
					 | 
				
			||||||
serde_json = "1.0"
 | 
					 | 
				
			||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] }
 | 
					 | 
				
			||||||
chrono = "0.4"
 | 
					 | 
				
			||||||
chrono-tz = "0.5"
 | 
					 | 
				
			||||||
lazy_static = "1.4.0"
 | 
					 | 
				
			||||||
rand = "0.7"
 | 
					 | 
				
			||||||
base64 = "0.13"
 | 
					 | 
				
			||||||
@@ -1,32 +0,0 @@
 | 
				
			|||||||
-----BEGIN CERTIFICATE-----
 | 
					 | 
				
			||||||
MIIFbzCCA1egAwIBAgIUUY2fYP5h41dtqcuNLVUS99N7+jAwDQYJKoZIhvcNAQEL
 | 
					 | 
				
			||||||
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
					 | 
				
			||||||
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
					 | 
				
			||||||
MDUxNTE5MDIyNFowRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQK
 | 
					 | 
				
			||||||
DAlSb2NrZXQgQ0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMIICIjANBgkqhkiG
 | 
					 | 
				
			||||||
9w0BAQEFAAOCAg8AMIICCgKCAgEA3MmVOUxbZ0rtBBbXjWkkM2n2IoPuqfiENdrM
 | 
					 | 
				
			||||||
NH9IcIbEgFb7AQ/c2vGBLWWw2RFtXxe2ubbgcHyAySQ+ENGEf+W2yox5JpcOOBP+
 | 
					 | 
				
			||||||
/KrUQ4ROqxLBWOD6+fA/q5m9/CXXJosWXly3DFE9nUEUlNXcaSxJm/bxIqnxwEMQ
 | 
					 | 
				
			||||||
NESWdG1qlsGVhHLi5sflgwAcYH+nZzdtINLyvQ5AGIs8s6LpmvUVbC3I+5oNNSwW
 | 
					 | 
				
			||||||
rRsIfCFaYLuUx7jZMaOk8Dfz8MbFL68+7OCGwmhduSHoGW8jVzDXwXv/crAmFtau
 | 
					 | 
				
			||||||
zmu43/vPDwKa4w/kQJLNr1QmiGz9FBt8DXVJ0s5bokOIlodEgAvjsL3xwnrKak0F
 | 
					 | 
				
			||||||
8ak74IBetfDdVvuC9WWmJ9/dj3z91KvCWLdZGm8ijlYZ2OuaiiJ/oC6iebaPFHEY
 | 
					 | 
				
			||||||
IQmF5wzT/nZLIbQsti9wFnOpptgkhDAcKIAk3+pncuMwZ9WRvWO2IFt+ZCqKCCDU
 | 
					 | 
				
			||||||
JliDZmm3ow/zob8DD6Txex5OkJGUF6iu8o7fjI9BxsjSc76bLrxIhvR43UXjzoHl
 | 
					 | 
				
			||||||
t3lvDH8SzSN/kxzQMXVl48h3xydJ3+ZyDOLle1bqMga8Kcin9GcEUqmL1JcyFafe
 | 
					 | 
				
			||||||
CUE/Nk1pHyrlnhpMCYIDVI3pmNLB0/m/IEWLckSr9+hXyUPJQyWiyrixwC86SPxZ
 | 
					 | 
				
			||||||
AonU2aUCAwEAAaNTMFEwHQYDVR0OBBYEFJ5uQa9kD5fioNeS/Ff/mxDcMAzhMB8G
 | 
					 | 
				
			||||||
A1UdIwQYMBaAFJ5uQa9kD5fioNeS/Ff/mxDcMAzhMA8GA1UdEwEB/wQFMAMBAf8w
 | 
					 | 
				
			||||||
DQYJKoZIhvcNAQELBQADggIBABf7adF1J40/8EjfSJ5XdG7OBUrZTas3+nuULh8B
 | 
					 | 
				
			||||||
6h1igAq0BgX9v3awmPCaxqDLqwJCsFZCk0s9Vgd62McKVKasyW1TQchWbdvlqeAB
 | 
					 | 
				
			||||||
QQfZpJtAZK12dcMl6iznC/JBTPvYl9q1FKS8kYtg19in/xlhxiiEJaL5z+slpTgT
 | 
					 | 
				
			||||||
cN12650W2FqjT9sD9jshZI/a8o53d2yUBXBBecCZ49djceBGLbxbteEi/sb9qM4f
 | 
					 | 
				
			||||||
IUxeSrvK6owxKMZ5okUqZWaFseFCkdMKpMg9eecyfSTweac8yLlZmeqX5D/H85Hr
 | 
					 | 
				
			||||||
hnWHjI5NeVZAoDkdmNeaDbAIv4f3prqC8uFP3ub8D0rG/WiPdOAd79KNgqbUUFyp
 | 
					 | 
				
			||||||
NbjSiAesx4Rm1WIgjHljFQKXJdtnxt+v1VkKV9entXJX2tye7+Z1+zurNYyUyM1J
 | 
					 | 
				
			||||||
COdXeV4jpq2mP8cLpAqX7ip1tNx9YMy5ctbAE1WsUw7lPyO91+VN2Q6MN5OyemY3
 | 
					 | 
				
			||||||
4nBzRJU8X1nsDgeNQWjzb/ZC7eoS916Gywk8Hu6GoIwvYMm3zea25n6mAAOX17vE
 | 
					 | 
				
			||||||
1YdvSzUlnVbwnbgd4BgFRGUfBVjO7qvFC7ZWLngWhBzIIYIGUJfO2rippgkxAoHH
 | 
					 | 
				
			||||||
dgWYk1MNnk2yM8WSo0VKoe4yuF9NwPlfPqeCE683JHdwGEJKZWMocszzHrGZ2KX2
 | 
					 | 
				
			||||||
I4/u
 | 
					 | 
				
			||||||
-----END CERTIFICATE-----
 | 
					 | 
				
			||||||
@@ -1,51 +0,0 @@
 | 
				
			|||||||
-----BEGIN RSA PRIVATE KEY-----
 | 
					 | 
				
			||||||
MIIJKAIBAAKCAgEA3MmVOUxbZ0rtBBbXjWkkM2n2IoPuqfiENdrMNH9IcIbEgFb7
 | 
					 | 
				
			||||||
AQ/c2vGBLWWw2RFtXxe2ubbgcHyAySQ+ENGEf+W2yox5JpcOOBP+/KrUQ4ROqxLB
 | 
					 | 
				
			||||||
WOD6+fA/q5m9/CXXJosWXly3DFE9nUEUlNXcaSxJm/bxIqnxwEMQNESWdG1qlsGV
 | 
					 | 
				
			||||||
hHLi5sflgwAcYH+nZzdtINLyvQ5AGIs8s6LpmvUVbC3I+5oNNSwWrRsIfCFaYLuU
 | 
					 | 
				
			||||||
x7jZMaOk8Dfz8MbFL68+7OCGwmhduSHoGW8jVzDXwXv/crAmFtauzmu43/vPDwKa
 | 
					 | 
				
			||||||
4w/kQJLNr1QmiGz9FBt8DXVJ0s5bokOIlodEgAvjsL3xwnrKak0F8ak74IBetfDd
 | 
					 | 
				
			||||||
VvuC9WWmJ9/dj3z91KvCWLdZGm8ijlYZ2OuaiiJ/oC6iebaPFHEYIQmF5wzT/nZL
 | 
					 | 
				
			||||||
IbQsti9wFnOpptgkhDAcKIAk3+pncuMwZ9WRvWO2IFt+ZCqKCCDUJliDZmm3ow/z
 | 
					 | 
				
			||||||
ob8DD6Txex5OkJGUF6iu8o7fjI9BxsjSc76bLrxIhvR43UXjzoHlt3lvDH8SzSN/
 | 
					 | 
				
			||||||
kxzQMXVl48h3xydJ3+ZyDOLle1bqMga8Kcin9GcEUqmL1JcyFafeCUE/Nk1pHyrl
 | 
					 | 
				
			||||||
nhpMCYIDVI3pmNLB0/m/IEWLckSr9+hXyUPJQyWiyrixwC86SPxZAonU2aUCAwEA
 | 
					 | 
				
			||||||
AQKCAgBVSXlfXOOiDwtnnPs/IPJe+fuecaBsABfyRcbEMLbm4OhfOzpSurHx0YC4
 | 
					 | 
				
			||||||
7KNX9qdtKFfpfX9NdIq7KEjhbk3kqfPmYkUaZxeTCgZhzAua2S0aYHBXyPCqQ+gU
 | 
					 | 
				
			||||||
fZsqH+Pwe6H0aZQ8KdXHPTCaHdK6veThXo7feQ5t2noT9rq31txpx/Xd6BNGWsmJ
 | 
					 | 
				
			||||||
xS0xCZ68/GgnWdVyumKAGKkmKzRaK3pPA5CzwFqBw7ouvFaWvLuQymU6kWk1B6Xb
 | 
					 | 
				
			||||||
NYIB7IaXWPbRwhnMV0x9C2ABEzFvqOpvT1rqDqloAR4dlvcfbsIZZkQ2mhjt6MeT
 | 
					 | 
				
			||||||
hsorwQ4yCjvtZvVRfW1gTP4iR7ZpmHgHIYQDJy7raZqRVNe9WgMxKTS/IYsHvEvH
 | 
					 | 
				
			||||||
MUBOfQEclAQ8FTUOgTg9+Hf9VXJOtjZRxY08gb+OgKxoAQKxRZmLDUZli9SNWzGe
 | 
					 | 
				
			||||||
R3UECh0gImm/CzWnV3fercLX+qHLTcyJFcb2gclAfG8v8cOT2qu8RtsJAQM2ZSn7
 | 
					 | 
				
			||||||
L8FaWXewjAwkZwCl8Zl5ky1RbBXUkcuLIkAeLAl0+ffM9xiwLhiIj8gRJoq0/jSr
 | 
					 | 
				
			||||||
K1pA8CQ/rZC+9RsI8hP4x2Fn1CU8bOyEBPeNFEIDJHBiCrs/Rl6EAzDMJHlhL/HT
 | 
					 | 
				
			||||||
f7bqssuRjnSbRCKaAdtfGTxQUEjefvqJ11xE3BfHGnKPqoasMQKCAQEA8nY+3MAB
 | 
					 | 
				
			||||||
eBPlE/H4JeVpj7/9/q+JWLFFH9E3Re25LbObzRzIQOd5bTCJkOPQXYRbRIZ1pMt9
 | 
					 | 
				
			||||||
+nZzeLKvWn5nuUFgG2S4n+BEJkBDV78LOkUuGIAPdztF2mwVumygEGJFFfZHbYrh
 | 
					 | 
				
			||||||
XtaGUKdNxn1R3Z4B8W5jWpNNkU+dvoq4Zt0LrL28HB4Sa2yrtHeXbhqSb0BA15+N
 | 
					 | 
				
			||||||
vO9mlfX2I6Yr8F+L/OBfxB0gaNLISJJsj5NSm302je/QrfoMAwbePoNPjEX1/qd2
 | 
					 | 
				
			||||||
rgj9JfM+NE2FsVnFhrV+q4DywRUdX4VBFP4+x7fPm8LxvtVUmLVA3/NtGwPdnC6U
 | 
					 | 
				
			||||||
mQh/+kKVhU2AQwKCAQEA6R2FrxZIUdmk45b/tSjlq7r1ycBYeSztjzXw6hx+w/K3
 | 
					 | 
				
			||||||
Lf+5OGyoGoKr5bZHMhHFxqoWzP6kiZ2Uyvu+pLAGLfenJPec89ZncN4FvW9yU8yL
 | 
					 | 
				
			||||||
nE2Sc8Vf2GnOw2KGJcJR11Ew4dDspHfnM3rof2G4gPC/EMZeGojXoc5GHmT4iasD
 | 
					 | 
				
			||||||
Xwje4qqx5WKvRu1Rw2y+XM5h4gDn3QLLojDOvBpt22yaqSTAupY7OliGFO9h2WPL
 | 
					 | 
				
			||||||
r9TL6va87nXNepCdtzuSlxqTVtgMu2wVu3CnQyw9RiD2M31YnUtBDLNy/UNFj/5Z
 | 
					 | 
				
			||||||
6uXYuakh8vSFFvjgCUyQwR1xCTJur/6LdpAnt9Bz9wKCAQAUqoN9KVh2tatm4c72
 | 
					 | 
				
			||||||
2/D9ca3ikW+xgZqUta5yZWrNPGvhNbzT22b8KZDwKprN/cQRuSw52aZpPMNm3EQa
 | 
					 | 
				
			||||||
AIAyyCG69ADQj7r/T6btybjZRKBDMlcfIIw5q9DGTQ/vlZCx6IX6DkZbYQmdwkTc
 | 
					 | 
				
			||||||
0D20GA2uWGxbggawhgq5/PTuv5SJKrrn4qBLS73u6eqcVeN5XA6q0kywd+9UhNxv
 | 
					 | 
				
			||||||
+W/xUxOJgE5pVto2VREBLonWSwZVfnyx6GjvC0sOzv0Ocv7KxAPNqtRwzQ9Wtr7s
 | 
					 | 
				
			||||||
klb84Nv3OW0MjTcjwfr481Cyy2DqgP5PFnSogWJuibR34jXAgbnX4BiGWrUdzaMU
 | 
					 | 
				
			||||||
86AlAoIBABnw9Bh41VFuc9/zxL7nLy++HW33HqFVc5Y1PXr/8sdhcisHQxhZVxek
 | 
					 | 
				
			||||||
JPbqIuAahDTIZsMnLy41QAKaoyt2fymMXqhJecjUuiwgOOlMxp82qu6Y30xM0Y6m
 | 
					 | 
				
			||||||
r6CkjSMUjcD1QwhOFJd01GCxM8BBIqQOpmR6fqxbQAu8hacKO3IuerCPryXwMt3A
 | 
					 | 
				
			||||||
7ppo/GlP55sySEg7K5I3pmuFHOxn0IPTgR6DfYMGBs9GXJ1lyjDD3z3Q42RhUsMC
 | 
					 | 
				
			||||||
jvwtra9fTL/N8EmAv2H39C8oqSRbfvIX5u3x6/ONFU8RhSFT5CDTADSYoVZ/0MxV
 | 
					 | 
				
			||||||
k53r0hqWz6D94r9QQmsJW4G1JwZYhx8CggEBANUybXsJRgDC5ZOlwbaq0FeTOpZ4
 | 
					 | 
				
			||||||
pSKx1RkVV+G93hK5XdXIVu365pSt3T68MgF+4wWlbC4MuKuzJltFnJBapzz5ygcU
 | 
					 | 
				
			||||||
jw+Q+l/jq+mJj8uvUfo5TAy9thuggj1a7SCs/dm5DBwYA7bfmamLbbBpA0qywMsF
 | 
					 | 
				
			||||||
/vL1bpePIFhbGMhBK7uiEzOAOZVuceZWqcUByjUYy925K/an0ANsQAch5PVeAsEv
 | 
					 | 
				
			||||||
wXC3soaCzfzUyv+eAYBy7LOcz+hpdRj1l4c9Zx6hZeBxMc5LSRoTs+HfE6GgTuI2
 | 
					 | 
				
			||||||
cCfCZNFw7DhDy1TBd3XjQ0AFykeaesImZVR6gp0NYH1kionsMtR5rl4C2tw=
 | 
					 | 
				
			||||||
-----END RSA PRIVATE KEY-----
 | 
					 | 
				
			||||||
@@ -1,21 +0,0 @@
 | 
				
			|||||||
-----BEGIN CERTIFICATE-----
 | 
					 | 
				
			||||||
MIIDYTCCAUmgAwIBAgIUA/EaCcXKgOxBiuaMNTackeF9DgcwDQYJKoZIhvcNAQEL
 | 
					 | 
				
			||||||
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
					 | 
				
			||||||
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
					 | 
				
			||||||
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
					 | 
				
			||||||
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49
 | 
					 | 
				
			||||||
AwEHA0IABOEVdmVd218y3Yy+ocjtt5siaFsNR7tknvS21pzKzxsFc05UrF74YqRx
 | 
					 | 
				
			||||||
Ic6/AQq56C48x6gDhUCdf+nMzD0NWTijGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9z
 | 
					 | 
				
			||||||
dDANBgkqhkiG9w0BAQsFAAOCAgEAW32kadHWEIsN5WXacjtgE9jbFii/rWJjrAO/
 | 
					 | 
				
			||||||
GWuARwLvjWeEFM5xSTny1cTDEz/7Bmfd9GRRpfgxKCFBGaU7zTmOV/AokrjUay+s
 | 
					 | 
				
			||||||
KPj0EV9ZE/84KVfr+MOuhwXhSSv+NvxduFw1sdhTUHs+Wopwjte+bnOUBXmnQH97
 | 
					 | 
				
			||||||
ITSQuUvEamPUS4tJD/kQ0qSGJKNv3CShhbe3XkjUd0UKK7lZzn6hY2fy3CrkkJDT
 | 
					 | 
				
			||||||
GyzFCRf3ZDfjW5/fGXN/TNoEK65pPQVr5taK/bovDesEO7rR4Y4YgtnGlJ7YToWh
 | 
					 | 
				
			||||||
E6I77CbsJCJdmgpjPNwRlwQ8upoWAfxOHqwg32s6uWq20v4TXZTOnEKOPEJG74bh
 | 
					 | 
				
			||||||
JHtwqsy2fIdGz4kZksKGerzJffyQPhX4f3qxxrijT9Hj2EoFIQ4u/jTRpK31IW3R
 | 
					 | 
				
			||||||
gEeNB8O4iyg3crx0oHRwc6zX63O6wBJCZ+0uxLoyjwtjKCxVYbJpccjoQh6zO3UO
 | 
					 | 
				
			||||||
pqAO+NKxQ469QDBV3uRyyQ7DRPu6p67/8gn1E/o8kyU1P7eLFIhBp4T4wCaMQBG6
 | 
					 | 
				
			||||||
IpL5W7eCyuOcqfdm471N07Z4KAqiV8eBJcKaAE+WzNIvrFvCZ/zq7Gj8p07nkjK8
 | 
					 | 
				
			||||||
+ZGDUieiY0mQEpiXLQZt1xNtT/MmlAzFYrK7t6gSygykQKjO1o4GG4g+eXu3h1YK
 | 
					 | 
				
			||||||
avsOwtc=
 | 
					 | 
				
			||||||
-----END CERTIFICATE-----
 | 
					 | 
				
			||||||
@@ -1,5 +0,0 @@
 | 
				
			|||||||
-----BEGIN PRIVATE KEY-----
 | 
					 | 
				
			||||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeo/7wBIYVax9bj4m
 | 
					 | 
				
			||||||
1UEe2H+H4YLsbApDCZMslZbubRuhRANCAAThFXZlXdtfMt2MvqHI7bebImhbDUe7
 | 
					 | 
				
			||||||
ZJ70ttacys8bBXNOVKxe+GKkcSHOvwEKueguPMeoA4VAnX/pzMw9DVk4
 | 
					 | 
				
			||||||
-----END PRIVATE KEY-----
 | 
					 | 
				
			||||||
@@ -1,21 +0,0 @@
 | 
				
			|||||||
-----BEGIN CERTIFICATE-----
 | 
					 | 
				
			||||||
MIIDfjCCAWagAwIBAgIUfmkM99JpQUsQsh8TVILPQDBGOhowDQYJKoZIhvcNAQEM
 | 
					 | 
				
			||||||
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
					 | 
				
			||||||
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
					 | 
				
			||||||
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
					 | 
				
			||||||
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDB2MBAGByqGSM49AgEGBSuBBAAi
 | 
					 | 
				
			||||||
A2IABM5rQQNZEGWeDyrg2dVQS15c+JsX/ginN9/ArHpTgGO6xaLfkbekIj7gewUR
 | 
					 | 
				
			||||||
VQcQrYulwu02HTFDzGVvf1DdCZzIJLJUpl+jagdI05yMPFg2s5lThzGB6HENyw1I
 | 
					 | 
				
			||||||
hU1dMaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBDAUAA4IC
 | 
					 | 
				
			||||||
AQAzpXFEeCMvTBG+kzmHN/+esjCK8MO/Grrw3Qoa1stX0yjNWzfhlGHfWk9Mgwnp
 | 
					 | 
				
			||||||
DnIEFT9lB+nT9uTkKL81Opu2bkWXuL0MyjyYmcCfEgJeDblUbD4HYzPO/s2P7QZu
 | 
					 | 
				
			||||||
Oa1pNKBiSWe1xq+F/gkn0+nDfICq3QQOOQMzaAmlFmszN3v7pryz+x21MqTFsfuW
 | 
					 | 
				
			||||||
ymycRcwZ3VyYeceZuBxjOhR2l8I5yZbR+KPj8VS7A3vgqvkS8ixTK0LrxcLwrTIz
 | 
					 | 
				
			||||||
W9Z1skzVpux4K7iwn3qlWIJ7EbERi9Ydz0MzpjD+CHxGlgHTzT552DGZhPO8D7BE
 | 
					 | 
				
			||||||
+4IoS0YeXEGLeC2a9Hmiy3FXbM4nt3aIWMiWyhjslXIbvWOJL1Vi5GNpdQCG+rB7
 | 
					 | 
				
			||||||
lvA0IISp/tAi9duufdONjgRceRDsqf7o6TOBQdB3QxHRt9gCB2QquRh5llMUY8YH
 | 
					 | 
				
			||||||
PxFJAEzF4X5b0q0k4b50HRVNfDY0SC/XIR7S2xnLDGuoUqrOpUligBzfXV4JHrPv
 | 
					 | 
				
			||||||
YKHsWSOiVxU9eY1SYKuKqnOsGvXSSQlYqSK1ol94eOKDjNy8wekaVYAQNsdNL7o5
 | 
					 | 
				
			||||||
QSWZUcbZLkaNMFdD9twWGduAs0eTichVgyvRgPdevjTGDPBHKNSUURZRTYeW4aIJ
 | 
					 | 
				
			||||||
QzSQUQXwOXsQOmfCkaaTISsDwZAJ0wFqqST1AOhlCWD1OQ==
 | 
					 | 
				
			||||||
-----END CERTIFICATE-----
 | 
					 | 
				
			||||||
@@ -1,6 +0,0 @@
 | 
				
			|||||||
-----BEGIN PRIVATE KEY-----
 | 
					 | 
				
			||||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCRBt7MeJXyJRq7grGQ
 | 
					 | 
				
			||||||
jEdBHE31hG5LuKQ5iL8yTVGk75Kc8ISll2LewbvQ3NhR6E+hZANiAATOa0EDWRBl
 | 
					 | 
				
			||||||
ng8q4NnVUEteXPibF/4IpzffwKx6U4BjusWi35G3pCI+4HsFEVUHEK2LpcLtNh0x
 | 
					 | 
				
			||||||
Q8xlb39Q3QmcyCSyVKZfo2oHSNOcjDxYNrOZU4cxgehxDcsNSIVNXTE=
 | 
					 | 
				
			||||||
-----END PRIVATE KEY-----
 | 
					 | 
				
			||||||
@@ -1,20 +0,0 @@
 | 
				
			|||||||
-----BEGIN CERTIFICATE-----
 | 
					 | 
				
			||||||
MIIDMjCCARqgAwIBAgIUSu1CGfaWlNPjjLG7SaEEgxI+AsAwDQYJKoZIhvcNAQEL
 | 
					 | 
				
			||||||
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
					 | 
				
			||||||
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
					 | 
				
			||||||
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
					 | 
				
			||||||
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUGAytlcAMhAKNv1fkv5rxY
 | 
					 | 
				
			||||||
xGT84C98g2xPpain+jsliHvYrqNkS1zhoxgwFjAUBgNVHREEDTALgglsb2NhbGhv
 | 
					 | 
				
			||||||
c3QwDQYJKoZIhvcNAQELBQADggIBAMgWZhr3whYbFPNYm5RZxjgEonMY7cH/Rza1
 | 
					 | 
				
			||||||
UrBkyoMRL9v00YKJTEvjl5Xl4ku7jqxY2qjValacDroD8hYTLhhkkbHxH6u+kJSC
 | 
					 | 
				
			||||||
cKRQ8QVBZMmoor8qD2Y2BuXZYGWEtgeoXh+DLHWOim7gXDD6GyFPnYFGHnRhNmkE
 | 
					 | 
				
			||||||
6fPcfw1FZmBrMM9rk31EeK9nLVRabL+vuLDEddX1LH4LwK4OuV51wQMZGYiC3M2b
 | 
					 | 
				
			||||||
JpmuPAUAUyiyN53Utuw/ubeih3cEglOwCyDPFt6XISYHO0UaQdw25uhpjxs8KTZB
 | 
					 | 
				
			||||||
qOPJAI4cvGaN3GARXvz2P/QHUiTzsf2XOXXtrO0g+F9P7t6x2xL35c0A8yxKkfsa
 | 
					 | 
				
			||||||
RKdCveRuVlsLXVLVBikqLE/OgPC6SIhIFvTYzXTaZyNfge6dRy2Wg6qWPcGwseeA
 | 
					 | 
				
			||||||
QpFKgWiY3cuy7nJm6uybR6zOEMj5GgNWIbICGguQ5161MLnv0bEmKh2d9/jQ/XN5
 | 
					 | 
				
			||||||
M+nixtGla/G4hL3FQIy9rhHVZzPWwSxl+/eE4qgnInEhmlQ2KrcpgneCt38t0vjJ
 | 
					 | 
				
			||||||
dwHZSzVv8yX7fE0OSq/DWQXJraWUcT7Ds0g7T7jWkWfS0qStVd9VJ+rYQ8dBbE9Y
 | 
					 | 
				
			||||||
gcmkm1DMUd+y9xDRDjxby7gMDWFiN2Au9FQgaIpIctTNCj0+agFBPnfXPVSIH6gX
 | 
					 | 
				
			||||||
10kA2ZVX
 | 
					 | 
				
			||||||
-----END CERTIFICATE-----
 | 
					 | 
				
			||||||
@@ -1,3 +0,0 @@
 | 
				
			|||||||
-----BEGIN PRIVATE KEY-----
 | 
					 | 
				
			||||||
MC4CAQAwBQYDK2VwBCIEIGtLkaYE4D+P5HLkoc3a/tNhPP+gnhPLF3jEtdYcAtpd
 | 
					 | 
				
			||||||
-----END PRIVATE KEY-----
 | 
					 | 
				
			||||||
@@ -1,114 +0,0 @@
 | 
				
			|||||||
#! /bin/bash
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Usage:
 | 
					 | 
				
			||||||
#   ./gen_certs.sh [cert-kind]
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# [cert-kind]:
 | 
					 | 
				
			||||||
#   ed25519
 | 
					 | 
				
			||||||
#   rsa_sha256
 | 
					 | 
				
			||||||
#   ecdsa_nistp256_sha256
 | 
					 | 
				
			||||||
#   ecdsa_nistp384_sha384
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# Generate a certificate of the [cert-kind] key type, or if no cert-kind is
 | 
					 | 
				
			||||||
# specified, all of the certificates.
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# Examples:
 | 
					 | 
				
			||||||
#   ./gen_certs.sh ed25519
 | 
					 | 
				
			||||||
#   ./gen_certs.sh rsa_sha256
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# TODO: `rustls` (really, `webpki`) doesn't currently use the CN in the subject
 | 
					 | 
				
			||||||
# to check if a certificate is valid for a server name sent via SNI. It's not
 | 
					 | 
				
			||||||
# clear if this is intended, since certificates _should_ have a `subjectAltName`
 | 
					 | 
				
			||||||
# with a DNS name, or if it simply hasn't been implemented yet. See
 | 
					 | 
				
			||||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=552346 for a bit more info.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CA_SUBJECT="/C=US/ST=CA/O=Rocket CA/CN=Rocket Root CA"
 | 
					 | 
				
			||||||
SUBJECT="/C=US/ST=CA/O=Rocket/CN=localhost"
 | 
					 | 
				
			||||||
ALT="DNS:localhost"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function gen_ca() {
 | 
					 | 
				
			||||||
  openssl genrsa -out ca_key.pem 4096
 | 
					 | 
				
			||||||
  openssl req -new -x509 -days 3650 -key ca_key.pem \
 | 
					 | 
				
			||||||
    -subj "${CA_SUBJECT}" -out ca_cert.pem
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function gen_ca_if_non_existent() {
 | 
					 | 
				
			||||||
  if ! [ -f ./ca_cert.pem ]; then gen_ca; fi
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function gen_rsa_sha256() {
 | 
					 | 
				
			||||||
  gen_ca_if_non_existent
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openssl req -newkey rsa:4096 -nodes -sha256 -keyout rsa_sha256_key.pem \
 | 
					 | 
				
			||||||
    -subj "${SUBJECT}" -out server.csr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
					 | 
				
			||||||
    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
					 | 
				
			||||||
    -in server.csr -out rsa_sha256_cert.pem
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  rm ca_cert.srl server.csr
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function gen_ed25519() {
 | 
					 | 
				
			||||||
  gen_ca_if_non_existent
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openssl genpkey -algorithm ED25519 > ed25519_key.pem
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openssl req -new -key ed25519_key.pem -subj "${SUBJECT}" -out server.csr
 | 
					 | 
				
			||||||
  openssl x509 -req -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
					 | 
				
			||||||
    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
					 | 
				
			||||||
    -in server.csr -out ed25519_cert.pem
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  rm ca_cert.srl server.csr
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function gen_ecdsa_nistp256_sha256() {
 | 
					 | 
				
			||||||
  gen_ca_if_non_existent
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openssl ecparam -out ecdsa_nistp256_sha256_key.pem -name prime256v1 -genkey
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # Convert to pkcs8 format supported by rustls
 | 
					 | 
				
			||||||
  openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp256_sha256_key.pem \
 | 
					 | 
				
			||||||
    -out ecdsa_nistp256_sha256_key_pkcs8.pem
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openssl req -new -nodes -sha256 -key ecdsa_nistp256_sha256_key_pkcs8.pem \
 | 
					 | 
				
			||||||
    -subj "${SUBJECT}" -out server.csr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
					 | 
				
			||||||
    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
					 | 
				
			||||||
    -in server.csr -out ecdsa_nistp256_sha256_cert.pem
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  rm ca_cert.srl server.csr ecdsa_nistp256_sha256_key.pem
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function gen_ecdsa_nistp384_sha384() {
 | 
					 | 
				
			||||||
  gen_ca_if_non_existent
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openssl ecparam -out ecdsa_nistp384_sha384_key.pem -name secp384r1 -genkey
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # Convert to pkcs8 format supported by rustls
 | 
					 | 
				
			||||||
  openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp384_sha384_key.pem \
 | 
					 | 
				
			||||||
    -out ecdsa_nistp384_sha384_key_pkcs8.pem
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openssl req -new -nodes -sha384 -key ecdsa_nistp384_sha384_key_pkcs8.pem \
 | 
					 | 
				
			||||||
    -subj "${SUBJECT}" -out server.csr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openssl x509 -req -sha384 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
					 | 
				
			||||||
    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
					 | 
				
			||||||
    -in server.csr -out ecdsa_nistp384_sha384_cert.pem
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  rm ca_cert.srl server.csr ecdsa_nistp384_sha384_key.pem
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
case $1 in
 | 
					 | 
				
			||||||
  ed25519) gen_ed25519 ;;
 | 
					 | 
				
			||||||
  rsa_sha256) gen_rsa_sha256 ;;
 | 
					 | 
				
			||||||
  ecdsa_nistp256_sha256) gen_ecdsa_nistp256_sha256 ;;
 | 
					 | 
				
			||||||
  ecdsa_nistp384_sha384) gen_ecdsa_nistp384_sha384 ;;
 | 
					 | 
				
			||||||
  *)
 | 
					 | 
				
			||||||
    gen_ed25519
 | 
					 | 
				
			||||||
    gen_rsa_sha256
 | 
					 | 
				
			||||||
    gen_ecdsa_nistp256_sha256
 | 
					 | 
				
			||||||
    gen_ecdsa_nistp384_sha384
 | 
					 | 
				
			||||||
    ;;
 | 
					 | 
				
			||||||
esac
 | 
					 | 
				
			||||||
@@ -1,30 +0,0 @@
 | 
				
			|||||||
-----BEGIN CERTIFICATE-----
 | 
					 | 
				
			||||||
MIIFLDCCAxSgAwIBAgIUZ461KXDgTT25LP1S6PK6i9ZKtOYwDQYJKoZIhvcNAQEL
 | 
					 | 
				
			||||||
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
					 | 
				
			||||||
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
					 | 
				
			||||||
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
					 | 
				
			||||||
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQAD
 | 
					 | 
				
			||||||
ggIPADCCAgoCggIBAKl8Re1dneFB2obYPOuZf3d6BR2xGcMhr2vmHpnVgkeg/xGI
 | 
					 | 
				
			||||||
cwHhhB04mEg4TqbZ0Q1wdKN4ruNigdrQAXDqnm4y2IB1q/uTdoXVWNsvAZmDq4N4
 | 
					 | 
				
			||||||
rPUDWagpkilV4HOHDQOI27Lo/+30wJX6H8i3J6Nag1y9spuySkL1F1SgQng9uzvP
 | 
					 | 
				
			||||||
3SwbsO9R/jS7qTZNEtirnJv3qJlxmT4BCvBuWNALShFlSZYnZk/2csYXEaVkWMUE
 | 
					 | 
				
			||||||
rnWGmTmzMZEzDOdQdPC3sJDCPQbbgD4SnQANqhOVjPSdJ6Y6joN6XYtB2EP+6LJ8
 | 
					 | 
				
			||||||
UBTICETnUROWeKqMm215Gsen32cyWkaCwEWtTFn7rJHbPvUY4vPYQZAB2/h07lYq
 | 
					 | 
				
			||||||
v67W3r5lTq1KuoStD8M/7nCIYIuNhmJLMzzWrSfJrQpYexoMII6kjOxv2kiUgb1y
 | 
					 | 
				
			||||||
bYKnFlVf4up3pTpi2/0We4SHRgc7xTcEPNvU5z5hc5JwHZIFjmi5dx50ZoAULrXl
 | 
					 | 
				
			||||||
OUhfdUPut7H38UQg3riJiNAzedUiTubek26po8qH1iS/EMgAi5RboaBovjWhgjwq
 | 
					 | 
				
			||||||
P/RP0vzpln6OdQIRaRlY9vZiik9HbFybafuA2zEkyc+zc4HqJk3vqX/0hgd9qWeL
 | 
					 | 
				
			||||||
zgsHr6A86tNUXsUaoInQbzznjpbFE85klxd6HeqasOyVDCeDUs4lWoDNp0DbAgMB
 | 
					 | 
				
			||||||
AAGjGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAgEA
 | 
					 | 
				
			||||||
sS2TUKKYtNYC68TEQb5KAa8/zi6Lhz9lyn9rpBXAcAVeBdxCqrBU0nRN8EjXEff1
 | 
					 | 
				
			||||||
oBeau9Wi9PwdK6J+4xFuer9YrZZWuWLKUBXJL9ZXEfI+USo5WVz7v/puoKZSUSu2
 | 
					 | 
				
			||||||
+Ee4+v1eoAsCtaA8IR0Sh1KTc9kg1QZW1nIdBV0zgAERWTzm1iy2Ff+euowM/lUR
 | 
					 | 
				
			||||||
FczMAJsNq83O2kaH541fRx3gC2iMN/QLwRalBR2y8hTI5wQa0Yr8moC4IsTfRrKQ
 | 
					 | 
				
			||||||
/SZDjFT1XoA9TnrByIvGQNlxK1Rm2hvK1OQn4OL6sdYK6F+MxfUn/atQNyBQ6CF+
 | 
					 | 
				
			||||||
oKuvOnE2jces8GGZIU1jYJ2271SQkzp0CW3N1ZKjClSQFAYED+5ERTGyeEL4o3Vr
 | 
					 | 
				
			||||||
V/BUd0KC7HhbWBlFc2EDmPOoOTp/ID901Kf499M68pe2Fr/63/nCAON6WFk1NPYA
 | 
					 | 
				
			||||||
+sWMfS25hikU5rOHGQgXxXAz90pwpvpoLQvaq0/azsbHvq9B4ubdcLE0xcoK+DGq
 | 
					 | 
				
			||||||
+/aOeWlYkalWp93akPYpWN3UJXzDXzzagRID/1LEGS+Ssbh1WVhAhLKVoxzBvkgm
 | 
					 | 
				
			||||||
ozVAhR3zHXI2QpQ2FEbOmkcRtpxv2CiaTsgHg2MK8cdmPx3G+ufCZjRbQx/VXVdN
 | 
					 | 
				
			||||||
vaFRdmzr/5EQ7DT5Uy8w32h1to4Fm2sAtbAFYaFLXaM=
 | 
					 | 
				
			||||||
-----END CERTIFICATE-----
 | 
					 | 
				
			||||||
@@ -1,52 +0,0 @@
 | 
				
			|||||||
-----BEGIN PRIVATE KEY-----
 | 
					 | 
				
			||||||
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCpfEXtXZ3hQdqG
 | 
					 | 
				
			||||||
2DzrmX93egUdsRnDIa9r5h6Z1YJHoP8RiHMB4YQdOJhIOE6m2dENcHSjeK7jYoHa
 | 
					 | 
				
			||||||
0AFw6p5uMtiAdav7k3aF1VjbLwGZg6uDeKz1A1moKZIpVeBzhw0DiNuy6P/t9MCV
 | 
					 | 
				
			||||||
+h/ItyejWoNcvbKbskpC9RdUoEJ4Pbs7z90sG7DvUf40u6k2TRLYq5yb96iZcZk+
 | 
					 | 
				
			||||||
AQrwbljQC0oRZUmWJ2ZP9nLGFxGlZFjFBK51hpk5szGRMwznUHTwt7CQwj0G24A+
 | 
					 | 
				
			||||||
Ep0ADaoTlYz0nSemOo6Del2LQdhD/uiyfFAUyAhE51ETlniqjJtteRrHp99nMlpG
 | 
					 | 
				
			||||||
gsBFrUxZ+6yR2z71GOLz2EGQAdv4dO5WKr+u1t6+ZU6tSrqErQ/DP+5wiGCLjYZi
 | 
					 | 
				
			||||||
SzM81q0nya0KWHsaDCCOpIzsb9pIlIG9cm2CpxZVX+Lqd6U6Ytv9FnuEh0YHO8U3
 | 
					 | 
				
			||||||
BDzb1Oc+YXOScB2SBY5ouXcedGaAFC615TlIX3VD7rex9/FEIN64iYjQM3nVIk7m
 | 
					 | 
				
			||||||
3pNuqaPKh9YkvxDIAIuUW6GgaL41oYI8Kj/0T9L86ZZ+jnUCEWkZWPb2YopPR2xc
 | 
					 | 
				
			||||||
m2n7gNsxJMnPs3OB6iZN76l/9IYHfalni84LB6+gPOrTVF7FGqCJ0G88546WxRPO
 | 
					 | 
				
			||||||
ZJcXeh3qmrDslQwng1LOJVqAzadA2wIDAQABAoICABT4gHp/Q+K0UEKxDNCl/ISe
 | 
					 | 
				
			||||||
/3UODb78MwVpws2MAoO0YvsbZAeOjNdEwmrlNK4mc1xzVqtHanROIv0dEaCUFyhR
 | 
					 | 
				
			||||||
eEJkzPPi6h5jKIxuQ4doKFerHdNvJ6/L/P7KVmxVAII4c96uP8SErTOhcD9Ykjn/
 | 
					 | 
				
			||||||
IBPgkPH83H1ucAWTksXn9XvQG3CyuHDUN1z0/1ntrXBLw6P0v9LEoI5weJcJQEn1
 | 
					 | 
				
			||||||
q6N9Yd6HX3xzZP4nqpJJWUZ/bsqx7dGa3340z9rrNJz4TYuLzRtFG5gSm4R/LFUi
 | 
					 | 
				
			||||||
Av/dViOWST3xbROnAQhgyRAUm6AGpCdKa9i9nI6VuUGRY4PivJy7OTpSQVIdwD2K
 | 
					 | 
				
			||||||
VFbFauCmsTY7B+Z+DXe9JJV+Tlazvlwop1DApxCgLMNr2pVB/1yPzMrNYDB4Sg1c
 | 
					 | 
				
			||||||
T3stu4A8PtljTykla2C2LPFrdIijvYv9n6PSPWV4vf1ix9uYs99FhZPQJMgm+CDr
 | 
					 | 
				
			||||||
n3cFfuofIWIRJpCuu3hn4woGgW2bXor4pyMNg1yCp1T2c8g6eJUYAAIRpse0HDbT
 | 
					 | 
				
			||||||
ZUifxlNBx819bf9aqv+lhwMU4UFlmWMv3NETcCBqgUn+z4scNJ3kZwO9ZodLRblK
 | 
					 | 
				
			||||||
SpalZ6SNejTnVukg3zbwJG8w5vIrJvfjBkikAL1O5X6Kn3YqfrXAl38bIvA50ZBe
 | 
					 | 
				
			||||||
eOFcgDPClLPGlNKxQXiZAoIBAQDh0aArTjWi8Kxt2Ga3Hu4jQ7IXklL9osGAlFCB
 | 
					 | 
				
			||||||
wZs831/ktLY0c7vY+mbwrY4AtqK21A00MrNSTsp/McHwAUi410DdcTqzkGnm9BBZ
 | 
					 | 
				
			||||||
FKVO1H9KGg2t344tOXcPsFTl6SR5a0aPqagyvgdH+YWC4ccfYZWMcLELn3sOGoEp
 | 
					 | 
				
			||||||
a7VBxjLZZ+/b+/oIzhwxEaBi8N0U3IZ3SsC9Lmojnb9+LMwGs14TFfahD3uM1hnU
 | 
					 | 
				
			||||||
vww1elwPwXafWc+7Ej1bXijWBXbyzkCITzHafmDsAatEZnemHL8zKKtTn2aSTUSj
 | 
					 | 
				
			||||||
Fl/y94WR7/V4nVEQ0R3fjGC0rKh08BtKzxPjYBqjT5aI7+yfAoIBAQDAIzROr00o
 | 
					 | 
				
			||||||
65auD5TB2k/pSVKQpB/U6ESGafDxg4a+R9Ng2ESmDN5hUUZDefm0Uxf9AZqS6eno
 | 
					 | 
				
			||||||
GSAqxNDixWPy2WFzbZ0mUCoyQn+Eh09WBvt0IqDZ2+ngBEXiKA/8dsvXinBHLuHV
 | 
					 | 
				
			||||||
u+rJdLMzPfhWVo1/iXq7SOjBvDuthKROku5BEt6Q3Cs0nk6uneRIqKtaqa7KIInF
 | 
					 | 
				
			||||||
BPtUifZtCP09h6iFFGNv2g2OYRYDg8TXvjt3scKy0DUC3kTeTCPvNvHaOObGbMRU
 | 
					 | 
				
			||||||
Q85HkXburxWfsMto5m/lRsbY3COxddB47WIQ6QNewESfCab/R2lOdlXQeaH8fuxT
 | 
					 | 
				
			||||||
wWC5DMz4F0ZFAoIBAFm+EjZDnaNEnHIHB0MNIryXAabGev7beKUdzCTVCVmWuChO
 | 
					 | 
				
			||||||
/P45ZFTlppVNk9qKun2IJjsxTvyN3YHRB27XQ8xZlyiqABcudDfZlMmiH9QFNRUA
 | 
					 | 
				
			||||||
56DK8Fjetodgn0zDa8BpNqCPXw3TYVdkPX/3NEgvYtxuSJ4C4keHlv8cE+uw1bJ6
 | 
					 | 
				
			||||||
0OMO754iMyf5BlFrwaCxxyqPZauJT5sZ7Ok66lZbYC6bkukNGx+sUpWu2y5Bk2ab
 | 
					 | 
				
			||||||
jwXjDmAc7o9qCzaK82upNhI1zu0zPldsjmDfi/tS/1VYe0X/WicYWAesM7N+VPHb
 | 
					 | 
				
			||||||
eCVX98iEIqgdxKzo1QWsClyfkRrSraNrVLrVBqcCggEAaLIGK6YMNnMBPUGSPnt2
 | 
					 | 
				
			||||||
NdlVWymDiuExjcimmQOhZYf/33KZHZ4/gunljpklfqQUmzHHh6xcX7NpOsTaSedj
 | 
					 | 
				
			||||||
Sg43ss0U566g/5gKoi2VBnxxglvoKC5T51SMu+o2o8wb0QxHmBIszulBy5qClzZ6
 | 
					 | 
				
			||||||
Xpl1Kvy/2tOkuQSXxDpVydb4ao8cpfTCuj5VA4NXxFvcW1/AtbU7PRc02GEA3XMb
 | 
					 | 
				
			||||||
gu6r3jA46tb3shCnDS09Eo4/Gz7Kp+MaL8Dr5/G3Vv8qlE2TOqZD6OK1wXu7Qd43
 | 
					 | 
				
			||||||
uzd772I5sMZ7TenOrUFUYsB/QlWmF3hPLBX3YH0KHc4PfrT4lnyWzCDAUrVt7vXH
 | 
					 | 
				
			||||||
vQKCAQEAz1Q+jW+NG7CAqkOJ19n+KmGn+zddvy8x4txKx8kV0+3XIVNkseS6IT65
 | 
					 | 
				
			||||||
uemD85Gn7MYwHxcKUHghHSKyJsvCb1vSmB3JtCnrsodmQNMb6Lsq89VlM8Ylc/s3
 | 
					 | 
				
			||||||
F4AraiOlylqcEwLkanD3XSfPdQZhExZHEtpAW/Rr6zYT9J94VO1nK3+RkFI4a8kl
 | 
					 | 
				
			||||||
pEbY+tqJj3LGTuaQkshB9rDtcBT5/JsaxlMk783qkKziCPZF47BWqrJb+t7tm3qg
 | 
					 | 
				
			||||||
5gf+QUInEjdW3k3uZBL4316RP/TJlLvo29PkEwoC8X4R2EgDeHQhhRC2Fi2Wpy4O
 | 
					 | 
				
			||||||
ce4G+zZOOYXwvWGJLwNhgsve8C3oqg==
 | 
					 | 
				
			||||||
-----END PRIVATE KEY-----
 | 
					 | 
				
			||||||
@@ -1,52 +0,0 @@
 | 
				
			|||||||
pub const DISCORD_OAUTH_TOKEN: &'static str = "https://discord.com/api/oauth2/token";
 | 
					 | 
				
			||||||
pub const DISCORD_OAUTH_AUTHORIZE: &'static str = "https://discord.com/api/oauth2/authorize";
 | 
					 | 
				
			||||||
pub const DISCORD_API: &'static str = "https://discord.com/api";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub const MAX_CONTENT_LENGTH: usize = 2000;
 | 
					 | 
				
			||||||
pub const MAX_EMBED_DESCRIPTION_LENGTH: usize = 4096;
 | 
					 | 
				
			||||||
pub const MAX_EMBED_TITLE_LENGTH: usize = 256;
 | 
					 | 
				
			||||||
pub const MAX_EMBED_AUTHOR_LENGTH: usize = 256;
 | 
					 | 
				
			||||||
pub const MAX_EMBED_FOOTER_LENGTH: usize = 2048;
 | 
					 | 
				
			||||||
pub const MAX_URL_LENGTH: usize = 512;
 | 
					 | 
				
			||||||
pub const MAX_USERNAME_LENGTH: usize = 100;
 | 
					 | 
				
			||||||
pub const MAX_EMBED_FIELDS: usize = 25;
 | 
					 | 
				
			||||||
pub const MAX_EMBED_FIELD_TITLE_LENGTH: usize = 256;
 | 
					 | 
				
			||||||
pub const MAX_EMBED_FIELD_VALUE_LENGTH: usize = 1024;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub const MINUTE: usize = 60;
 | 
					 | 
				
			||||||
pub const HOUR: usize = 60 * MINUTE;
 | 
					 | 
				
			||||||
pub const DAY: usize = 24 * HOUR;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::{collections::HashSet, env, iter::FromIterator};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use lazy_static::lazy_static;
 | 
					 | 
				
			||||||
use serenity::model::prelude::AttachmentType;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
lazy_static! {
 | 
					 | 
				
			||||||
    pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
 | 
					 | 
				
			||||||
        include_bytes!(concat!(
 | 
					 | 
				
			||||||
            env!("CARGO_MANIFEST_DIR"),
 | 
					 | 
				
			||||||
            "/../assets/",
 | 
					 | 
				
			||||||
            env!("WEBHOOK_AVATAR", "WEBHOOK_AVATAR not provided for compilation")
 | 
					 | 
				
			||||||
        )) as &[u8],
 | 
					 | 
				
			||||||
        env!("WEBHOOK_AVATAR"),
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
        .into();
 | 
					 | 
				
			||||||
    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
					 | 
				
			||||||
        env::var("SUBSCRIPTION_ROLES")
 | 
					 | 
				
			||||||
            .map(|var| var
 | 
					 | 
				
			||||||
                .split(',')
 | 
					 | 
				
			||||||
                .filter_map(|item| { item.parse::<u64>().ok() })
 | 
					 | 
				
			||||||
                .collect::<Vec<u64>>())
 | 
					 | 
				
			||||||
            .unwrap_or_else(|_| Vec::new())
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    pub static ref CNC_GUILD: Option<u64> =
 | 
					 | 
				
			||||||
        env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten();
 | 
					 | 
				
			||||||
    pub static ref MIN_INTERVAL: u32 = env::var("MIN_INTERVAL")
 | 
					 | 
				
			||||||
        .ok()
 | 
					 | 
				
			||||||
        .map(|inner| inner.parse::<u32>().ok())
 | 
					 | 
				
			||||||
        .flatten()
 | 
					 | 
				
			||||||
        .unwrap_or(600);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										199
									
								
								web/src/lib.rs
									
									
									
									
									
								
							
							
						
						@@ -1,199 +0,0 @@
 | 
				
			|||||||
#[macro_use]
 | 
					 | 
				
			||||||
extern crate rocket;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
mod consts;
 | 
					 | 
				
			||||||
#[macro_use]
 | 
					 | 
				
			||||||
mod macros;
 | 
					 | 
				
			||||||
mod routes;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::{collections::HashMap, env};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
 | 
					 | 
				
			||||||
use rocket::{
 | 
					 | 
				
			||||||
    fs::FileServer,
 | 
					 | 
				
			||||||
    serde::json::{json, Value as JsonValue},
 | 
					 | 
				
			||||||
    tokio::sync::broadcast::Sender,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use rocket_dyn_templates::Template;
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    http::CacheHttp,
 | 
					 | 
				
			||||||
    model::id::{GuildId, UserId},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Database = MySql;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
enum Error {
 | 
					 | 
				
			||||||
    SQLx(sqlx::Error),
 | 
					 | 
				
			||||||
    Serenity(serenity::Error),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[catch(401)]
 | 
					 | 
				
			||||||
async fn not_authorized() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<String, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("errors/401", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[catch(403)]
 | 
					 | 
				
			||||||
async fn forbidden() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<String, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("errors/403", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[catch(404)]
 | 
					 | 
				
			||||||
async fn not_found() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<String, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("errors/404", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[catch(413)]
 | 
					 | 
				
			||||||
async fn payload_too_large() -> JsonValue {
 | 
					 | 
				
			||||||
    json!({"error": "Data too large.", "errors": ["Data too large."]})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[catch(422)]
 | 
					 | 
				
			||||||
async fn unprocessable_entity() -> JsonValue {
 | 
					 | 
				
			||||||
    json!({"error": "Invalid request.", "errors": ["Invalid request."]})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[catch(500)]
 | 
					 | 
				
			||||||
async fn internal_server_error() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<String, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("errors/500", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn initialize(
 | 
					 | 
				
			||||||
    kill_channel: Sender<()>,
 | 
					 | 
				
			||||||
    serenity_context: Context,
 | 
					 | 
				
			||||||
    db_pool: Pool<Database>,
 | 
					 | 
				
			||||||
) -> Result<(), Box<dyn std::error::Error>> {
 | 
					 | 
				
			||||||
    info!("Checking environment variables...");
 | 
					 | 
				
			||||||
    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_DISCORD_CALLBACK").expect("`OAUTH2_DISCORD_CALLBACK' not supplied");
 | 
					 | 
				
			||||||
    env::var("PATREON_GUILD_ID").expect("`PATREON_GUILD' not supplied");
 | 
					 | 
				
			||||||
    info!("Done!");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let oauth2_client = BasicClient::new(
 | 
					 | 
				
			||||||
        ClientId::new(env::var("OAUTH2_CLIENT_ID")?),
 | 
					 | 
				
			||||||
        Some(ClientSecret::new(env::var("OAUTH2_CLIENT_SECRET")?)),
 | 
					 | 
				
			||||||
        AuthUrl::new(DISCORD_OAUTH_AUTHORIZE.to_string())?,
 | 
					 | 
				
			||||||
        Some(TokenUrl::new(DISCORD_OAUTH_TOKEN.to_string())?),
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .set_redirect_uri(RedirectUrl::new(env::var("OAUTH2_DISCORD_CALLBACK")?)?);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let reqwest_client = reqwest::Client::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    rocket::build()
 | 
					 | 
				
			||||||
        .attach(Template::fairing())
 | 
					 | 
				
			||||||
        .register(
 | 
					 | 
				
			||||||
            "/",
 | 
					 | 
				
			||||||
            catchers![
 | 
					 | 
				
			||||||
                not_authorized,
 | 
					 | 
				
			||||||
                forbidden,
 | 
					 | 
				
			||||||
                not_found,
 | 
					 | 
				
			||||||
                internal_server_error,
 | 
					 | 
				
			||||||
                unprocessable_entity,
 | 
					 | 
				
			||||||
                payload_too_large,
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .manage(oauth2_client)
 | 
					 | 
				
			||||||
        .manage(reqwest_client)
 | 
					 | 
				
			||||||
        .manage(serenity_context)
 | 
					 | 
				
			||||||
        .manage(db_pool)
 | 
					 | 
				
			||||||
        .mount("/static", FileServer::from(concat!(env!("CARGO_MANIFEST_DIR"), "/static")))
 | 
					 | 
				
			||||||
        .mount(
 | 
					 | 
				
			||||||
            "/",
 | 
					 | 
				
			||||||
            routes![
 | 
					 | 
				
			||||||
                routes::index,
 | 
					 | 
				
			||||||
                routes::cookies,
 | 
					 | 
				
			||||||
                routes::privacy,
 | 
					 | 
				
			||||||
                routes::terms,
 | 
					 | 
				
			||||||
                routes::return_to_same_site
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .mount(
 | 
					 | 
				
			||||||
            "/help",
 | 
					 | 
				
			||||||
            routes![
 | 
					 | 
				
			||||||
                routes::help,
 | 
					 | 
				
			||||||
                routes::help_timezone,
 | 
					 | 
				
			||||||
                routes::help_create_reminder,
 | 
					 | 
				
			||||||
                routes::help_delete_reminder,
 | 
					 | 
				
			||||||
                routes::help_timers,
 | 
					 | 
				
			||||||
                routes::help_todo_lists,
 | 
					 | 
				
			||||||
                routes::help_macros,
 | 
					 | 
				
			||||||
                routes::help_intervals,
 | 
					 | 
				
			||||||
                routes::help_dashboard,
 | 
					 | 
				
			||||||
                routes::help_iemanager,
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .mount("/login", routes![routes::login::discord_login, routes::login::discord_callback])
 | 
					 | 
				
			||||||
        .mount(
 | 
					 | 
				
			||||||
            "/dashboard",
 | 
					 | 
				
			||||||
            routes![
 | 
					 | 
				
			||||||
                routes::dashboard::dashboard,
 | 
					 | 
				
			||||||
                routes::dashboard::dashboard_home,
 | 
					 | 
				
			||||||
                routes::dashboard::user::get_user_info,
 | 
					 | 
				
			||||||
                routes::dashboard::user::update_user_info,
 | 
					 | 
				
			||||||
                routes::dashboard::user::get_user_guilds,
 | 
					 | 
				
			||||||
                routes::dashboard::guild::get_guild_patreon,
 | 
					 | 
				
			||||||
                routes::dashboard::guild::get_guild_channels,
 | 
					 | 
				
			||||||
                routes::dashboard::guild::get_guild_roles,
 | 
					 | 
				
			||||||
                routes::dashboard::guild::get_reminder_templates,
 | 
					 | 
				
			||||||
                routes::dashboard::guild::create_reminder_template,
 | 
					 | 
				
			||||||
                routes::dashboard::guild::delete_reminder_template,
 | 
					 | 
				
			||||||
                routes::dashboard::guild::create_reminder,
 | 
					 | 
				
			||||||
                routes::dashboard::guild::get_reminders,
 | 
					 | 
				
			||||||
                routes::dashboard::guild::edit_reminder,
 | 
					 | 
				
			||||||
                routes::dashboard::guild::delete_reminder,
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .launch()
 | 
					 | 
				
			||||||
        .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    warn!("Exiting rocket runtime");
 | 
					 | 
				
			||||||
    // distribute kill signal
 | 
					 | 
				
			||||||
    match kill_channel.send(()) {
 | 
					 | 
				
			||||||
        Ok(_) => {}
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            error!("Failed to issue kill signal: {:?}", e);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
 | 
					 | 
				
			||||||
    if let Some(subscription_guild) = *CNC_GUILD {
 | 
					 | 
				
			||||||
        let guild_member = GuildId(subscription_guild).member(cache_http, user_id).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Ok(member) = guild_member {
 | 
					 | 
				
			||||||
            for role in member.roles {
 | 
					 | 
				
			||||||
                if SUBSCRIPTION_ROLES.contains(role.as_u64()) {
 | 
					 | 
				
			||||||
                    return true;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        false
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        true
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn check_guild_subscription(
 | 
					 | 
				
			||||||
    cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
    guild_id: impl Into<GuildId>,
 | 
					 | 
				
			||||||
) -> bool {
 | 
					 | 
				
			||||||
    if let Some(guild) = cache_http.cache().unwrap().guild(guild_id) {
 | 
					 | 
				
			||||||
        let owner = guild.owner_id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        check_subscription(&cache_http, owner).await
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        false
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,119 +0,0 @@
 | 
				
			|||||||
macro_rules! check_length {
 | 
					 | 
				
			||||||
    ($max:ident, $field:expr) => {
 | 
					 | 
				
			||||||
        if $field.len() > $max {
 | 
					 | 
				
			||||||
            return json!({ "error": format!("{} exceeded", stringify!($max)) });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    ($max:ident, $field:expr, $($fields:expr),+) => {
 | 
					 | 
				
			||||||
        check_length!($max, $field);
 | 
					 | 
				
			||||||
        check_length!($max, $($fields),+);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
macro_rules! check_length_opt {
 | 
					 | 
				
			||||||
    ($max:ident, $field:expr) => {
 | 
					 | 
				
			||||||
        if let Some(field) = &$field {
 | 
					 | 
				
			||||||
            check_length!($max, field);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    ($max:ident, $field:expr, $($fields:expr),+) => {
 | 
					 | 
				
			||||||
        check_length_opt!($max, $field);
 | 
					 | 
				
			||||||
        check_length_opt!($max, $($fields),+);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
macro_rules! check_url {
 | 
					 | 
				
			||||||
    ($field:expr) => {
 | 
					 | 
				
			||||||
        if !($field.starts_with("http://") || $field.starts_with("https://")) {
 | 
					 | 
				
			||||||
            return json!({ "error": "URL invalid" });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    ($field:expr, $($fields:expr),+) => {
 | 
					 | 
				
			||||||
        check_url!($max, $field);
 | 
					 | 
				
			||||||
        check_url!($max, $($fields),+);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
macro_rules! check_url_opt {
 | 
					 | 
				
			||||||
    ($field:expr) => {
 | 
					 | 
				
			||||||
        if let Some(field) = &$field {
 | 
					 | 
				
			||||||
            check_url!(field);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    ($field:expr, $($fields:expr),+) => {
 | 
					 | 
				
			||||||
        check_url_opt!($field);
 | 
					 | 
				
			||||||
        check_url_opt!($($fields),+);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
macro_rules! check_authorization {
 | 
					 | 
				
			||||||
    ($cookies:expr, $ctx:expr, $guild:expr) => {
 | 
					 | 
				
			||||||
        use serenity::model::id::UserId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match user_id {
 | 
					 | 
				
			||||||
            Some(user_id) => {
 | 
					 | 
				
			||||||
                match GuildId($guild).to_guild_cached($ctx) {
 | 
					 | 
				
			||||||
                    Some(guild) => {
 | 
					 | 
				
			||||||
                        let member = guild.member($ctx, UserId(user_id)).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        match member {
 | 
					 | 
				
			||||||
                            Err(_) => {
 | 
					 | 
				
			||||||
                                return json!({"error": "User not in guild"})
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Ok(_) => {}
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    None => {
 | 
					 | 
				
			||||||
                        return json!({"error": "Bot not in guild"})
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            None => {
 | 
					 | 
				
			||||||
                return json!({"error": "User not authorized"});
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
macro_rules! update_field {
 | 
					 | 
				
			||||||
    ($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => {
 | 
					 | 
				
			||||||
        if let Some(value) = &$reminder.$field {
 | 
					 | 
				
			||||||
            match sqlx::query(concat!(
 | 
					 | 
				
			||||||
                "UPDATE reminders SET `",
 | 
					 | 
				
			||||||
                stringify!($field),
 | 
					 | 
				
			||||||
                "` = ? WHERE uid = ?"
 | 
					 | 
				
			||||||
            ))
 | 
					 | 
				
			||||||
            .bind(value)
 | 
					 | 
				
			||||||
            .bind(&$reminder.uid)
 | 
					 | 
				
			||||||
            .execute($pool)
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Ok(_) => {}
 | 
					 | 
				
			||||||
                Err(e) => {
 | 
					 | 
				
			||||||
                    warn!(
 | 
					 | 
				
			||||||
                        concat!(
 | 
					 | 
				
			||||||
                            "Error in `update_field!(",
 | 
					 | 
				
			||||||
                            stringify!($pool),
 | 
					 | 
				
			||||||
                            stringify!($reminder),
 | 
					 | 
				
			||||||
                            stringify!($field),
 | 
					 | 
				
			||||||
                            ")': {:?}"
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                        e
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    $error.push(format!("Error setting field {}", stringify!($field)));
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ($pool:expr, $error:ident, $reminder:ident.[$field:ident, $($fields:ident),+]) => {
 | 
					 | 
				
			||||||
        update_field!($pool, $error, $reminder.[$field]);
 | 
					 | 
				
			||||||
        update_field!($pool, $error, $reminder.[$($fields),+]);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,727 +0,0 @@
 | 
				
			|||||||
use std::env;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use base64;
 | 
					 | 
				
			||||||
use chrono::Utc;
 | 
					 | 
				
			||||||
use rocket::{
 | 
					 | 
				
			||||||
    http::CookieJar,
 | 
					 | 
				
			||||||
    serde::json::{json, Json, Value as JsonValue},
 | 
					 | 
				
			||||||
    State,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serde::Serialize;
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        channel::GuildChannel,
 | 
					 | 
				
			||||||
        id::{ChannelId, GuildId, RoleId},
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    check_guild_subscription, check_subscription,
 | 
					 | 
				
			||||||
    consts::{
 | 
					 | 
				
			||||||
        DAY, 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_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH,
 | 
					 | 
				
			||||||
        MIN_INTERVAL,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    routes::dashboard::{
 | 
					 | 
				
			||||||
        create_database_channel, generate_uid, name_default, template_name_default, DeleteReminder,
 | 
					 | 
				
			||||||
        DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize)]
 | 
					 | 
				
			||||||
struct ChannelInfo {
 | 
					 | 
				
			||||||
    id: String,
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
    webhook_avatar: Option<String>,
 | 
					 | 
				
			||||||
    webhook_name: Option<String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/guild/<id>/patreon")]
 | 
					 | 
				
			||||||
pub async fn get_guild_patreon(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
) -> JsonValue {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match GuildId(id).to_guild_cached(ctx.inner()) {
 | 
					 | 
				
			||||||
        Some(guild) => {
 | 
					 | 
				
			||||||
            let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
 | 
					 | 
				
			||||||
                .member(&ctx.inner(), guild.owner_id)
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let patreon = member_res.map_or(false, |member| {
 | 
					 | 
				
			||||||
                member
 | 
					 | 
				
			||||||
                    .roles
 | 
					 | 
				
			||||||
                    .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!({ "patreon": patreon })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        None => {
 | 
					 | 
				
			||||||
            json!({"error": "Bot not in guild"})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/guild/<id>/channels")]
 | 
					 | 
				
			||||||
pub async fn get_guild_channels(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
) -> JsonValue {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match GuildId(id).to_guild_cached(ctx.inner()) {
 | 
					 | 
				
			||||||
        Some(guild) => {
 | 
					 | 
				
			||||||
            let mut channels = guild
 | 
					 | 
				
			||||||
                .channels
 | 
					 | 
				
			||||||
                .iter()
 | 
					 | 
				
			||||||
                .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c)))
 | 
					 | 
				
			||||||
                .filter(|(_, channel)| channel.is_text_based())
 | 
					 | 
				
			||||||
                .collect::<Vec<(ChannelId, GuildChannel)>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let channel_info = channels
 | 
					 | 
				
			||||||
                .iter()
 | 
					 | 
				
			||||||
                .map(|(channel_id, channel)| ChannelInfo {
 | 
					 | 
				
			||||||
                    name: channel.name.to_string(),
 | 
					 | 
				
			||||||
                    id: channel_id.to_string(),
 | 
					 | 
				
			||||||
                    webhook_avatar: None,
 | 
					 | 
				
			||||||
                    webhook_name: None,
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .collect::<Vec<ChannelInfo>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!(channel_info)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        None => {
 | 
					 | 
				
			||||||
            json!({"error": "Bot not in guild"})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize)]
 | 
					 | 
				
			||||||
struct RoleInfo {
 | 
					 | 
				
			||||||
    id: String,
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/guild/<id>/roles")]
 | 
					 | 
				
			||||||
pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonValue {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let roles_res = ctx.cache.guild_roles(id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match roles_res {
 | 
					 | 
				
			||||||
        Some(roles) => {
 | 
					 | 
				
			||||||
            let roles = roles
 | 
					 | 
				
			||||||
                .iter()
 | 
					 | 
				
			||||||
                .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() })
 | 
					 | 
				
			||||||
                .collect::<Vec<RoleInfo>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!(roles)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        None => {
 | 
					 | 
				
			||||||
            warn!("Could not fetch roles from {}", id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!({"error": "Could not get roles"})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/guild/<id>/templates")]
 | 
					 | 
				
			||||||
pub async fn get_reminder_templates(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonValue {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
        ReminderTemplate,
 | 
					 | 
				
			||||||
        "SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
					 | 
				
			||||||
        id
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_all(pool.inner())
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Ok(templates) => {
 | 
					 | 
				
			||||||
            json!(templates)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!({"error": "Could not get templates"})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[post("/api/guild/<id>/templates", data = "<reminder_template>")]
 | 
					 | 
				
			||||||
pub async fn create_reminder_template(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    reminder_template: Json<ReminderTemplate>,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonValue {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // validate lengths
 | 
					 | 
				
			||||||
    check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
 | 
					 | 
				
			||||||
    check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description);
 | 
					 | 
				
			||||||
    check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title);
 | 
					 | 
				
			||||||
    check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author);
 | 
					 | 
				
			||||||
    check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer);
 | 
					 | 
				
			||||||
    check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields);
 | 
					 | 
				
			||||||
    if let Some(fields) = &reminder_template.embed_fields {
 | 
					 | 
				
			||||||
        for field in &fields.0 {
 | 
					 | 
				
			||||||
            check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
 | 
					 | 
				
			||||||
            check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username);
 | 
					 | 
				
			||||||
    check_length_opt!(
 | 
					 | 
				
			||||||
        MAX_URL_LENGTH,
 | 
					 | 
				
			||||||
        reminder_template.embed_footer_url,
 | 
					 | 
				
			||||||
        reminder_template.embed_thumbnail_url,
 | 
					 | 
				
			||||||
        reminder_template.embed_author_url,
 | 
					 | 
				
			||||||
        reminder_template.embed_image_url,
 | 
					 | 
				
			||||||
        reminder_template.avatar
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // validate urls
 | 
					 | 
				
			||||||
    check_url_opt!(
 | 
					 | 
				
			||||||
        reminder_template.embed_footer_url,
 | 
					 | 
				
			||||||
        reminder_template.embed_thumbnail_url,
 | 
					 | 
				
			||||||
        reminder_template.embed_author_url,
 | 
					 | 
				
			||||||
        reminder_template.embed_image_url,
 | 
					 | 
				
			||||||
        reminder_template.avatar
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let name = if reminder_template.name.is_empty() {
 | 
					 | 
				
			||||||
        template_name_default()
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        reminder_template.name.clone()
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match sqlx::query!(
 | 
					 | 
				
			||||||
        "INSERT INTO reminder_template
 | 
					 | 
				
			||||||
        (guild_id,
 | 
					 | 
				
			||||||
         name,
 | 
					 | 
				
			||||||
         attachment,
 | 
					 | 
				
			||||||
         attachment_name,
 | 
					 | 
				
			||||||
         avatar,
 | 
					 | 
				
			||||||
         content,
 | 
					 | 
				
			||||||
         embed_author,
 | 
					 | 
				
			||||||
         embed_author_url,
 | 
					 | 
				
			||||||
         embed_color,
 | 
					 | 
				
			||||||
         embed_description,
 | 
					 | 
				
			||||||
         embed_footer,
 | 
					 | 
				
			||||||
         embed_footer_url,
 | 
					 | 
				
			||||||
         embed_image_url,
 | 
					 | 
				
			||||||
         embed_thumbnail_url,
 | 
					 | 
				
			||||||
         embed_title,
 | 
					 | 
				
			||||||
         embed_fields,
 | 
					 | 
				
			||||||
         tts,
 | 
					 | 
				
			||||||
         username
 | 
					 | 
				
			||||||
        ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
 | 
					 | 
				
			||||||
        id, name,
 | 
					 | 
				
			||||||
        reminder_template.attachment,
 | 
					 | 
				
			||||||
        reminder_template.attachment_name,
 | 
					 | 
				
			||||||
        reminder_template.avatar,
 | 
					 | 
				
			||||||
        reminder_template.content,
 | 
					 | 
				
			||||||
        reminder_template.embed_author,
 | 
					 | 
				
			||||||
        reminder_template.embed_author_url,
 | 
					 | 
				
			||||||
        reminder_template.embed_color,
 | 
					 | 
				
			||||||
        reminder_template.embed_description,
 | 
					 | 
				
			||||||
        reminder_template.embed_footer,
 | 
					 | 
				
			||||||
        reminder_template.embed_footer_url,
 | 
					 | 
				
			||||||
        reminder_template.embed_image_url,
 | 
					 | 
				
			||||||
        reminder_template.embed_thumbnail_url,
 | 
					 | 
				
			||||||
        reminder_template.embed_title,
 | 
					 | 
				
			||||||
        reminder_template.embed_fields,
 | 
					 | 
				
			||||||
        reminder_template.tts,
 | 
					 | 
				
			||||||
        reminder_template.username,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_all(pool.inner())
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Ok(_) => {
 | 
					 | 
				
			||||||
            json!({})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!({"error": "Could not get templates"})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")]
 | 
					 | 
				
			||||||
pub async fn delete_reminder_template(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    delete_reminder_template: Json<DeleteReminderTemplate>,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonValue {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match sqlx::query!(
 | 
					 | 
				
			||||||
        "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?",
 | 
					 | 
				
			||||||
        id, delete_reminder_template.id
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_all(pool.inner())
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Ok(_) => {
 | 
					 | 
				
			||||||
            json!({})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Could not delete template from {}: {:?}", id, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!({"error": "Could not delete template"})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[post("/api/guild/<id>/reminders", data = "<reminder>")]
 | 
					 | 
				
			||||||
pub async fn create_reminder(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    reminder: Json<Reminder>,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    serenity_context: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonValue {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, serenity_context.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let user_id =
 | 
					 | 
				
			||||||
        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // validate channel
 | 
					 | 
				
			||||||
    let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner());
 | 
					 | 
				
			||||||
    let channel_exists = channel.is_some();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let channel_matches_guild =
 | 
					 | 
				
			||||||
        channel.map_or(false, |c| c.guild().map_or(false, |c| c.guild_id.0 == id));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if !channel_matches_guild || !channel_exists {
 | 
					 | 
				
			||||||
        warn!(
 | 
					 | 
				
			||||||
            "Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})",
 | 
					 | 
				
			||||||
            reminder.channel, id, channel_exists
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return json!({"error": "Channel not found"});
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let channel = create_database_channel(
 | 
					 | 
				
			||||||
        serenity_context.inner(),
 | 
					 | 
				
			||||||
        ChannelId(reminder.channel),
 | 
					 | 
				
			||||||
        pool.inner(),
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if let Err(e) = channel {
 | 
					 | 
				
			||||||
        warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"});
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let channel = channel.unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // validate lengths
 | 
					 | 
				
			||||||
    check_length!(MAX_CONTENT_LENGTH, reminder.content);
 | 
					 | 
				
			||||||
    check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder.embed_description);
 | 
					 | 
				
			||||||
    check_length!(MAX_EMBED_TITLE_LENGTH, reminder.embed_title);
 | 
					 | 
				
			||||||
    check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder.embed_author);
 | 
					 | 
				
			||||||
    check_length!(MAX_EMBED_FOOTER_LENGTH, reminder.embed_footer);
 | 
					 | 
				
			||||||
    check_length_opt!(MAX_EMBED_FIELDS, reminder.embed_fields);
 | 
					 | 
				
			||||||
    if let Some(fields) = &reminder.embed_fields {
 | 
					 | 
				
			||||||
        for field in &fields.0 {
 | 
					 | 
				
			||||||
            check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
 | 
					 | 
				
			||||||
            check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    check_length_opt!(MAX_USERNAME_LENGTH, reminder.username);
 | 
					 | 
				
			||||||
    check_length_opt!(
 | 
					 | 
				
			||||||
        MAX_URL_LENGTH,
 | 
					 | 
				
			||||||
        reminder.embed_footer_url,
 | 
					 | 
				
			||||||
        reminder.embed_thumbnail_url,
 | 
					 | 
				
			||||||
        reminder.embed_author_url,
 | 
					 | 
				
			||||||
        reminder.embed_image_url,
 | 
					 | 
				
			||||||
        reminder.avatar
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // validate urls
 | 
					 | 
				
			||||||
    check_url_opt!(
 | 
					 | 
				
			||||||
        reminder.embed_footer_url,
 | 
					 | 
				
			||||||
        reminder.embed_thumbnail_url,
 | 
					 | 
				
			||||||
        reminder.embed_author_url,
 | 
					 | 
				
			||||||
        reminder.embed_image_url,
 | 
					 | 
				
			||||||
        reminder.avatar
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // validate time and interval
 | 
					 | 
				
			||||||
    if reminder.utc_time < Utc::now().naive_utc() {
 | 
					 | 
				
			||||||
        return json!({"error": "Time must be in the future"});
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() {
 | 
					 | 
				
			||||||
        if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32
 | 
					 | 
				
			||||||
            + reminder.interval_seconds.unwrap_or(0)
 | 
					 | 
				
			||||||
            < *MIN_INTERVAL
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return json!({"error": "Interval too short"});
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // check patreon if necessary
 | 
					 | 
				
			||||||
    if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() {
 | 
					 | 
				
			||||||
        if !check_guild_subscription(serenity_context.inner(), GuildId(id)).await
 | 
					 | 
				
			||||||
            && !check_subscription(serenity_context.inner(), user_id).await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return json!({"error": "Patreon is required to set intervals"});
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // base64 decode error dropped here
 | 
					 | 
				
			||||||
    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 new_uid = generate_uid();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // write to db
 | 
					 | 
				
			||||||
    match sqlx::query!(
 | 
					 | 
				
			||||||
        "INSERT INTO reminders (
 | 
					 | 
				
			||||||
         uid,
 | 
					 | 
				
			||||||
         attachment,
 | 
					 | 
				
			||||||
         attachment_name,
 | 
					 | 
				
			||||||
         channel_id,
 | 
					 | 
				
			||||||
         avatar,
 | 
					 | 
				
			||||||
         content,
 | 
					 | 
				
			||||||
         embed_author,
 | 
					 | 
				
			||||||
         embed_author_url,
 | 
					 | 
				
			||||||
         embed_color,
 | 
					 | 
				
			||||||
         embed_description,
 | 
					 | 
				
			||||||
         embed_footer,
 | 
					 | 
				
			||||||
         embed_footer_url,
 | 
					 | 
				
			||||||
         embed_image_url,
 | 
					 | 
				
			||||||
         embed_thumbnail_url,
 | 
					 | 
				
			||||||
         embed_title,
 | 
					 | 
				
			||||||
         embed_fields,
 | 
					 | 
				
			||||||
         enabled,
 | 
					 | 
				
			||||||
         expires,
 | 
					 | 
				
			||||||
         interval_seconds,
 | 
					 | 
				
			||||||
         interval_months,
 | 
					 | 
				
			||||||
         name,
 | 
					 | 
				
			||||||
         restartable,
 | 
					 | 
				
			||||||
         tts,
 | 
					 | 
				
			||||||
         username,
 | 
					 | 
				
			||||||
         `utc_time`
 | 
					 | 
				
			||||||
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
 | 
					 | 
				
			||||||
        new_uid,
 | 
					 | 
				
			||||||
        attachment_data,
 | 
					 | 
				
			||||||
        reminder.attachment_name,
 | 
					 | 
				
			||||||
        channel,
 | 
					 | 
				
			||||||
        reminder.avatar,
 | 
					 | 
				
			||||||
        reminder.content,
 | 
					 | 
				
			||||||
        reminder.embed_author,
 | 
					 | 
				
			||||||
        reminder.embed_author_url,
 | 
					 | 
				
			||||||
        reminder.embed_color,
 | 
					 | 
				
			||||||
        reminder.embed_description,
 | 
					 | 
				
			||||||
        reminder.embed_footer,
 | 
					 | 
				
			||||||
        reminder.embed_footer_url,
 | 
					 | 
				
			||||||
        reminder.embed_image_url,
 | 
					 | 
				
			||||||
        reminder.embed_thumbnail_url,
 | 
					 | 
				
			||||||
        reminder.embed_title,
 | 
					 | 
				
			||||||
        reminder.embed_fields,
 | 
					 | 
				
			||||||
        reminder.enabled,
 | 
					 | 
				
			||||||
        reminder.expires,
 | 
					 | 
				
			||||||
        reminder.interval_seconds,
 | 
					 | 
				
			||||||
        reminder.interval_months,
 | 
					 | 
				
			||||||
        name,
 | 
					 | 
				
			||||||
        reminder.restartable,
 | 
					 | 
				
			||||||
        reminder.tts,
 | 
					 | 
				
			||||||
        reminder.username,
 | 
					 | 
				
			||||||
        reminder.utc_time,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .execute(pool.inner())
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Ok(_) => sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Reminder,
 | 
					 | 
				
			||||||
            "SELECT
 | 
					 | 
				
			||||||
             reminders.attachment,
 | 
					 | 
				
			||||||
             reminders.attachment_name,
 | 
					 | 
				
			||||||
             reminders.avatar,
 | 
					 | 
				
			||||||
             channels.channel,
 | 
					 | 
				
			||||||
             reminders.content,
 | 
					 | 
				
			||||||
             reminders.embed_author,
 | 
					 | 
				
			||||||
             reminders.embed_author_url,
 | 
					 | 
				
			||||||
             reminders.embed_color,
 | 
					 | 
				
			||||||
             reminders.embed_description,
 | 
					 | 
				
			||||||
             reminders.embed_footer,
 | 
					 | 
				
			||||||
             reminders.embed_footer_url,
 | 
					 | 
				
			||||||
             reminders.embed_image_url,
 | 
					 | 
				
			||||||
             reminders.embed_thumbnail_url,
 | 
					 | 
				
			||||||
             reminders.embed_title,
 | 
					 | 
				
			||||||
             reminders.embed_fields,
 | 
					 | 
				
			||||||
             reminders.enabled,
 | 
					 | 
				
			||||||
             reminders.expires,
 | 
					 | 
				
			||||||
             reminders.interval_seconds,
 | 
					 | 
				
			||||||
             reminders.interval_months,
 | 
					 | 
				
			||||||
             reminders.name,
 | 
					 | 
				
			||||||
             reminders.restartable,
 | 
					 | 
				
			||||||
             reminders.tts,
 | 
					 | 
				
			||||||
             reminders.uid,
 | 
					 | 
				
			||||||
             reminders.username,
 | 
					 | 
				
			||||||
             reminders.utc_time
 | 
					 | 
				
			||||||
            FROM reminders
 | 
					 | 
				
			||||||
            LEFT JOIN channels ON channels.id = reminders.channel_id
 | 
					 | 
				
			||||||
            WHERE uid = ?",
 | 
					 | 
				
			||||||
            new_uid
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(pool.inner())
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .map(|r| json!(r))
 | 
					 | 
				
			||||||
        .unwrap_or_else(|e| {
 | 
					 | 
				
			||||||
            warn!("Failed to complete SQL query: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!({"error": "Could not load reminder"})
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Error in `create_reminder`: Could not execute query: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!({"error": "Unknown error"})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/guild/<id>/reminders")]
 | 
					 | 
				
			||||||
pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySql>>) -> JsonValue {
 | 
					 | 
				
			||||||
    let channels_res = GuildId(id).channels(&ctx.inner()).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match channels_res {
 | 
					 | 
				
			||||||
        Ok(channels) => {
 | 
					 | 
				
			||||||
            let channels = channels
 | 
					 | 
				
			||||||
                .keys()
 | 
					 | 
				
			||||||
                .into_iter()
 | 
					 | 
				
			||||||
                .map(|k| k.as_u64().to_string())
 | 
					 | 
				
			||||||
                .collect::<Vec<String>>()
 | 
					 | 
				
			||||||
                .join(",");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
                Reminder,
 | 
					 | 
				
			||||||
                "SELECT
 | 
					 | 
				
			||||||
                 reminders.attachment,
 | 
					 | 
				
			||||||
                 reminders.attachment_name,
 | 
					 | 
				
			||||||
                 reminders.avatar,
 | 
					 | 
				
			||||||
                 channels.channel,
 | 
					 | 
				
			||||||
                 reminders.content,
 | 
					 | 
				
			||||||
                 reminders.embed_author,
 | 
					 | 
				
			||||||
                 reminders.embed_author_url,
 | 
					 | 
				
			||||||
                 reminders.embed_color,
 | 
					 | 
				
			||||||
                 reminders.embed_description,
 | 
					 | 
				
			||||||
                 reminders.embed_footer,
 | 
					 | 
				
			||||||
                 reminders.embed_footer_url,
 | 
					 | 
				
			||||||
                 reminders.embed_image_url,
 | 
					 | 
				
			||||||
                 reminders.embed_thumbnail_url,
 | 
					 | 
				
			||||||
                 reminders.embed_title,
 | 
					 | 
				
			||||||
                 reminders.embed_fields,
 | 
					 | 
				
			||||||
                 reminders.enabled,
 | 
					 | 
				
			||||||
                 reminders.expires,
 | 
					 | 
				
			||||||
                 reminders.interval_seconds,
 | 
					 | 
				
			||||||
                 reminders.interval_months,
 | 
					 | 
				
			||||||
                 reminders.name,
 | 
					 | 
				
			||||||
                 reminders.restartable,
 | 
					 | 
				
			||||||
                 reminders.tts,
 | 
					 | 
				
			||||||
                 reminders.uid,
 | 
					 | 
				
			||||||
                 reminders.username,
 | 
					 | 
				
			||||||
                 reminders.utc_time
 | 
					 | 
				
			||||||
                FROM reminders
 | 
					 | 
				
			||||||
                LEFT JOIN channels ON channels.id = reminders.channel_id
 | 
					 | 
				
			||||||
                WHERE FIND_IN_SET(channels.channel, ?)",
 | 
					 | 
				
			||||||
                channels
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .fetch_all(pool.inner())
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map(|r| json!(r))
 | 
					 | 
				
			||||||
            .unwrap_or_else(|e| {
 | 
					 | 
				
			||||||
                warn!("Failed to complete SQL query: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                json!({"error": "Could not load reminders"})
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Could not fetch channels from {}: {:?}", id, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!([])
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[patch("/api/guild/<id>/reminders", data = "<reminder>")]
 | 
					 | 
				
			||||||
pub async fn edit_reminder(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    reminder: Json<PatchReminder>,
 | 
					 | 
				
			||||||
    serenity_context: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonValue {
 | 
					 | 
				
			||||||
    let mut error = vec![];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    update_field!(pool.inner(), error, reminder.[
 | 
					 | 
				
			||||||
        attachment,
 | 
					 | 
				
			||||||
        attachment_name,
 | 
					 | 
				
			||||||
        avatar,
 | 
					 | 
				
			||||||
        content,
 | 
					 | 
				
			||||||
        embed_author,
 | 
					 | 
				
			||||||
        embed_author_url,
 | 
					 | 
				
			||||||
        embed_color,
 | 
					 | 
				
			||||||
        embed_description,
 | 
					 | 
				
			||||||
        embed_footer,
 | 
					 | 
				
			||||||
        embed_footer_url,
 | 
					 | 
				
			||||||
        embed_image_url,
 | 
					 | 
				
			||||||
        embed_thumbnail_url,
 | 
					 | 
				
			||||||
        embed_title,
 | 
					 | 
				
			||||||
        embed_fields,
 | 
					 | 
				
			||||||
        enabled,
 | 
					 | 
				
			||||||
        expires,
 | 
					 | 
				
			||||||
        interval_seconds,
 | 
					 | 
				
			||||||
        interval_months,
 | 
					 | 
				
			||||||
        name,
 | 
					 | 
				
			||||||
        restartable,
 | 
					 | 
				
			||||||
        tts,
 | 
					 | 
				
			||||||
        username,
 | 
					 | 
				
			||||||
        utc_time
 | 
					 | 
				
			||||||
    ]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if reminder.channel > 0 {
 | 
					 | 
				
			||||||
        let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner());
 | 
					 | 
				
			||||||
        match channel {
 | 
					 | 
				
			||||||
            Some(channel) => {
 | 
					 | 
				
			||||||
                let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if !channel_matches_guild {
 | 
					 | 
				
			||||||
                    warn!(
 | 
					 | 
				
			||||||
                        "Error in `edit_reminder`: channel {:?} not found for guild {}",
 | 
					 | 
				
			||||||
                        reminder.channel, id
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return json!({"error": "Channel not found"});
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let channel = create_database_channel(
 | 
					 | 
				
			||||||
                    serenity_context.inner(),
 | 
					 | 
				
			||||||
                    ChannelId(reminder.channel),
 | 
					 | 
				
			||||||
                    pool.inner(),
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Err(e) = channel {
 | 
					 | 
				
			||||||
                    warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"});
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let channel = channel.unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                match sqlx::query!(
 | 
					 | 
				
			||||||
                    "UPDATE reminders SET channel_id = ? WHERE uid = ?",
 | 
					 | 
				
			||||||
                    channel,
 | 
					 | 
				
			||||||
                    reminder.uid
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .execute(pool.inner())
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    Ok(_) => {}
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        warn!("Error setting channel: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        error.push("Couldn't set channel".to_string())
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            None => {
 | 
					 | 
				
			||||||
                warn!(
 | 
					 | 
				
			||||||
                    "Error in `edit_reminder`: channel {:?} not found for guild {}",
 | 
					 | 
				
			||||||
                    reminder.channel, id
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return json!({"error": "Channel not found"});
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
        Reminder,
 | 
					 | 
				
			||||||
        "SELECT reminders.attachment,
 | 
					 | 
				
			||||||
         reminders.attachment_name,
 | 
					 | 
				
			||||||
         reminders.avatar,
 | 
					 | 
				
			||||||
         channels.channel,
 | 
					 | 
				
			||||||
         reminders.content,
 | 
					 | 
				
			||||||
         reminders.embed_author,
 | 
					 | 
				
			||||||
         reminders.embed_author_url,
 | 
					 | 
				
			||||||
         reminders.embed_color,
 | 
					 | 
				
			||||||
         reminders.embed_description,
 | 
					 | 
				
			||||||
         reminders.embed_footer,
 | 
					 | 
				
			||||||
         reminders.embed_footer_url,
 | 
					 | 
				
			||||||
         reminders.embed_image_url,
 | 
					 | 
				
			||||||
         reminders.embed_thumbnail_url,
 | 
					 | 
				
			||||||
         reminders.embed_title,
 | 
					 | 
				
			||||||
         reminders.embed_fields,
 | 
					 | 
				
			||||||
         reminders.enabled,
 | 
					 | 
				
			||||||
         reminders.expires,
 | 
					 | 
				
			||||||
         reminders.interval_seconds,
 | 
					 | 
				
			||||||
         reminders.interval_months,
 | 
					 | 
				
			||||||
         reminders.name,
 | 
					 | 
				
			||||||
         reminders.restartable,
 | 
					 | 
				
			||||||
         reminders.tts,
 | 
					 | 
				
			||||||
         reminders.uid,
 | 
					 | 
				
			||||||
         reminders.username,
 | 
					 | 
				
			||||||
         reminders.utc_time
 | 
					 | 
				
			||||||
        FROM reminders
 | 
					 | 
				
			||||||
        LEFT JOIN channels ON channels.id = reminders.channel_id
 | 
					 | 
				
			||||||
        WHERE uid = ?",
 | 
					 | 
				
			||||||
        reminder.uid
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_one(pool.inner())
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Ok(reminder) => json!({"reminder": reminder, "errors": error}),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Error exiting `edit_reminder': {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[delete("/api/guild/<_>/reminders", data = "<reminder>")]
 | 
					 | 
				
			||||||
pub async fn delete_reminder(
 | 
					 | 
				
			||||||
    reminder: Json<DeleteReminder>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonValue {
 | 
					 | 
				
			||||||
    match sqlx::query!("DELETE FROM reminders WHERE uid = ?", reminder.uid)
 | 
					 | 
				
			||||||
        .execute(pool.inner())
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Ok(_) => {
 | 
					 | 
				
			||||||
            json!({})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Error in `delete_reminder`: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!({"error": "Could not delete reminder"})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,311 +0,0 @@
 | 
				
			|||||||
use std::collections::HashMap;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::naive::NaiveDateTime;
 | 
					 | 
				
			||||||
use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
					 | 
				
			||||||
use rocket::{http::CookieJar, response::Redirect};
 | 
					 | 
				
			||||||
use rocket_dyn_templates::Template;
 | 
					 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					 | 
				
			||||||
use serenity::{http::Http, model::id::ChannelId};
 | 
					 | 
				
			||||||
use sqlx::{types::Json, Executor};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    consts::{CHARACTERS, DEFAULT_AVATAR},
 | 
					 | 
				
			||||||
    Database, Error,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub mod guild;
 | 
					 | 
				
			||||||
pub mod user;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Unset<T> = Option<T>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn name_default() -> String {
 | 
					 | 
				
			||||||
    "Reminder".to_string()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn template_name_default() -> String {
 | 
					 | 
				
			||||||
    "Template".to_string()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn channel_default() -> u64 {
 | 
					 | 
				
			||||||
    0
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn id_default() -> u32 {
 | 
					 | 
				
			||||||
    0
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct ReminderTemplate {
 | 
					 | 
				
			||||||
    #[serde(default = "id_default")]
 | 
					 | 
				
			||||||
    id: u32,
 | 
					 | 
				
			||||||
    #[serde(default = "id_default")]
 | 
					 | 
				
			||||||
    guild_id: u32,
 | 
					 | 
				
			||||||
    #[serde(default = "template_name_default")]
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
    attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    attachment_name: Option<String>,
 | 
					 | 
				
			||||||
    avatar: Option<String>,
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
    embed_author: String,
 | 
					 | 
				
			||||||
    embed_author_url: Option<String>,
 | 
					 | 
				
			||||||
    embed_color: u32,
 | 
					 | 
				
			||||||
    embed_description: String,
 | 
					 | 
				
			||||||
    embed_footer: String,
 | 
					 | 
				
			||||||
    embed_footer_url: Option<String>,
 | 
					 | 
				
			||||||
    embed_image_url: Option<String>,
 | 
					 | 
				
			||||||
    embed_thumbnail_url: Option<String>,
 | 
					 | 
				
			||||||
    embed_title: String,
 | 
					 | 
				
			||||||
    embed_fields: Option<Json<Vec<EmbedField>>>,
 | 
					 | 
				
			||||||
    tts: bool,
 | 
					 | 
				
			||||||
    username: Option<String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					 | 
				
			||||||
pub struct DeleteReminderTemplate {
 | 
					 | 
				
			||||||
    id: u32,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct EmbedField {
 | 
					 | 
				
			||||||
    title: String,
 | 
					 | 
				
			||||||
    value: String,
 | 
					 | 
				
			||||||
    inline: bool,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct Reminder {
 | 
					 | 
				
			||||||
    #[serde(with = "base64s")]
 | 
					 | 
				
			||||||
    attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    attachment_name: Option<String>,
 | 
					 | 
				
			||||||
    avatar: Option<String>,
 | 
					 | 
				
			||||||
    #[serde(with = "string")]
 | 
					 | 
				
			||||||
    channel: u64,
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
    embed_author: String,
 | 
					 | 
				
			||||||
    embed_author_url: Option<String>,
 | 
					 | 
				
			||||||
    embed_color: u32,
 | 
					 | 
				
			||||||
    embed_description: String,
 | 
					 | 
				
			||||||
    embed_footer: String,
 | 
					 | 
				
			||||||
    embed_footer_url: Option<String>,
 | 
					 | 
				
			||||||
    embed_image_url: Option<String>,
 | 
					 | 
				
			||||||
    embed_thumbnail_url: Option<String>,
 | 
					 | 
				
			||||||
    embed_title: String,
 | 
					 | 
				
			||||||
    embed_fields: Option<Json<Vec<EmbedField>>>,
 | 
					 | 
				
			||||||
    enabled: bool,
 | 
					 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    interval_seconds: Option<u32>,
 | 
					 | 
				
			||||||
    interval_months: Option<u32>,
 | 
					 | 
				
			||||||
    #[serde(default = "name_default")]
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
    restartable: bool,
 | 
					 | 
				
			||||||
    tts: bool,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    uid: String,
 | 
					 | 
				
			||||||
    username: Option<String>,
 | 
					 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					 | 
				
			||||||
pub struct PatchReminder {
 | 
					 | 
				
			||||||
    uid: String,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    attachment: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    attachment_name: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    avatar: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default = "channel_default")]
 | 
					 | 
				
			||||||
    #[serde(with = "string")]
 | 
					 | 
				
			||||||
    channel: u64,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    content: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    embed_author: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    embed_author_url: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    embed_color: Unset<u32>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    embed_description: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    embed_footer: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    embed_footer_url: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    embed_image_url: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    embed_thumbnail_url: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    embed_title: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    embed_fields: Unset<Json<Vec<EmbedField>>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    enabled: Unset<bool>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    expires: Unset<Option<NaiveDateTime>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    interval_seconds: Unset<Option<u32>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    interval_months: Unset<Option<u32>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    name: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    restartable: Unset<bool>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    tts: Unset<bool>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    username: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    utc_time: Unset<NaiveDateTime>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn generate_uid() -> String {
 | 
					 | 
				
			||||||
    let mut generator: OsRng = Default::default();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    (0..64)
 | 
					 | 
				
			||||||
        .map(|_| CHARACTERS.chars().choose(&mut generator).unwrap().to_owned().to_string())
 | 
					 | 
				
			||||||
        .collect::<Vec<String>>()
 | 
					 | 
				
			||||||
        .join("")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// https://github.com/serde-rs/json/issues/329#issuecomment-305608405
 | 
					 | 
				
			||||||
mod string {
 | 
					 | 
				
			||||||
    use std::{fmt::Display, str::FromStr};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    use serde::{de, Deserialize, Deserializer, Serializer};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
 | 
					 | 
				
			||||||
    where
 | 
					 | 
				
			||||||
        T: Display,
 | 
					 | 
				
			||||||
        S: Serializer,
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        serializer.collect_str(value)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
 | 
					 | 
				
			||||||
    where
 | 
					 | 
				
			||||||
        T: FromStr,
 | 
					 | 
				
			||||||
        T::Err: Display,
 | 
					 | 
				
			||||||
        D: Deserializer<'de>,
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
mod base64s {
 | 
					 | 
				
			||||||
    use serde::{de, Deserialize, Deserializer, Serializer};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
 | 
					 | 
				
			||||||
    where
 | 
					 | 
				
			||||||
        S: Serializer,
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if let Some(opt) = value {
 | 
					 | 
				
			||||||
            serializer.collect_str(&base64::encode(opt))
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            serializer.serialize_none()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
 | 
					 | 
				
			||||||
    where
 | 
					 | 
				
			||||||
        D: Deserializer<'de>,
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let string = Option::<String>::deserialize(deserializer)?;
 | 
					 | 
				
			||||||
        Some(string.map(|b| base64::decode(b).map_err(de::Error::custom))).flatten().transpose()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					 | 
				
			||||||
pub struct DeleteReminder {
 | 
					 | 
				
			||||||
    uid: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn create_database_channel(
 | 
					 | 
				
			||||||
    ctx: impl AsRef<Http>,
 | 
					 | 
				
			||||||
    channel: ChannelId,
 | 
					 | 
				
			||||||
    pool: impl Executor<'_, Database = Database> + Copy,
 | 
					 | 
				
			||||||
) -> Result<u32, crate::Error> {
 | 
					 | 
				
			||||||
    println!("{:?}", channel);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let row =
 | 
					 | 
				
			||||||
        sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0)
 | 
					 | 
				
			||||||
            .fetch_one(pool)
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match row {
 | 
					 | 
				
			||||||
        Ok(row) => {
 | 
					 | 
				
			||||||
            if row.webhook_token.is_none() || row.webhook_id.is_none() {
 | 
					 | 
				
			||||||
                let webhook = channel
 | 
					 | 
				
			||||||
                    .create_webhook_with_avatar(&ctx, "Reminder", DEFAULT_AVATAR.clone())
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .map_err(|e| Error::Serenity(e))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                sqlx::query!(
 | 
					 | 
				
			||||||
                    "UPDATE channels SET webhook_id = ?, webhook_token = ? WHERE channel = ?",
 | 
					 | 
				
			||||||
                    webhook.id.0,
 | 
					 | 
				
			||||||
                    webhook.token,
 | 
					 | 
				
			||||||
                    channel.0
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .execute(pool)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .map_err(|e| Error::SQLx(e))?;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Ok(())
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(sqlx::Error::RowNotFound) => {
 | 
					 | 
				
			||||||
            // create webhook
 | 
					 | 
				
			||||||
            let webhook = channel
 | 
					 | 
				
			||||||
                .create_webhook_with_avatar(&ctx, "Reminder", DEFAULT_AVATAR.clone())
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .map_err(|e| Error::Serenity(e))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // create database entry
 | 
					 | 
				
			||||||
            sqlx::query!(
 | 
					 | 
				
			||||||
                "INSERT INTO channels (
 | 
					 | 
				
			||||||
                 webhook_id,
 | 
					 | 
				
			||||||
                 webhook_token,
 | 
					 | 
				
			||||||
                 channel
 | 
					 | 
				
			||||||
                ) VALUES (?, ?, ?)",
 | 
					 | 
				
			||||||
                webhook.id.0,
 | 
					 | 
				
			||||||
                webhook.token,
 | 
					 | 
				
			||||||
                channel.0
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(pool)
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_err(|e| Error::SQLx(e))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Ok(())
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(e) => Err(Error::SQLx(e)),
 | 
					 | 
				
			||||||
    }?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.0)
 | 
					 | 
				
			||||||
        .fetch_one(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .map_err(|e| Error::SQLx(e))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(row.id)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/")]
 | 
					 | 
				
			||||||
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
 | 
					 | 
				
			||||||
    if cookies.get_private("userid").is_some() {
 | 
					 | 
				
			||||||
        let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
        Ok(Template::render("dashboard", &map))
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        Err(Redirect::to("/login/discord"))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/<_>")]
 | 
					 | 
				
			||||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
 | 
					 | 
				
			||||||
    if cookies.get_private("userid").is_some() {
 | 
					 | 
				
			||||||
        let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
        Ok(Template::render("dashboard", &map))
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        Err(Redirect::to("/login/discord"))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,165 +0,0 @@
 | 
				
			|||||||
use std::env;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use reqwest::Client;
 | 
					 | 
				
			||||||
use rocket::{
 | 
					 | 
				
			||||||
    http::CookieJar,
 | 
					 | 
				
			||||||
    serde::json::{json, Json, Value as JsonValue},
 | 
					 | 
				
			||||||
    State,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        id::{GuildId, RoleId},
 | 
					 | 
				
			||||||
        permissions::Permissions,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::consts::DISCORD_API;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize)]
 | 
					 | 
				
			||||||
struct UserInfo {
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
    patreon: bool,
 | 
					 | 
				
			||||||
    timezone: Option<String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					 | 
				
			||||||
pub struct UpdateUser {
 | 
					 | 
				
			||||||
    timezone: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize)]
 | 
					 | 
				
			||||||
struct GuildInfo {
 | 
					 | 
				
			||||||
    id: String,
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					 | 
				
			||||||
pub struct PartialGuild {
 | 
					 | 
				
			||||||
    pub id: GuildId,
 | 
					 | 
				
			||||||
    pub icon: Option<String>,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub owner: bool,
 | 
					 | 
				
			||||||
    #[serde(rename = "permissions_new")]
 | 
					 | 
				
			||||||
    pub permissions: Option<String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/user")]
 | 
					 | 
				
			||||||
pub async fn get_user_info(
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonValue {
 | 
					 | 
				
			||||||
    if let Some(user_id) =
 | 
					 | 
				
			||||||
        cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
 | 
					 | 
				
			||||||
            .member(&ctx.inner(), user_id)
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let timezone = sqlx::query!("SELECT timezone FROM users WHERE user = ?", user_id)
 | 
					 | 
				
			||||||
            .fetch_one(pool.inner())
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_or(None, |q| Some(q.timezone));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let user_info = UserInfo {
 | 
					 | 
				
			||||||
            name: cookies
 | 
					 | 
				
			||||||
                .get_private("username")
 | 
					 | 
				
			||||||
                .map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()),
 | 
					 | 
				
			||||||
            patreon: member_res.map_or(false, |member| {
 | 
					 | 
				
			||||||
                member
 | 
					 | 
				
			||||||
                    .roles
 | 
					 | 
				
			||||||
                    .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            timezone,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        json!(user_info)
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        json!({"error": "Not authorized"})
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[patch("/api/user", data = "<user>")]
 | 
					 | 
				
			||||||
pub async fn update_user_info(
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    user: Json<UpdateUser>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonValue {
 | 
					 | 
				
			||||||
    if let Some(user_id) =
 | 
					 | 
				
			||||||
        cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if user.timezone.parse::<Tz>().is_ok() {
 | 
					 | 
				
			||||||
            let _ = sqlx::query!(
 | 
					 | 
				
			||||||
                "UPDATE users SET timezone = ? WHERE user = ?",
 | 
					 | 
				
			||||||
                user.timezone,
 | 
					 | 
				
			||||||
                user_id,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(pool.inner())
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json!({})
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            json!({"error": "Timezone not recognized"})
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        json!({"error": "Not authorized"})
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/user/guilds")]
 | 
					 | 
				
			||||||
pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue {
 | 
					 | 
				
			||||||
    if let Some(access_token) = cookies.get_private("access_token") {
 | 
					 | 
				
			||||||
        let request_res = reqwest_client
 | 
					 | 
				
			||||||
            .get(format!("{}/users/@me/guilds", DISCORD_API))
 | 
					 | 
				
			||||||
            .bearer_auth(access_token.value())
 | 
					 | 
				
			||||||
            .send()
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match request_res {
 | 
					 | 
				
			||||||
            Ok(response) => {
 | 
					 | 
				
			||||||
                let guilds_res = response.json::<Vec<PartialGuild>>().await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                match guilds_res {
 | 
					 | 
				
			||||||
                    Ok(guilds) => {
 | 
					 | 
				
			||||||
                        let reduced_guilds = guilds
 | 
					 | 
				
			||||||
                            .iter()
 | 
					 | 
				
			||||||
                            .filter(|g| {
 | 
					 | 
				
			||||||
                                g.owner
 | 
					 | 
				
			||||||
                                    || g.permissions.as_ref().map_or(false, |p| {
 | 
					 | 
				
			||||||
                                        let permissions =
 | 
					 | 
				
			||||||
                                            Permissions::from_bits_truncate(p.parse().unwrap());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                        permissions.manage_messages()
 | 
					 | 
				
			||||||
                                            || permissions.manage_guild()
 | 
					 | 
				
			||||||
                                            || permissions.administrator()
 | 
					 | 
				
			||||||
                                    })
 | 
					 | 
				
			||||||
                            })
 | 
					 | 
				
			||||||
                            .map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() })
 | 
					 | 
				
			||||||
                            .collect::<Vec<GuildInfo>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        json!(reduced_guilds)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        warn!("Error constructing user from request: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        json!({"error": "Could not get user details"})
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(e) => {
 | 
					 | 
				
			||||||
                warn!("Error getting user guilds: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                json!({"error": "Could not reach Discord"})
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        json!({"error": "Not authorized"})
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,149 +0,0 @@
 | 
				
			|||||||
use log::warn;
 | 
					 | 
				
			||||||
use oauth2::{
 | 
					 | 
				
			||||||
    basic::BasicClient, reqwest::async_http_client, AuthorizationCode, CsrfToken,
 | 
					 | 
				
			||||||
    PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use reqwest::Client;
 | 
					 | 
				
			||||||
use rocket::{
 | 
					 | 
				
			||||||
    http::{private::cookie::Expiration, Cookie, CookieJar, SameSite},
 | 
					 | 
				
			||||||
    response::{Flash, Redirect},
 | 
					 | 
				
			||||||
    uri, State,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serenity::model::user::User;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::consts::DISCORD_API;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/discord")]
 | 
					 | 
				
			||||||
pub async fn discord_login(
 | 
					 | 
				
			||||||
    oauth2_client: &State<BasicClient>,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
) -> Redirect {
 | 
					 | 
				
			||||||
    let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let (auth_url, csrf_token) = oauth2_client
 | 
					 | 
				
			||||||
        .authorize_url(CsrfToken::new_random)
 | 
					 | 
				
			||||||
        // Set the desired scopes.
 | 
					 | 
				
			||||||
        .add_scope(Scope::new("identify".to_string()))
 | 
					 | 
				
			||||||
        .add_scope(Scope::new("guilds".to_string()))
 | 
					 | 
				
			||||||
        .add_scope(Scope::new("email".to_string()))
 | 
					 | 
				
			||||||
        // Set the PKCE code challenge.
 | 
					 | 
				
			||||||
        .set_pkce_challenge(pkce_challenge)
 | 
					 | 
				
			||||||
        .url();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // store the pkce secret to verify the authorization later
 | 
					 | 
				
			||||||
    cookies.add_private(
 | 
					 | 
				
			||||||
        Cookie::build("verify", pkce_verifier.secret().to_string())
 | 
					 | 
				
			||||||
            .http_only(true)
 | 
					 | 
				
			||||||
            .path("/login")
 | 
					 | 
				
			||||||
            .same_site(SameSite::Lax)
 | 
					 | 
				
			||||||
            .expires(Expiration::Session)
 | 
					 | 
				
			||||||
            .finish(),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // store the csrf token to verify no interference
 | 
					 | 
				
			||||||
    cookies.add_private(
 | 
					 | 
				
			||||||
        Cookie::build("csrf", csrf_token.secret().to_string())
 | 
					 | 
				
			||||||
            .http_only(true)
 | 
					 | 
				
			||||||
            .path("/login")
 | 
					 | 
				
			||||||
            .same_site(SameSite::Lax)
 | 
					 | 
				
			||||||
            .expires(Expiration::Session)
 | 
					 | 
				
			||||||
            .finish(),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Redirect::to(auth_url.to_string())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/discord/authorized?<code>&<state>")]
 | 
					 | 
				
			||||||
pub async fn discord_callback(
 | 
					 | 
				
			||||||
    code: &str,
 | 
					 | 
				
			||||||
    state: &str,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    oauth2_client: &State<BasicClient>,
 | 
					 | 
				
			||||||
    reqwest_client: &State<Client>,
 | 
					 | 
				
			||||||
) -> Result<Redirect, Flash<Redirect>> {
 | 
					 | 
				
			||||||
    if let (Some(pkce_secret), Some(csrf_token)) =
 | 
					 | 
				
			||||||
        (cookies.get_private("verify"), cookies.get_private("csrf"))
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if state == csrf_token.value() {
 | 
					 | 
				
			||||||
            let token_result = oauth2_client
 | 
					 | 
				
			||||||
                .exchange_code(AuthorizationCode::new(code.to_string()))
 | 
					 | 
				
			||||||
                // Set the PKCE code verifier.
 | 
					 | 
				
			||||||
                .set_pkce_verifier(PkceCodeVerifier::new(pkce_secret.value().to_string()))
 | 
					 | 
				
			||||||
                .request_async(async_http_client)
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            cookies.remove_private(Cookie::named("verify"));
 | 
					 | 
				
			||||||
            cookies.remove_private(Cookie::named("csrf"));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            match token_result {
 | 
					 | 
				
			||||||
                Ok(token) => {
 | 
					 | 
				
			||||||
                    cookies.add_private(
 | 
					 | 
				
			||||||
                        Cookie::build("access_token", token.access_token().secret().to_string())
 | 
					 | 
				
			||||||
                            .secure(true)
 | 
					 | 
				
			||||||
                            .http_only(true)
 | 
					 | 
				
			||||||
                            .path("/dashboard")
 | 
					 | 
				
			||||||
                            .finish(),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let request_res = reqwest_client
 | 
					 | 
				
			||||||
                        .get(format!("{}/users/@me", DISCORD_API))
 | 
					 | 
				
			||||||
                        .bearer_auth(token.access_token().secret())
 | 
					 | 
				
			||||||
                        .send()
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    match request_res {
 | 
					 | 
				
			||||||
                        Ok(response) => {
 | 
					 | 
				
			||||||
                            let user_res = response.json::<User>().await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            match user_res {
 | 
					 | 
				
			||||||
                                Ok(user) => {
 | 
					 | 
				
			||||||
                                    let user_name = format!("{}#{}", user.name, user.discriminator);
 | 
					 | 
				
			||||||
                                    let user_id = user.id.as_u64().to_string();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    cookies.add_private(Cookie::new("username", user_name));
 | 
					 | 
				
			||||||
                                    cookies.add_private(Cookie::new("userid", user_id));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    Ok(Redirect::to(uri!(super::return_to_same_site("dashboard"))))
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                Err(e) => {
 | 
					 | 
				
			||||||
                                    warn!("Error constructing user from request: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    Err(Flash::new(
 | 
					 | 
				
			||||||
                                        Redirect::to(uri!(super::return_to_same_site(""))),
 | 
					 | 
				
			||||||
                                        "danger",
 | 
					 | 
				
			||||||
                                        "Failed to contact Discord",
 | 
					 | 
				
			||||||
                                    ))
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        Err(e) => {
 | 
					 | 
				
			||||||
                            warn!("Error getting user info: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Err(Flash::new(
 | 
					 | 
				
			||||||
                                Redirect::to(uri!(super::return_to_same_site(""))),
 | 
					 | 
				
			||||||
                                "danger",
 | 
					 | 
				
			||||||
                                "Failed to contact Discord",
 | 
					 | 
				
			||||||
                            ))
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Err(e) => {
 | 
					 | 
				
			||||||
                    warn!("Error in discord callback: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Err(Flash::new(
 | 
					 | 
				
			||||||
                        Redirect::to(uri!(super::return_to_same_site(""))),
 | 
					 | 
				
			||||||
                        "warning",
 | 
					 | 
				
			||||||
                        "Your login request was rejected",
 | 
					 | 
				
			||||||
                    ))
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } 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)"))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } 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)"))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,106 +0,0 @@
 | 
				
			|||||||
pub mod dashboard;
 | 
					 | 
				
			||||||
pub mod login;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::collections::HashMap;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use rocket::request::FlashMessage;
 | 
					 | 
				
			||||||
use rocket_dyn_templates::Template;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/")]
 | 
					 | 
				
			||||||
pub async fn index(flash: Option<FlashMessage<'_>>) -> Template {
 | 
					 | 
				
			||||||
    let mut map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if let Some(message) = flash {
 | 
					 | 
				
			||||||
        map.insert("flashed_message", message.message().to_string());
 | 
					 | 
				
			||||||
        map.insert("flashed_grade", message.kind().to_string());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Template::render("index", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/ret?<to>")]
 | 
					 | 
				
			||||||
pub async fn return_to_same_site(to: &str) -> Template {
 | 
					 | 
				
			||||||
    let mut map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    map.insert("to", to.to_string());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Template::render("return", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/cookies")]
 | 
					 | 
				
			||||||
pub async fn cookies() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("cookies", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/privacy")]
 | 
					 | 
				
			||||||
pub async fn privacy() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("privacy", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/terms")]
 | 
					 | 
				
			||||||
pub async fn terms() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("terms", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/")]
 | 
					 | 
				
			||||||
pub async fn help() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("help", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/timezone")]
 | 
					 | 
				
			||||||
pub async fn help_timezone() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("support/timezone", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/create_reminder")]
 | 
					 | 
				
			||||||
pub async fn help_create_reminder() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("support/create_reminder", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/delete_reminder")]
 | 
					 | 
				
			||||||
pub async fn help_delete_reminder() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("support/delete_reminder", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/timers")]
 | 
					 | 
				
			||||||
pub async fn help_timers() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("support/timers", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/todo_lists")]
 | 
					 | 
				
			||||||
pub async fn help_todo_lists() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("support/todo_lists", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/macros")]
 | 
					 | 
				
			||||||
pub async fn help_macros() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("support/macros", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/intervals")]
 | 
					 | 
				
			||||||
pub async fn help_intervals() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("support/intervals", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/dashboard")]
 | 
					 | 
				
			||||||
pub async fn help_dashboard() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("support/dashboard", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/iemanager")]
 | 
					 | 
				
			||||||
pub async fn help_iemanager() -> Template {
 | 
					 | 
				
			||||||
    let map: HashMap<&str, String> = HashMap::new();
 | 
					 | 
				
			||||||
    Template::render("support/iemanager", &map)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										1
									
								
								web/static/css/bulma.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,91 +0,0 @@
 | 
				
			|||||||
.date-selector-wrapper {
 | 
					 | 
				
			||||||
    width: 200px;
 | 
					 | 
				
			||||||
    padding: 3px;
 | 
					 | 
				
			||||||
    background-color: #fff;
 | 
					 | 
				
			||||||
    box-shadow: 1px 1px 10px 1px #5c5c5c;
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    font-size: 12px;
 | 
					 | 
				
			||||||
    -webkit-user-select: none;
 | 
					 | 
				
			||||||
    -khtml-user-select: none;
 | 
					 | 
				
			||||||
    -moz-user-select: none;
 | 
					 | 
				
			||||||
    -ms-user-select: none;
 | 
					 | 
				
			||||||
    -o-user-select: none;
 | 
					 | 
				
			||||||
    /* user-select: none; */
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-header, .cal-row {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    height: 30px;
 | 
					 | 
				
			||||||
    line-height: 30px;
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-cell, .cal-nav {
 | 
					 | 
				
			||||||
    cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-day-names {
 | 
					 | 
				
			||||||
    height: 25px;
 | 
					 | 
				
			||||||
    line-height: 25px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-day-names .cal-cell {
 | 
					 | 
				
			||||||
    cursor: default;
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-cell-prev, .cal-cell-next {
 | 
					 | 
				
			||||||
    color: #777;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-months .cal-row, .cal-years .cal-row {
 | 
					 | 
				
			||||||
    height: 60px;
 | 
					 | 
				
			||||||
    line-height: 60px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-nav-prev, .cal-nav-next {
 | 
					 | 
				
			||||||
    flex: 0.15;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-nav-current {
 | 
					 | 
				
			||||||
    flex: 0.75;
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-months .cal-cell, .cal-years .cal-cell {
 | 
					 | 
				
			||||||
    flex: 0.25;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-days .cal-cell {
 | 
					 | 
				
			||||||
    flex: 0.143;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-value {
 | 
					 | 
				
			||||||
    color: #fff;
 | 
					 | 
				
			||||||
    background-color: #286090;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-cell:hover, .cal-nav:hover {
 | 
					 | 
				
			||||||
    background-color: #eee;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-value:hover {
 | 
					 | 
				
			||||||
    background-color: #204d74;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* time footer */
 | 
					 | 
				
			||||||
.cal-time {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: flex-start;
 | 
					 | 
				
			||||||
    height: 27px;
 | 
					 | 
				
			||||||
    line-height: 27px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-time-label, .cal-time-value {
 | 
					 | 
				
			||||||
    flex: 0.12;
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-time-slider {
 | 
					 | 
				
			||||||
    flex: 0.77;
 | 
					 | 
				
			||||||
    background-image: linear-gradient(to right, #d1d8dd, #d1d8dd);
 | 
					 | 
				
			||||||
    background-repeat: no-repeat;
 | 
					 | 
				
			||||||
    background-size: 100% 1px;
 | 
					 | 
				
			||||||
    background-position: left 50%;
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.cal-time-slider input {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    -webkit-appearance: none;
 | 
					 | 
				
			||||||
    background: 0 0;
 | 
					 | 
				
			||||||
    cursor: pointer;
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
    outline: 0;
 | 
					 | 
				
			||||||
    user-select: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										12749
									
								
								web/static/css/fa.css
									
									
									
									
									
								
							
							
						
						@@ -1,63 +0,0 @@
 | 
				
			|||||||
@font-face {
 | 
					 | 
				
			||||||
  font-family: 'Source Sans Pro';
 | 
					 | 
				
			||||||
  font-style: italic;
 | 
					 | 
				
			||||||
  font-weight: 300;
 | 
					 | 
				
			||||||
  src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkids18E.ttf) format('truetype');
 | 
					 | 
				
			||||||
  font-display: swap;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@font-face {
 | 
					 | 
				
			||||||
  font-family: 'Source Sans Pro';
 | 
					 | 
				
			||||||
  font-style: italic;
 | 
					 | 
				
			||||||
  font-weight: 400;
 | 
					 | 
				
			||||||
  src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDc.ttf) format('truetype');
 | 
					 | 
				
			||||||
  font-display: swap;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@font-face {
 | 
					 | 
				
			||||||
  font-family: 'Source Sans Pro';
 | 
					 | 
				
			||||||
  font-style: italic;
 | 
					 | 
				
			||||||
  font-weight: 600;
 | 
					 | 
				
			||||||
  src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCds18E.ttf) format('truetype');
 | 
					 | 
				
			||||||
  font-display: swap;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@font-face {
 | 
					 | 
				
			||||||
  font-family: 'Source Sans Pro';
 | 
					 | 
				
			||||||
  font-style: normal;
 | 
					 | 
				
			||||||
  font-weight: 300;
 | 
					 | 
				
			||||||
  src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdr.ttf) format('truetype');
 | 
					 | 
				
			||||||
  font-display: swap;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@font-face {
 | 
					 | 
				
			||||||
  font-family: 'Source Sans Pro';
 | 
					 | 
				
			||||||
  font-style: normal;
 | 
					 | 
				
			||||||
  font-weight: 400;
 | 
					 | 
				
			||||||
  src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7g.ttf) format('truetype');
 | 
					 | 
				
			||||||
  font-display: swap;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@font-face {
 | 
					 | 
				
			||||||
  font-family: 'Source Sans Pro';
 | 
					 | 
				
			||||||
  font-style: normal;
 | 
					 | 
				
			||||||
  font-weight: 600;
 | 
					 | 
				
			||||||
  src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdr.ttf) format('truetype');
 | 
					 | 
				
			||||||
  font-display: swap;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@font-face {
 | 
					 | 
				
			||||||
  font-family: 'Source Sans Pro';
 | 
					 | 
				
			||||||
  font-style: normal;
 | 
					 | 
				
			||||||
  font-weight: 700;
 | 
					 | 
				
			||||||
  src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdr.ttf) format('truetype');
 | 
					 | 
				
			||||||
  font-display: swap;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@font-face {
 | 
					 | 
				
			||||||
  font-family: 'Ubuntu';
 | 
					 | 
				
			||||||
  font-style: normal;
 | 
					 | 
				
			||||||
  font-weight: 400;
 | 
					 | 
				
			||||||
  src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgo6eA.ttf) format('truetype');
 | 
					 | 
				
			||||||
  font-display: swap;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@font-face {
 | 
					 | 
				
			||||||
  font-family: 'Ubuntu';
 | 
					 | 
				
			||||||
  font-style: normal;
 | 
					 | 
				
			||||||
  font-weight: 700;
 | 
					 | 
				
			||||||
  src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvTtw.ttf) format('truetype');
 | 
					 | 
				
			||||||
  font-display: swap;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,582 +0,0 @@
 | 
				
			|||||||
* {
 | 
					 | 
				
			||||||
    font-family: "Ubuntu Bold", "Ubuntu", sans-serif;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
button {
 | 
					 | 
				
			||||||
    font-weight: 700;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* override styles for when the div is collapsed */
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed .column.discord-frame {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed .collapses {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed .invert-collapses {
 | 
					 | 
				
			||||||
    display: inline-flex;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent .invert-collapses {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed .settings {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: row;
 | 
					 | 
				
			||||||
    padding-bottom: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed .channel-field {
 | 
					 | 
				
			||||||
    display: inline-flex;
 | 
					 | 
				
			||||||
    order: 1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed .reminder-topbar {
 | 
					 | 
				
			||||||
    display: inline-flex;
 | 
					 | 
				
			||||||
    margin-bottom: 0px;
 | 
					 | 
				
			||||||
    flex-grow: 1;
 | 
					 | 
				
			||||||
    order: 2;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed input[name="name"] {
 | 
					 | 
				
			||||||
    display: inline-flex;
 | 
					 | 
				
			||||||
    flex-grow: 1;
 | 
					 | 
				
			||||||
    border: none;
 | 
					 | 
				
			||||||
    font-weight: 700;
 | 
					 | 
				
			||||||
    background: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed button.hide-box {
 | 
					 | 
				
			||||||
    display: inline-flex;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed button.hide-box i {
 | 
					 | 
				
			||||||
    transform: rotate(90deg);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
/* END */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* dashboard styles */
 | 
					 | 
				
			||||||
button.inline-btn {
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
    padding: 5px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
button.change-color {
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    left: calc(-1rem - 40px);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
button.disable-enable[data-action="enable"]:after {
 | 
					 | 
				
			||||||
    content: "Enable";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
button.disable-enable[data-action="disable"]:after {
 | 
					 | 
				
			||||||
    content: "Disable";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.media-content {
 | 
					 | 
				
			||||||
    overflow-x: visible;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.discord-embed {
 | 
					 | 
				
			||||||
    position: relative;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent {
 | 
					 | 
				
			||||||
    padding: 2px;
 | 
					 | 
				
			||||||
    background-color: #f5f5f5;
 | 
					 | 
				
			||||||
    border-radius: 8px;
 | 
					 | 
				
			||||||
    margin: 8px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.interval-group > button {
 | 
					 | 
				
			||||||
    margin-left: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Interval inputs */
 | 
					 | 
				
			||||||
div.interval-group > .interval-group-left input {
 | 
					 | 
				
			||||||
    -webkit-appearance: none;
 | 
					 | 
				
			||||||
    border-style: none;
 | 
					 | 
				
			||||||
    background-color: #eee;
 | 
					 | 
				
			||||||
    font-size: 1rem;
 | 
					 | 
				
			||||||
    font-family: monospace;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.interval-group > .interval-group-left input.w2 {
 | 
					 | 
				
			||||||
    width: 3ch;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.interval-group > .interval-group-left input.w3 {
 | 
					 | 
				
			||||||
    width: 6ch;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.interval-group {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: row;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
/* !Interval inputs */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.left-pad {
 | 
					 | 
				
			||||||
    padding-left: 1rem;
 | 
					 | 
				
			||||||
    padding-right: 0.2rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.notification {
 | 
					 | 
				
			||||||
    padding-right: 1.5rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.inset-content {
 | 
					 | 
				
			||||||
    margin-left: 10%;
 | 
					 | 
				
			||||||
    margin-right: 10%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.flash-message {
 | 
					 | 
				
			||||||
    position: fixed;
 | 
					 | 
				
			||||||
    width: calc(100% - 32px);
 | 
					 | 
				
			||||||
    margin: 16px !important;
 | 
					 | 
				
			||||||
    z-index: 99;
 | 
					 | 
				
			||||||
    bottom: 0;
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.flash-message.is-active {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
body {
 | 
					 | 
				
			||||||
    min-height: 100vh;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
span.spacer {
 | 
					 | 
				
			||||||
    width: 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
nav .dashboard-button {
 | 
					 | 
				
			||||||
    background: white ;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
span.patreon-color {
 | 
					 | 
				
			||||||
    color: #f96854;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
p.pageTitle {
 | 
					 | 
				
			||||||
    margin-left: 12px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#welcome > div {
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
    padding-top: 30vh;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div#pageNavbar {
 | 
					 | 
				
			||||||
    background-color: #363636;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div#pageNavbar a {
 | 
					 | 
				
			||||||
    color: #fff;
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div#pageNavbar a:hover {
 | 
					 | 
				
			||||||
    background-color: #4a4a4a;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
img.rounded-corners {
 | 
					 | 
				
			||||||
    border-radius: 12px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.brand {
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
    height: 52px;
 | 
					 | 
				
			||||||
    background-color: #8fb677;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
img.dashboard-brand {
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
    width: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.dashboard-sidebar {
 | 
					 | 
				
			||||||
    background-color: #363636;
 | 
					 | 
				
			||||||
    width: 230px !important;
 | 
					 | 
				
			||||||
    padding-right: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.dashboard-sidebar:not(.mobile-sidebar) {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
 | 
					 | 
				
			||||||
    position: fixed;
 | 
					 | 
				
			||||||
    bottom: 0;
 | 
					 | 
				
			||||||
    width: 226px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.mobile-sidebar {
 | 
					 | 
				
			||||||
    z-index: 100;
 | 
					 | 
				
			||||||
    min-height: 100vh;
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    top: 0;
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#expandAll {
 | 
					 | 
				
			||||||
    width: 60px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.mobile-sidebar .aside-footer {
 | 
					 | 
				
			||||||
    margin-top: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.mobile-sidebar.is-active {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
aside.menu {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    flex-grow: 1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.dashboard-frame {
 | 
					 | 
				
			||||||
    min-height: 100vh;
 | 
					 | 
				
			||||||
    margin-bottom: 0 !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-field-box[data-inlined="0"] .inline-btn > i {
 | 
					 | 
				
			||||||
    transform: rotate(90deg);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-field-box[data-inlined="0"] {
 | 
					 | 
				
			||||||
    min-width: 100%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-field-box[data-inlined="1"] {
 | 
					 | 
				
			||||||
    min-width: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.menu a {
 | 
					 | 
				
			||||||
    color: #fff;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.menu .menu-label {
 | 
					 | 
				
			||||||
    color: #bbb;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.menu {
 | 
					 | 
				
			||||||
    padding-left: 4px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dashboard-navbar {
 | 
					 | 
				
			||||||
    background-color: #8fb677 !important;
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    top: 0;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
textarea.autoresize {
 | 
					 | 
				
			||||||
    resize: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
textarea, input {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
input.default-width {
 | 
					 | 
				
			||||||
    width: initial;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.message-input:placeholder-shown {
 | 
					 | 
				
			||||||
    border-top: none;
 | 
					 | 
				
			||||||
    border-left: none;
 | 
					 | 
				
			||||||
    border-right: none;
 | 
					 | 
				
			||||||
    border-bottom-style: dashed;
 | 
					 | 
				
			||||||
    background-color: #40444b;
 | 
					 | 
				
			||||||
    color: #fff;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.message-input {
 | 
					 | 
				
			||||||
    border: none;
 | 
					 | 
				
			||||||
    background-color: rgba(0, 0, 0, 0);
 | 
					 | 
				
			||||||
    color: #fff;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.time-input {
 | 
					 | 
				
			||||||
    border-top: none;
 | 
					 | 
				
			||||||
    border-left: none;
 | 
					 | 
				
			||||||
    border-right: none;
 | 
					 | 
				
			||||||
    border-bottom-style: solid;
 | 
					 | 
				
			||||||
    background-color: #40444b;
 | 
					 | 
				
			||||||
    color: #fff;
 | 
					 | 
				
			||||||
    width: 120px;
 | 
					 | 
				
			||||||
    font-size: 0.875rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.message-input::placeholder {
 | 
					 | 
				
			||||||
    color: #72767b;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-title {
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
    font-size: 1rem;
 | 
					 | 
				
			||||||
    margin: 4px 0 4px 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-description {
 | 
					 | 
				
			||||||
    font-size: 0.875rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-username {
 | 
					 | 
				
			||||||
    font-size: 1rem;
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
    margin-bottom: 4px;
 | 
					 | 
				
			||||||
    width: initial;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-message-header {
 | 
					 | 
				
			||||||
    white-space: nowrap;
 | 
					 | 
				
			||||||
    margin-bottom: 8px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-content {
 | 
					 | 
				
			||||||
    margin-bottom: 8px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.customizable img {
 | 
					 | 
				
			||||||
    background-color: #72767b;
 | 
					 | 
				
			||||||
    border-radius: 8px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.customizable.is-20x20 img {
 | 
					 | 
				
			||||||
    width: 20px;
 | 
					 | 
				
			||||||
    height: 20px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.customizable.is-24x24 img {
 | 
					 | 
				
			||||||
    width: 24px;
 | 
					 | 
				
			||||||
    height: 24px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.customizable.is-400x300 img {
 | 
					 | 
				
			||||||
    margin-top: 10px;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    min-height: 100px;
 | 
					 | 
				
			||||||
    max-height: 400px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.customizable.is-32x32 img {
 | 
					 | 
				
			||||||
    width: 32px;
 | 
					 | 
				
			||||||
    height: 32px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.customizable.thumbnail img {
 | 
					 | 
				
			||||||
    width: 100px;
 | 
					 | 
				
			||||||
    height: 100px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.customizable input.imageInput {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    top: 0;
 | 
					 | 
				
			||||||
    left: 36px;
 | 
					 | 
				
			||||||
    width: 400px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.customizable.thumbnail input.imageInput {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    top: 0;
 | 
					 | 
				
			||||||
    left: -400px;
 | 
					 | 
				
			||||||
    width: 400px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.customizable input.is-active {
 | 
					 | 
				
			||||||
    display: block !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-frame {
 | 
					 | 
				
			||||||
    color: #fff;
 | 
					 | 
				
			||||||
    padding: 10px;
 | 
					 | 
				
			||||||
    border-radius: 8px;
 | 
					 | 
				
			||||||
    background-color: #36393f;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-embed {
 | 
					 | 
				
			||||||
    padding: 8px 16px 16px 12px;
 | 
					 | 
				
			||||||
    margin: 0 20px 4px 0;
 | 
					 | 
				
			||||||
    border-radius: 4px;
 | 
					 | 
				
			||||||
    border-left: 4px solid #fff;
 | 
					 | 
				
			||||||
    background-color: #2f3136;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-author-box {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    margin-bottom: 8px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-author-box > .a {
 | 
					 | 
				
			||||||
    flex: initial;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-author-box > .b {
 | 
					 | 
				
			||||||
    flex: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-footer-box {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    margin-bottom: 8px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-author-box .image {
 | 
					 | 
				
			||||||
    margin: 0 8px 0 0 !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-footer-box .image {
 | 
					 | 
				
			||||||
    margin: 0 8px 0 0 !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-embed-author {
 | 
					 | 
				
			||||||
    display: inline-block;
 | 
					 | 
				
			||||||
    font-size: 0.875rem;
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-embed-footer {
 | 
					 | 
				
			||||||
    font-size: 0.75rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-body {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-body > .a {
 | 
					 | 
				
			||||||
    flex-grow: 1;
 | 
					 | 
				
			||||||
    flex-shrink: 1;
 | 
					 | 
				
			||||||
    flex-basis: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-body input, .embed-body textarea {
 | 
					 | 
				
			||||||
    min-width: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-body > .b {
 | 
					 | 
				
			||||||
    flex-grow: 0;
 | 
					 | 
				
			||||||
    flex-shrink: 0;
 | 
					 | 
				
			||||||
    flex-basis: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-field-title, .discord-field-value {
 | 
					 | 
				
			||||||
    max-width: 120px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.discord-field-title {
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-field-box {
 | 
					 | 
				
			||||||
    margin: 12px 8px 0 0;
 | 
					 | 
				
			||||||
    max-width: 120px;
 | 
					 | 
				
			||||||
    flex: initial;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.field-input {
 | 
					 | 
				
			||||||
    font-size: 0.875rem;
 | 
					 | 
				
			||||||
    width: 120px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed-multifield-box {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    max-width: 100%;
 | 
					 | 
				
			||||||
    flex-wrap: wrap;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.channel-select {
 | 
					 | 
				
			||||||
    font-size: 1.125rem;
 | 
					 | 
				
			||||||
    margin-bottom: 4px;
 | 
					 | 
				
			||||||
    margin-left: 48px;
 | 
					 | 
				
			||||||
    display: inline-flex;
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
    color: #6e89da;
 | 
					 | 
				
			||||||
    width: auto;
 | 
					 | 
				
			||||||
    border-radius: 2px;
 | 
					 | 
				
			||||||
    border-bottom: 1px solid #fff;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@media only screen and (max-width: 768px) {
 | 
					 | 
				
			||||||
    .customizable.thumbnail img {
 | 
					 | 
				
			||||||
        width: 60px;
 | 
					 | 
				
			||||||
        height: 60px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .customizable.is-24x24 img {
 | 
					 | 
				
			||||||
        width: 16px;
 | 
					 | 
				
			||||||
        height: 16px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* loader */
 | 
					 | 
				
			||||||
#loader {
 | 
					 | 
				
			||||||
    position: fixed;
 | 
					 | 
				
			||||||
    background-color: rgba(255, 255, 255, 0.8);
 | 
					 | 
				
			||||||
    width: 100vw;
 | 
					 | 
				
			||||||
    z-index: 999;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#loader .title {
 | 
					 | 
				
			||||||
    font-size: 6rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* END */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* other stuff */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.half-rem {
 | 
					 | 
				
			||||||
    width: 0.5rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pad-left {
 | 
					 | 
				
			||||||
    width: 12px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#dead {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.colorpicker-container {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.create-reminder {
 | 
					 | 
				
			||||||
    margin: 0 12px 12px 12px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.button.is-success:not(.is-outlined) {
 | 
					 | 
				
			||||||
    color: white;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.button.is-outlined.is-success {
 | 
					 | 
				
			||||||
    background-color: white;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.is-locked {
 | 
					 | 
				
			||||||
    pointer-events: none;
 | 
					 | 
				
			||||||
    opacity: 0.4;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.is-locked .foreground {
 | 
					 | 
				
			||||||
    pointer-events: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.is-locked .field:last-of-type {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 7.2 KiB  | 
| 
		 Before Width: | Height: | Size: 20 KiB  | 
| 
		 Before Width: | Height: | Size: 6.8 KiB  | 
@@ -1,9 +0,0 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="utf-8"?>
 | 
					 | 
				
			||||||
<browserconfig>
 | 
					 | 
				
			||||||
    <msapplication>
 | 
					 | 
				
			||||||
        <tile>
 | 
					 | 
				
			||||||
            <square150x150logo src="/mstile-150x150.png"/>
 | 
					 | 
				
			||||||
            <TileColor>#da532c</TileColor>
 | 
					 | 
				
			||||||
        </tile>
 | 
					 | 
				
			||||||
    </msapplication>
 | 
					 | 
				
			||||||
</browserconfig>
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 1.8 KiB  | 
| 
		 Before Width: | Height: | Size: 2.3 KiB  | 
| 
		 Before Width: | Height: | Size: 7.2 KiB  | 
| 
		 Before Width: | Height: | Size: 4.9 KiB  | 
@@ -1,19 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "name": "",
 | 
					 | 
				
			||||||
    "short_name": "",
 | 
					 | 
				
			||||||
    "icons": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            "src": "/android-chrome-192x192.png",
 | 
					 | 
				
			||||||
            "sizes": "192x192",
 | 
					 | 
				
			||||||
            "type": "image/png"
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            "src": "/android-chrome-512x512.png",
 | 
					 | 
				
			||||||
            "sizes": "512x512",
 | 
					 | 
				
			||||||
            "type": "image/png"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    "theme_color": "#ffffff",
 | 
					 | 
				
			||||||
    "background_color": "#ffffff",
 | 
					 | 
				
			||||||
    "display": "standalone"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 762 B  | 
| 
		 Before Width: | Height: | Size: 11 KiB  | 
| 
		 Before Width: | Height: | Size: 323 KiB  | 
| 
		 Before Width: | Height: | Size: 61 KiB  | 
| 
		 Before Width: | Height: | Size: 23 KiB  | 
| 
		 Before Width: | Height: | Size: 35 KiB  | 
| 
		 Before Width: | Height: | Size: 55 KiB  | 
| 
		 Before Width: | Height: | Size: 3.6 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB  | 
| 
		 Before Width: | Height: | Size: 14 KiB  | 
| 
		 Before Width: | Height: | Size: 24 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB  | 
| 
		 Before Width: | Height: | Size: 65 KiB  |