Compare commits
	
		
			1 Commits
		
	
	
		
			jellywx/gu
			...
			postgres
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2d1668a63a | 
							
								
								
									
										1092
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1092
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										19
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								Cargo.toml
									
									
									
									
									
								
							@@ -1,30 +1,29 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "reminder_rs"
 | 
					name = "reminder_rs"
 | 
				
			||||||
version = "1.6.6"
 | 
					version = "1.6.0"
 | 
				
			||||||
authors = ["jellywx <judesouthworth@pm.me>"]
 | 
					authors = ["jellywx <judesouthworth@pm.me>"]
 | 
				
			||||||
edition = "2018"
 | 
					edition = "2018"
 | 
				
			||||||
 | 
					workspaces = [".", "postman", "web", "entity", "migration"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
poise = "0.3"
 | 
					poise = "0.2"
 | 
				
			||||||
dotenv = "0.15"
 | 
					dotenv = "0.15"
 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
reqwest = "0.11"
 | 
					reqwest = "0.11"
 | 
				
			||||||
lazy-regex = "2.3.0"
 | 
					regex = "1.4"
 | 
				
			||||||
regex = "1.6"
 | 
					 | 
				
			||||||
log = "0.4"
 | 
					log = "0.4"
 | 
				
			||||||
env_logger = "0.9"
 | 
					env_logger = "0.8"
 | 
				
			||||||
chrono = "0.4"
 | 
					chrono = "0.4"
 | 
				
			||||||
chrono-tz = { version = "0.6", features = ["serde"] }
 | 
					chrono-tz = { version = "0.5", features = ["serde"] }
 | 
				
			||||||
lazy_static = "1.4"
 | 
					lazy_static = "1.4"
 | 
				
			||||||
num-integer = "0.1"
 | 
					num-integer = "0.1"
 | 
				
			||||||
serde = "1.0"
 | 
					serde = "1.0"
 | 
				
			||||||
serde_json = "1.0"
 | 
					serde_json = "1.0"
 | 
				
			||||||
serde_repr = "0.1"
 | 
					serde_repr = "0.1"
 | 
				
			||||||
rmp-serde = "1.1"
 | 
					rmp-serde = "0.15"
 | 
				
			||||||
rand = "0.8"
 | 
					rand = "0.7"
 | 
				
			||||||
levenshtein = "1.0"
 | 
					levenshtein = "1.0"
 | 
				
			||||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]}
 | 
					base64 = "0.13.0"
 | 
				
			||||||
base64 = "0.13"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.postman]
 | 
					[dependencies.postman]
 | 
				
			||||||
path = "postman"
 | 
					path = "postman"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,233 +0,0 @@
 | 
				
			|||||||
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);
 | 
					 | 
				
			||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,13 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
@@ -1,4 +0,0 @@
 | 
				
			|||||||
USE reminders;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE reminders.reminders RENAME COLUMN `interval` TO `interval_seconds`;
 | 
					 | 
				
			||||||
ALTER TABLE reminders.reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL;
 | 
					 | 
				
			||||||
@@ -1,51 +0,0 @@
 | 
				
			|||||||
USE reminders;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CREATE TABLE reminder_template (
 | 
					 | 
				
			||||||
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    `name` VARCHAR(24) NOT NULL DEFAULT 'Reminder',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    `guild_id` INT UNSIGNED NOT NULL,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    `username` VARCHAR(32) DEFAULT NULL,
 | 
					 | 
				
			||||||
    `avatar` VARCHAR(512) DEFAULT NULL,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    `content` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
					 | 
				
			||||||
    `tts` BOOL NOT NULL DEFAULT 0,
 | 
					 | 
				
			||||||
    `attachment` MEDIUMBLOB,
 | 
					 | 
				
			||||||
    `attachment_name` VARCHAR(260),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    `embed_title` VARCHAR(256) NOT NULL DEFAULT '',
 | 
					 | 
				
			||||||
    `embed_description` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
					 | 
				
			||||||
    `embed_image_url` VARCHAR(512),
 | 
					 | 
				
			||||||
    `embed_thumbnail_url` VARCHAR(512),
 | 
					 | 
				
			||||||
    `embed_footer` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
					 | 
				
			||||||
    `embed_footer_url` VARCHAR(512),
 | 
					 | 
				
			||||||
    `embed_author` VARCHAR(256) NOT NULL DEFAULT '',
 | 
					 | 
				
			||||||
    `embed_author_url` VARCHAR(512),
 | 
					 | 
				
			||||||
    `embed_color` INT UNSIGNED NOT NULL DEFAULT 0x0,
 | 
					 | 
				
			||||||
    `embed_fields` JSON,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    FOREIGN KEY (`guild_id`) REFERENCES guilds (`id`) ON DELETE CASCADE
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE reminders ADD COLUMN embed_fields JSON;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
update reminders
 | 
					 | 
				
			||||||
    inner join embed_fields as E
 | 
					 | 
				
			||||||
    on E.reminder_id = reminders.id
 | 
					 | 
				
			||||||
set embed_fields = (
 | 
					 | 
				
			||||||
    select JSON_ARRAYAGG(
 | 
					 | 
				
			||||||
        JSON_OBJECT(
 | 
					 | 
				
			||||||
            'title', E.title,
 | 
					 | 
				
			||||||
            'value', E.value,
 | 
					 | 
				
			||||||
            'inline',
 | 
					 | 
				
			||||||
            if(inline = 1, cast(TRUE as json), cast(FALSE as json))
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    from embed_fields
 | 
					 | 
				
			||||||
    group by reminder_id
 | 
					 | 
				
			||||||
    having reminder_id = reminders.id
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
@@ -1,92 +0,0 @@
 | 
				
			|||||||
SET foreign_key_checks = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
START TRANSACTION;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- drop existing constraints
 | 
					 | 
				
			||||||
ALTER TABLE channels DROP FOREIGN KEY `channels_ibfk_1`;
 | 
					 | 
				
			||||||
ALTER TABLE command_aliases DROP FOREIGN KEY `command_aliases_ibfk_1`;
 | 
					 | 
				
			||||||
ALTER TABLE events DROP FOREIGN KEY `events_ibfk_1`;
 | 
					 | 
				
			||||||
ALTER TABLE guild_users DROP FOREIGN KEY `guild_users_ibfk_1`;
 | 
					 | 
				
			||||||
ALTER TABLE macro DROP FOREIGN KEY `macro_ibfk_1`;
 | 
					 | 
				
			||||||
ALTER TABLE roles DROP FOREIGN KEY `roles_ibfk_1`;
 | 
					 | 
				
			||||||
ALTER TABLE todos DROP FOREIGN KEY `todos_ibfk_2`;
 | 
					 | 
				
			||||||
ALTER TABLE reminder_template DROP FOREIGN KEY `reminder_template_ibfk_1`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- update foreign key types
 | 
					 | 
				
			||||||
ALTER TABLE channels MODIFY `guild_id` BIGINT UNSIGNED;
 | 
					 | 
				
			||||||
ALTER TABLE command_aliases MODIFY `guild_id` BIGINT UNSIGNED;
 | 
					 | 
				
			||||||
ALTER TABLE events MODIFY `guild_id` BIGINT UNSIGNED;
 | 
					 | 
				
			||||||
ALTER TABLE guild_users MODIFY `guild` BIGINT UNSIGNED;
 | 
					 | 
				
			||||||
ALTER TABLE macro MODIFY `guild_id` BIGINT UNSIGNED;
 | 
					 | 
				
			||||||
ALTER TABLE roles MODIFY `guild_id` BIGINT UNSIGNED;
 | 
					 | 
				
			||||||
ALTER TABLE todos MODIFY `guild_id` BIGINT UNSIGNED;
 | 
					 | 
				
			||||||
ALTER TABLE reminder_template MODIFY `guild_id` BIGINT UNSIGNED;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- update foreign key values
 | 
					 | 
				
			||||||
UPDATE channels SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
					 | 
				
			||||||
UPDATE command_aliases SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
					 | 
				
			||||||
UPDATE events SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
					 | 
				
			||||||
UPDATE guild_users SET `guild` = (SELECT `guild` FROM guilds WHERE guilds.`id` = guild_users.`guild`);
 | 
					 | 
				
			||||||
UPDATE macro SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
					 | 
				
			||||||
UPDATE roles SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
					 | 
				
			||||||
UPDATE todos SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
					 | 
				
			||||||
UPDATE reminder_template SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- update guilds table
 | 
					 | 
				
			||||||
ALTER TABLE guilds MODIFY `id` BIGINT UNSIGNED NOT NULL;
 | 
					 | 
				
			||||||
UPDATE guilds SET `id` = `guild`;
 | 
					 | 
				
			||||||
ALTER TABLE guilds DROP COLUMN `guild`;
 | 
					 | 
				
			||||||
ALTER TABLE guilds ADD COLUMN `default_channel` BIGINT UNSIGNED;
 | 
					 | 
				
			||||||
ALTER TABLE guilds ADD CONSTRAINT `default_channel_fk`
 | 
					 | 
				
			||||||
    FOREIGN KEY (`default_channel`)
 | 
					 | 
				
			||||||
        REFERENCES channels(`channel`)
 | 
					 | 
				
			||||||
        ON DELETE SET NULL
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- re-add constraints
 | 
					 | 
				
			||||||
ALTER TABLE channels ADD CONSTRAINT
 | 
					 | 
				
			||||||
    FOREIGN KEY (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES guilds(`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE command_aliases ADD CONSTRAINT
 | 
					 | 
				
			||||||
    FOREIGN KEY (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES guilds(`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE events ADD CONSTRAINT
 | 
					 | 
				
			||||||
    FOREIGN KEY (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES guilds(`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE guild_users ADD CONSTRAINT
 | 
					 | 
				
			||||||
    FOREIGN KEY (`guild`)
 | 
					 | 
				
			||||||
        REFERENCES guilds(`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE macro ADD CONSTRAINT
 | 
					 | 
				
			||||||
    FOREIGN KEY (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES guilds(`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE roles ADD CONSTRAINT
 | 
					 | 
				
			||||||
    FOREIGN KEY (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES guilds(`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE todos ADD CONSTRAINT
 | 
					 | 
				
			||||||
    FOREIGN KEY (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES guilds(`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE
 | 
					 | 
				
			||||||
        ON UPDATE CASCADE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
COMMIT;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SET foreign_key_checks = 1;
 | 
					 | 
				
			||||||
							
								
								
									
										7
									
								
								models/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								models/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					# This file is automatically @generated by Cargo.
 | 
				
			||||||
 | 
					# It is not intended for manual editing.
 | 
				
			||||||
 | 
					version = 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "models"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
							
								
								
									
										8
									
								
								models/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								models/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					[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]
 | 
				
			||||||
							
								
								
									
										8
									
								
								models/entity/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								models/entity/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					[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"] }
 | 
				
			||||||
							
								
								
									
										60
									
								
								models/entity/src/channel.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								models/entity/src/channel.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					//! 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 {}
 | 
				
			||||||
							
								
								
									
										34
									
								
								models/entity/src/command_macro.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								models/entity/src/command_macro.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					//! 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 {}
 | 
				
			||||||
							
								
								
									
										48
									
								
								models/entity/src/guild.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								models/entity/src/guild.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					//! 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
									
								
								models/entity/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								models/entity/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										14
									
								
								models/entity/src/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								models/entity/src/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					//! 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;
 | 
				
			||||||
							
								
								
									
										8
									
								
								models/entity/src/prelude.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								models/entity/src/prelude.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					//! 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,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										73
									
								
								models/entity/src/reminder.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								models/entity/src/reminder.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					//! 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 {}
 | 
				
			||||||
							
								
								
									
										48
									
								
								models/entity/src/reminder_template.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								models/entity/src/reminder_template.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					//! 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 {}
 | 
				
			||||||
							
								
								
									
										1196
									
								
								models/entity/src/sea_orm_active_enums.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1196
									
								
								models/entity/src/sea_orm_active_enums.rs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										22
									
								
								models/entity/src/seaql_migrations.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								models/entity/src/seaql_migrations.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					//! 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 {}
 | 
				
			||||||
							
								
								
									
										36
									
								
								models/entity/src/timer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								models/entity/src/timer.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					//! 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 {}
 | 
				
			||||||
							
								
								
									
										62
									
								
								models/entity/src/todo.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								models/entity/src/todo.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					//! 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 {}
 | 
				
			||||||
							
								
								
									
										50
									
								
								models/entity/src/user.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								models/entity/src/user.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					//! 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
									
									
									
										Normal file
									
								
							
							
						
						
									
										2400
									
								
								models/migration/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										16
									
								
								models/migration/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								models/migration/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					[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"
 | 
				
			||||||
							
								
								
									
										37
									
								
								models/migration/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								models/migration/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					# 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
 | 
				
			||||||
 | 
					    ```
 | 
				
			||||||
							
								
								
									
										12
									
								
								models/migration/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								models/migration/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					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)]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										553
									
								
								models/migration/src/m20220101_000001_create_table.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										553
									
								
								models/migration/src/m20220101_000001_create_table.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,553 @@
 | 
				
			|||||||
 | 
					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(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								models/migration/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								models/migration/src/main.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					use sea_orm_migration::prelude::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_std::main]
 | 
				
			||||||
 | 
					async fn main() {
 | 
				
			||||||
 | 
					    cli::run_cli(migration::Migrator).await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								models/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								models/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@@ -7,10 +7,12 @@ edition = "2021"
 | 
				
			|||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
regex = "1.4"
 | 
					regex = "1.4"
 | 
				
			||||||
log = "0.4"
 | 
					log = "0.4"
 | 
				
			||||||
 | 
					env_logger = "0.8"
 | 
				
			||||||
chrono = "0.4"
 | 
					chrono = "0.4"
 | 
				
			||||||
chrono-tz = { version = "0.5", features = ["serde"] }
 | 
					chrono-tz = { version = "0.5", features = ["serde"] }
 | 
				
			||||||
lazy_static = "1.4"
 | 
					lazy_static = "1.4"
 | 
				
			||||||
num-integer = "0.1"
 | 
					num-integer = "0.1"
 | 
				
			||||||
serde = "1.0"
 | 
					serde = "1.0"
 | 
				
			||||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
 | 
					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"] }
 | 
					serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -226,6 +226,7 @@ impl Into<CreateEmbed> for Embed {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
pub struct Reminder {
 | 
					pub struct Reminder {
 | 
				
			||||||
    id: u32,
 | 
					    id: u32,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -292,20 +293,8 @@ INNER JOIN
 | 
				
			|||||||
ON
 | 
					ON
 | 
				
			||||||
    reminders.channel_id = channels.id
 | 
					    reminders.channel_id = channels.id
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
    reminders.`id` IN (
 | 
					    reminders.`utc_time` < NOW()
 | 
				
			||||||
        SELECT
 | 
					LIMIT 25
 | 
				
			||||||
            MIN(id)
 | 
					 | 
				
			||||||
        FROM
 | 
					 | 
				
			||||||
            reminders
 | 
					 | 
				
			||||||
        WHERE
 | 
					 | 
				
			||||||
            reminders.`utc_time` <= NOW()
 | 
					 | 
				
			||||||
            AND (
 | 
					 | 
				
			||||||
                reminders.`interval_seconds` IS NOT NULL
 | 
					 | 
				
			||||||
                OR reminders.`interval_months` IS NOT NULL
 | 
					 | 
				
			||||||
                OR reminders.enabled
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        GROUP BY channel_id
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
            "#,
 | 
					            "#,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .fetch_all(pool)
 | 
					        .fetch_all(pool)
 | 
				
			||||||
@@ -577,7 +566,7 @@ UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if let Err(e) = result {
 | 
					            if let Err(e) = result {
 | 
				
			||||||
                error!("Error sending reminder {}: {:?}", self.id, e);
 | 
					                error!("Error sending {:?}: {:?}", self, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if let Error::Http(error) = e {
 | 
					                if let Error::Http(error) = e {
 | 
				
			||||||
                    if error.status_code() == Some(StatusCode::NOT_FOUND) {
 | 
					                    if error.status_code() == Some(StatusCode::NOT_FOUND) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,131 +0,0 @@
 | 
				
			|||||||
use std::time::{SystemTime, UNIX_EPOCH};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono_tz::TZ_VARIANTS;
 | 
					 | 
				
			||||||
use poise::AutocompleteChoice;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{models::CtxData, time_parser::natural_parser, Context};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
 | 
					 | 
				
			||||||
    if partial.is_empty() {
 | 
					 | 
				
			||||||
        ctx.data().popular_timezones.iter().map(|t| t.to_string()).collect::<Vec<String>>()
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        TZ_VARIANTS
 | 
					 | 
				
			||||||
            .iter()
 | 
					 | 
				
			||||||
            .filter(|tz| tz.to_string().contains(&partial))
 | 
					 | 
				
			||||||
            .take(25)
 | 
					 | 
				
			||||||
            .map(|t| t.to_string())
 | 
					 | 
				
			||||||
            .collect::<Vec<String>>()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn macro_name_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
 | 
					 | 
				
			||||||
    sqlx::query!(
 | 
					 | 
				
			||||||
        "
 | 
					 | 
				
			||||||
SELECT name
 | 
					 | 
				
			||||||
FROM macro
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    guild_id = ?
 | 
					 | 
				
			||||||
    AND name LIKE CONCAT(?, '%')",
 | 
					 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					 | 
				
			||||||
        partial,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_all(&ctx.data().database)
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    .unwrap_or_default()
 | 
					 | 
				
			||||||
    .iter()
 | 
					 | 
				
			||||||
    .map(|s| s.name.clone())
 | 
					 | 
				
			||||||
    .collect()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn multiline_autocomplete(
 | 
					 | 
				
			||||||
    _ctx: Context<'_>,
 | 
					 | 
				
			||||||
    partial: &str,
 | 
					 | 
				
			||||||
) -> Vec<AutocompleteChoice<String>> {
 | 
					 | 
				
			||||||
    if partial.is_empty() {
 | 
					 | 
				
			||||||
        vec![AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() }]
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        vec![
 | 
					 | 
				
			||||||
            AutocompleteChoice { name: partial.to_string(), value: partial.to_string() },
 | 
					 | 
				
			||||||
            AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() },
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn time_hint_autocomplete(
 | 
					 | 
				
			||||||
    ctx: Context<'_>,
 | 
					 | 
				
			||||||
    partial: &str,
 | 
					 | 
				
			||||||
) -> Vec<AutocompleteChoice<String>> {
 | 
					 | 
				
			||||||
    if partial.is_empty() {
 | 
					 | 
				
			||||||
        vec![AutocompleteChoice {
 | 
					 | 
				
			||||||
            name: "Start typing a time...".to_string(),
 | 
					 | 
				
			||||||
            value: "now".to_string(),
 | 
					 | 
				
			||||||
        }]
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        match natural_parser(partial, &ctx.timezone().await.to_string()).await {
 | 
					 | 
				
			||||||
            Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
 | 
					 | 
				
			||||||
                Ok(now) => {
 | 
					 | 
				
			||||||
                    let diff = timestamp - now.as_secs() as i64;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if diff < 0 {
 | 
					 | 
				
			||||||
                        vec![AutocompleteChoice {
 | 
					 | 
				
			||||||
                            name: "Time is in the past".to_string(),
 | 
					 | 
				
			||||||
                            value: "now".to_string(),
 | 
					 | 
				
			||||||
                        }]
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        if diff > 86400 {
 | 
					 | 
				
			||||||
                            vec![
 | 
					 | 
				
			||||||
                                AutocompleteChoice {
 | 
					 | 
				
			||||||
                                    name: partial.to_string(),
 | 
					 | 
				
			||||||
                                    value: partial.to_string(),
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                                AutocompleteChoice {
 | 
					 | 
				
			||||||
                                    name: format!(
 | 
					 | 
				
			||||||
                                        "In approximately {} days, {} hours",
 | 
					 | 
				
			||||||
                                        diff / 86400,
 | 
					 | 
				
			||||||
                                        (diff % 86400) / 3600
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                    value: partial.to_string(),
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                            ]
 | 
					 | 
				
			||||||
                        } else if diff > 3600 {
 | 
					 | 
				
			||||||
                            vec![
 | 
					 | 
				
			||||||
                                AutocompleteChoice {
 | 
					 | 
				
			||||||
                                    name: partial.to_string(),
 | 
					 | 
				
			||||||
                                    value: partial.to_string(),
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                                AutocompleteChoice {
 | 
					 | 
				
			||||||
                                    name: format!("In approximately {} hours", diff / 3600),
 | 
					 | 
				
			||||||
                                    value: partial.to_string(),
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                            ]
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            vec![
 | 
					 | 
				
			||||||
                                AutocompleteChoice {
 | 
					 | 
				
			||||||
                                    name: partial.to_string(),
 | 
					 | 
				
			||||||
                                    value: partial.to_string(),
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                                AutocompleteChoice {
 | 
					 | 
				
			||||||
                                    name: format!("In approximately {} minutes", diff / 60),
 | 
					 | 
				
			||||||
                                    value: partial.to_string(),
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                            ]
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Err(_) => {
 | 
					 | 
				
			||||||
                    vec![AutocompleteChoice {
 | 
					 | 
				
			||||||
                        name: partial.to_string(),
 | 
					 | 
				
			||||||
                        value: partial.to_string(),
 | 
					 | 
				
			||||||
                    }]
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            None => {
 | 
					 | 
				
			||||||
                vec![AutocompleteChoice {
 | 
					 | 
				
			||||||
                    name: "Time not recognised".to_string(),
 | 
					 | 
				
			||||||
                    value: "now".to_string(),
 | 
					 | 
				
			||||||
                }]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,46 +0,0 @@
 | 
				
			|||||||
use super::super::autocomplete::macro_name_autocomplete;
 | 
					 | 
				
			||||||
use crate::{Context, Error};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Delete a recorded macro
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "delete",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					 | 
				
			||||||
    identifying_name = "delete_macro"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn delete_macro(
 | 
					 | 
				
			||||||
    ctx: Context<'_>,
 | 
					 | 
				
			||||||
    #[description = "Name of macro to delete"]
 | 
					 | 
				
			||||||
    #[autocomplete = "macro_name_autocomplete"]
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    match sqlx::query!(
 | 
					 | 
				
			||||||
        "
 | 
					 | 
				
			||||||
SELECT id FROM macro WHERE guild_id = ? AND name = ?",
 | 
					 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					 | 
				
			||||||
        name
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_one(&ctx.data().database)
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Ok(row) => {
 | 
					 | 
				
			||||||
            sqlx::query!("DELETE FROM macro WHERE id = ?", row.id)
 | 
					 | 
				
			||||||
                .execute(&ctx.data().database)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            ctx.say(format!("Macro \"{}\" deleted", name)).await?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(sqlx::Error::RowNotFound) => {
 | 
					 | 
				
			||||||
            ctx.say(format!("Macro \"{}\" not found", name)).await?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            panic!("{}", e);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,89 +0,0 @@
 | 
				
			|||||||
use poise::CreateReply;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    component_models::pager::{MacroPager, Pager},
 | 
					 | 
				
			||||||
    consts::THEME_COLOR,
 | 
					 | 
				
			||||||
    models::{command_macro::CommandMacro, CtxData},
 | 
					 | 
				
			||||||
    Context, Error,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// List recorded macros
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    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> {
 | 
					 | 
				
			||||||
    let macros = ctx.command_macros().await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let resp = show_macro_page(¯os, 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ctx.send(|m| {
 | 
					 | 
				
			||||||
        *m = resp;
 | 
					 | 
				
			||||||
        m
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize {
 | 
					 | 
				
			||||||
    ((macros.len() as f64) / 25.0).ceil() as usize
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply {
 | 
					 | 
				
			||||||
    let pager = MacroPager::new(page);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if macros.is_empty() {
 | 
					 | 
				
			||||||
        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)
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return reply;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let pages = max_macro_page(macros);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut page = page;
 | 
					 | 
				
			||||||
    if page >= pages {
 | 
					 | 
				
			||||||
        page = pages - 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let lower = (page * 25).min(macros.len());
 | 
					 | 
				
			||||||
    let upper = ((page + 1) * 25).min(macros.len());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let fields = macros[lower..upper].iter().map(|m| {
 | 
					 | 
				
			||||||
        if let Some(description) = &m.description {
 | 
					 | 
				
			||||||
            (
 | 
					 | 
				
			||||||
                m.name.clone(),
 | 
					 | 
				
			||||||
                format!("*{}*\n- Has {} commands", description, m.commands.len()),
 | 
					 | 
				
			||||||
                true,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            (m.name.clone(), format!("- Has {} commands", m.commands.len()), true)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut reply = CreateReply::default();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reply
 | 
					 | 
				
			||||||
        .embed(|e| {
 | 
					 | 
				
			||||||
            e.title("Macros")
 | 
					 | 
				
			||||||
                .fields(fields)
 | 
					 | 
				
			||||||
                .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
					 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .components(|comp| {
 | 
					 | 
				
			||||||
            pager.create_button_row(pages, comp);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            comp
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reply
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,229 +0,0 @@
 | 
				
			|||||||
use lazy_regex::regex;
 | 
					 | 
				
			||||||
use poise::serenity_prelude::command::CommandOptionType;
 | 
					 | 
				
			||||||
use regex::Captures;
 | 
					 | 
				
			||||||
use serde_json::{json, Value};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{models::command_macro::RawCommandMacro, Context, Error, GuildId};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Alias {
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
    command: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Migrate old $alias reminder commands to macros. Only macro names that are not taken will be used.
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "migrate",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					 | 
				
			||||||
    identifying_name = "migrate_macro"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let guild_id = ctx.guild_id().unwrap();
 | 
					 | 
				
			||||||
    let mut transaction = ctx.data().database.begin().await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let aliases = sqlx::query_as!(
 | 
					 | 
				
			||||||
        Alias,
 | 
					 | 
				
			||||||
        "SELECT name, command FROM command_aliases WHERE guild_id = ?",
 | 
					 | 
				
			||||||
        guild_id.0
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_all(&mut transaction)
 | 
					 | 
				
			||||||
    .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut added_aliases = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for alias in aliases {
 | 
					 | 
				
			||||||
        match parse_text_command(guild_id, alias.name, &alias.command) {
 | 
					 | 
				
			||||||
            Some(cmd_macro) => {
 | 
					 | 
				
			||||||
                sqlx::query!(
 | 
					 | 
				
			||||||
                    "INSERT INTO macro (guild_id, name, description, commands) VALUES (?, ?, ?, ?)",
 | 
					 | 
				
			||||||
                    cmd_macro.guild_id.0,
 | 
					 | 
				
			||||||
                    cmd_macro.name,
 | 
					 | 
				
			||||||
                    cmd_macro.description,
 | 
					 | 
				
			||||||
                    cmd_macro.commands
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .execute(&mut transaction)
 | 
					 | 
				
			||||||
                .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                added_aliases += 1;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            None => {}
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    transaction.commit().await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ctx.send(|b| b.content(format!("Added {} macros.", added_aliases))).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn parse_text_command(
 | 
					 | 
				
			||||||
    guild_id: GuildId,
 | 
					 | 
				
			||||||
    alias_name: String,
 | 
					 | 
				
			||||||
    command: &str,
 | 
					 | 
				
			||||||
) -> Option<RawCommandMacro> {
 | 
					 | 
				
			||||||
    match command.split_once(" ") {
 | 
					 | 
				
			||||||
        Some((command_word, args)) => {
 | 
					 | 
				
			||||||
            let command_word = command_word.to_lowercase();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if command_word == "r"
 | 
					 | 
				
			||||||
                || command_word == "i"
 | 
					 | 
				
			||||||
                || command_word == "remind"
 | 
					 | 
				
			||||||
                || command_word == "interval"
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                let matcher = regex!(
 | 
					 | 
				
			||||||
                    r#"(?P<mentions>(?:<@\d+>\s+|<@!\d+>\s+|<#\d+>\s+)*)(?P<time>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+)(?:\s+(?P<interval>(?:(?:\d+)(?:s|m|h|d|))+))?(?:\s+(?P<expires>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+))?\s+(?P<content>.*)"#s
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                match matcher.captures(&args) {
 | 
					 | 
				
			||||||
                    Some(captures) => {
 | 
					 | 
				
			||||||
                        let mut args: Vec<Value> = vec![];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(group) = captures.name("time") {
 | 
					 | 
				
			||||||
                            let content = group.as_str();
 | 
					 | 
				
			||||||
                            args.push(json!({
 | 
					 | 
				
			||||||
                                "name": "time",
 | 
					 | 
				
			||||||
                                "value": content,
 | 
					 | 
				
			||||||
                                "type": CommandOptionType::String,
 | 
					 | 
				
			||||||
                            }));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(group) = captures.name("content") {
 | 
					 | 
				
			||||||
                            let content = group.as_str();
 | 
					 | 
				
			||||||
                            args.push(json!({
 | 
					 | 
				
			||||||
                                "name": "content",
 | 
					 | 
				
			||||||
                                "value": content,
 | 
					 | 
				
			||||||
                                "type": CommandOptionType::String,
 | 
					 | 
				
			||||||
                            }));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(group) = captures.name("interval") {
 | 
					 | 
				
			||||||
                            let content = group.as_str();
 | 
					 | 
				
			||||||
                            args.push(json!({
 | 
					 | 
				
			||||||
                                "name": "interval",
 | 
					 | 
				
			||||||
                                "value": content,
 | 
					 | 
				
			||||||
                                "type": CommandOptionType::String,
 | 
					 | 
				
			||||||
                            }));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(group) = captures.name("expires") {
 | 
					 | 
				
			||||||
                            let content = group.as_str();
 | 
					 | 
				
			||||||
                            args.push(json!({
 | 
					 | 
				
			||||||
                                "name": "expires",
 | 
					 | 
				
			||||||
                                "value": content,
 | 
					 | 
				
			||||||
                                "type": CommandOptionType::String,
 | 
					 | 
				
			||||||
                            }));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(group) = captures.name("mentions") {
 | 
					 | 
				
			||||||
                            let content = group.as_str();
 | 
					 | 
				
			||||||
                            args.push(json!({
 | 
					 | 
				
			||||||
                                "name": "channels",
 | 
					 | 
				
			||||||
                                "value": content,
 | 
					 | 
				
			||||||
                                "type": CommandOptionType::String,
 | 
					 | 
				
			||||||
                            }));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        Some(RawCommandMacro {
 | 
					 | 
				
			||||||
                            guild_id,
 | 
					 | 
				
			||||||
                            name: alias_name,
 | 
					 | 
				
			||||||
                            description: None,
 | 
					 | 
				
			||||||
                            commands: json!([
 | 
					 | 
				
			||||||
                                {
 | 
					 | 
				
			||||||
                                    "command_name": "remind",
 | 
					 | 
				
			||||||
                                    "options": args,
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            ]),
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    None => None,
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else if command_word == "n" || command_word == "natural" {
 | 
					 | 
				
			||||||
                let matcher_primary = regex!(
 | 
					 | 
				
			||||||
                    r#"(?P<time>.*?)(?:\s+)(?:send|say)(?:\s+)(?P<content>.*?)(?:(?:\s+)to(?:\s+)(?P<mentions>((?:<@\d+>)|(?:<@!\d+>)|(?:<#\d+>)|(?:\s+))+))?$"#s
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                let matcher_secondary = regex!(
 | 
					 | 
				
			||||||
                    r#"(?P<msg>.*)(?:\s+)every(?:\s+)(?P<interval>.*?)(?:(?:\s+)(?:until|for)(?:\s+)(?P<expires>.*?))?$"#s
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                match matcher_primary.captures(&args) {
 | 
					 | 
				
			||||||
                    Some(captures) => {
 | 
					 | 
				
			||||||
                        let captures_secondary = matcher_secondary.captures(&args);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        let mut args: Vec<Value> = vec![];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(group) = captures.name("time") {
 | 
					 | 
				
			||||||
                            let content = group.as_str();
 | 
					 | 
				
			||||||
                            args.push(json!({
 | 
					 | 
				
			||||||
                                "name": "time",
 | 
					 | 
				
			||||||
                                "value": content,
 | 
					 | 
				
			||||||
                                "type": CommandOptionType::String,
 | 
					 | 
				
			||||||
                            }));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(group) = captures.name("content") {
 | 
					 | 
				
			||||||
                            let content = group.as_str();
 | 
					 | 
				
			||||||
                            args.push(json!({
 | 
					 | 
				
			||||||
                                "name": "content",
 | 
					 | 
				
			||||||
                                "value": content,
 | 
					 | 
				
			||||||
                                "type": CommandOptionType::String,
 | 
					 | 
				
			||||||
                            }));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(group) =
 | 
					 | 
				
			||||||
                            captures_secondary.as_ref().and_then(|c: &Captures| c.name("interval"))
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            let content = group.as_str();
 | 
					 | 
				
			||||||
                            args.push(json!({
 | 
					 | 
				
			||||||
                                "name": "interval",
 | 
					 | 
				
			||||||
                                "value": content,
 | 
					 | 
				
			||||||
                                "type": CommandOptionType::String,
 | 
					 | 
				
			||||||
                            }));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(group) =
 | 
					 | 
				
			||||||
                            captures_secondary.and_then(|c: Captures| c.name("expires"))
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            let content = group.as_str();
 | 
					 | 
				
			||||||
                            args.push(json!({
 | 
					 | 
				
			||||||
                                "name": "expires",
 | 
					 | 
				
			||||||
                                "value": content,
 | 
					 | 
				
			||||||
                                "type": CommandOptionType::String,
 | 
					 | 
				
			||||||
                            }));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(group) = captures.name("mentions") {
 | 
					 | 
				
			||||||
                            let content = group.as_str();
 | 
					 | 
				
			||||||
                            args.push(json!({
 | 
					 | 
				
			||||||
                                "name": "channels",
 | 
					 | 
				
			||||||
                                "value": content,
 | 
					 | 
				
			||||||
                                "type": CommandOptionType::String,
 | 
					 | 
				
			||||||
                            }));
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        Some(RawCommandMacro {
 | 
					 | 
				
			||||||
                            guild_id,
 | 
					 | 
				
			||||||
                            name: alias_name,
 | 
					 | 
				
			||||||
                            description: None,
 | 
					 | 
				
			||||||
                            commands: json!([
 | 
					 | 
				
			||||||
                                {
 | 
					 | 
				
			||||||
                                    "command_name": "remind",
 | 
					 | 
				
			||||||
                                    "options": args,
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            ]),
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    None => None,
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                None
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        None => None,
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,19 +0,0 @@
 | 
				
			|||||||
use crate::{Context, Error};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub mod delete;
 | 
					 | 
				
			||||||
pub mod list;
 | 
					 | 
				
			||||||
pub mod migrate;
 | 
					 | 
				
			||||||
pub mod record;
 | 
					 | 
				
			||||||
pub mod run;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Record and replay command sequences
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    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> {
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,151 +0,0 @@
 | 
				
			|||||||
use std::collections::hash_map::Entry;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{consts::THEME_COLOR, models::command_macro::CommandMacro, Context, Error};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Start recording up to 5 commands to replay
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "record",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					 | 
				
			||||||
    identifying_name = "record_macro"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn record_macro(
 | 
					 | 
				
			||||||
    ctx: Context<'_>,
 | 
					 | 
				
			||||||
    #[description = "Name for the new macro"] name: String,
 | 
					 | 
				
			||||||
    #[description = "Description for the new macro"] description: Option<String>,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    if name.len() > 100 {
 | 
					 | 
				
			||||||
        ctx.say("Name must be less than 100 characters").await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Ok(());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if description.as_ref().map_or(0, |d| d.len()) > 100 {
 | 
					 | 
				
			||||||
        ctx.say("Description must be less than 100 characters").await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Ok(());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let guild_id = ctx.guild_id().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let row = sqlx::query!(
 | 
					 | 
				
			||||||
        "
 | 
					 | 
				
			||||||
SELECT 1 as _e FROM macro WHERE guild_id = ? AND name = ?",
 | 
					 | 
				
			||||||
        guild_id.0,
 | 
					 | 
				
			||||||
        name
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_one(&ctx.data().database)
 | 
					 | 
				
			||||||
    .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if row.is_ok() {
 | 
					 | 
				
			||||||
        ctx.send(|m| {
 | 
					 | 
				
			||||||
            m.ephemeral(true).embed(|e| {
 | 
					 | 
				
			||||||
                e.title("Unique Name Required")
 | 
					 | 
				
			||||||
                    .description(
 | 
					 | 
				
			||||||
                        "A macro already exists under this name.
 | 
					 | 
				
			||||||
Please select a unique name for your macro.",
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .await?;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        let okay = {
 | 
					 | 
				
			||||||
            let mut lock = ctx.data().recording_macros.write().await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Entry::Vacant(e) = lock.entry((guild_id, ctx.author().id)) {
 | 
					 | 
				
			||||||
                e.insert(CommandMacro { guild_id, name, description, commands: vec![] });
 | 
					 | 
				
			||||||
                true
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                false
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if okay {
 | 
					 | 
				
			||||||
            ctx.send(|m| {
 | 
					 | 
				
			||||||
                m.ephemeral(true).embed(|e| {
 | 
					 | 
				
			||||||
                    e.title("Macro Recording Started")
 | 
					 | 
				
			||||||
                        .description(
 | 
					 | 
				
			||||||
                            "Run up to 5 commands, or type `/macro finish` to stop at any point.
 | 
					 | 
				
			||||||
Any commands ran as part of recording will be inconsequential",
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            ctx.send(|m| {
 | 
					 | 
				
			||||||
                m.ephemeral(true).embed(|e| {
 | 
					 | 
				
			||||||
                    e.title("Macro Already Recording")
 | 
					 | 
				
			||||||
                        .description(
 | 
					 | 
				
			||||||
                            "You are already recording a macro in this server.
 | 
					 | 
				
			||||||
Please use `/macro finish` to end this recording before starting another.",
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Finish current macro recording
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "finish",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					 | 
				
			||||||
    identifying_name = "finish_macro"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let key = (ctx.guild_id().unwrap(), ctx.author().id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let lock = ctx.data().recording_macros.read().await;
 | 
					 | 
				
			||||||
        let contained = lock.get(&key);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if contained.map_or(true, |cmacro| cmacro.commands.is_empty()) {
 | 
					 | 
				
			||||||
            ctx.send(|m| {
 | 
					 | 
				
			||||||
                m.embed(|e| {
 | 
					 | 
				
			||||||
                    e.title("No Macro Recorded")
 | 
					 | 
				
			||||||
                        .description("Use `/macro record` to start recording a macro")
 | 
					 | 
				
			||||||
                        .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            let command_macro = contained.unwrap();
 | 
					 | 
				
			||||||
            let json = serde_json::to_string(&command_macro.commands).unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            sqlx::query!(
 | 
					 | 
				
			||||||
                "INSERT INTO macro (guild_id, name, description, commands) VALUES (?, ?, ?, ?)",
 | 
					 | 
				
			||||||
                command_macro.guild_id.0,
 | 
					 | 
				
			||||||
                command_macro.name,
 | 
					 | 
				
			||||||
                command_macro.description,
 | 
					 | 
				
			||||||
                json
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(&ctx.data().database)
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            ctx.send(|m| {
 | 
					 | 
				
			||||||
                m.embed(|e| {
 | 
					 | 
				
			||||||
                    e.title("Macro Recorded")
 | 
					 | 
				
			||||||
                        .description("Use `/macro run` to execute the macro")
 | 
					 | 
				
			||||||
                        .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let mut lock = ctx.data().recording_macros.write().await;
 | 
					 | 
				
			||||||
        lock.remove(&key);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,46 +0,0 @@
 | 
				
			|||||||
use super::super::autocomplete::macro_name_autocomplete;
 | 
					 | 
				
			||||||
use crate::{models::command_macro::guild_command_macro, Context, Data, Error};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Run a recorded macro
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    rename = "run",
 | 
					 | 
				
			||||||
    guild_only = true,
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD",
 | 
					 | 
				
			||||||
    identifying_name = "run_macro"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn run_macro(
 | 
					 | 
				
			||||||
    ctx: poise::ApplicationContext<'_, Data, Error>,
 | 
					 | 
				
			||||||
    #[description = "Name of macro to run"]
 | 
					 | 
				
			||||||
    #[autocomplete = "macro_name_autocomplete"]
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    match guild_command_macro(&Context::Application(ctx), &name).await {
 | 
					 | 
				
			||||||
        Some(command_macro) => {
 | 
					 | 
				
			||||||
            ctx.defer_response(false).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for command in command_macro.commands {
 | 
					 | 
				
			||||||
                if let Some(action) = command.action {
 | 
					 | 
				
			||||||
                    match (action)(poise::ApplicationContext { args: &command.options, ..ctx })
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Ok(()) => {}
 | 
					 | 
				
			||||||
                        Err(e) => {
 | 
					 | 
				
			||||||
                            println!("{:?}", e);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    Context::Application(ctx)
 | 
					 | 
				
			||||||
                        .say(format!("Command \"{}\" not found", command.command_name))
 | 
					 | 
				
			||||||
                        .await?;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        None => {
 | 
					 | 
				
			||||||
            Context::Application(ctx).say(format!("Macro \"{}\" not found", name)).await?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -49,7 +49,6 @@ __Todo Commands__
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
__Setup Commands__
 | 
					__Setup Commands__
 | 
				
			||||||
`/timezone` - Set your timezone (necessary for `/remind` to work properly)
 | 
					`/timezone` - Set your timezone (necessary for `/remind` to work properly)
 | 
				
			||||||
`/dm allow/block` - Change your DM settings for reminders.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
__Advanced Commands__
 | 
					__Advanced Commands__
 | 
				
			||||||
`/macro` - Record and replay command sequences
 | 
					`/macro` - Record and replay command sequences
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,3 @@
 | 
				
			|||||||
mod autocomplete;
 | 
					 | 
				
			||||||
pub mod command_macro;
 | 
					 | 
				
			||||||
pub mod info_cmds;
 | 
					pub mod info_cmds;
 | 
				
			||||||
pub mod moderation_cmds;
 | 
					pub mod moderation_cmds;
 | 
				
			||||||
pub mod reminder_cmds;
 | 
					pub mod reminder_cmds;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,32 @@
 | 
				
			|||||||
 | 
					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 log::warn;
 | 
					use poise::CreateReply;
 | 
				
			||||||
use poise::serenity_prelude::{ChannelId, Mentionable};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use super::autocomplete::timezone_autocomplete;
 | 
					use crate::{
 | 
				
			||||||
use crate::{consts::THEME_COLOR, models::CtxData, Context, Error};
 | 
					    component_models::pager::{MacroPager, Pager},
 | 
				
			||||||
 | 
					    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
				
			||||||
 | 
					    models::{
 | 
				
			||||||
 | 
					        command_macro::{guild_command_macro, CommandMacro},
 | 
				
			||||||
 | 
					        CtxData,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Context, Data, Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn timezone_autocomplete(ctx: Context<'_>, partial: String) -> Vec<String> {
 | 
				
			||||||
 | 
					    if partial.is_empty() {
 | 
				
			||||||
 | 
					        ctx.data().popular_timezones.iter().map(|t| t.to_string()).collect::<Vec<String>>()
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        TZ_VARIANTS
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .filter(|tz| tz.to_string().contains(&partial))
 | 
				
			||||||
 | 
					            .take(25)
 | 
				
			||||||
 | 
					            .map(|t| t.to_string())
 | 
				
			||||||
 | 
					            .collect::<Vec<String>>()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Select your timezone
 | 
					/// Select your timezone
 | 
				
			||||||
#[poise::command(slash_command, identifying_name = "timezone")]
 | 
					#[poise::command(slash_command, identifying_name = "timezone")]
 | 
				
			||||||
@@ -103,122 +124,376 @@ You may want to use one of the popular timezones below, otherwise click [here](h
 | 
				
			|||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Configure whether other users can set reminders to your direct messages
 | 
					async fn macro_name_autocomplete(ctx: Context<'_>, partial: String) -> Vec<String> {
 | 
				
			||||||
#[poise::command(slash_command, rename = "dm", identifying_name = "allowed_dm")]
 | 
					    sqlx::query!(
 | 
				
			||||||
pub async fn allowed_dm(_ctx: Context<'_>) -> Result<(), Error> {
 | 
					        "
 | 
				
			||||||
 | 
					SELECT name
 | 
				
			||||||
 | 
					FROM macro
 | 
				
			||||||
 | 
					WHERE
 | 
				
			||||||
 | 
					    guild_id = (SELECT id FROM guilds WHERE guild = ?)
 | 
				
			||||||
 | 
					    AND name LIKE CONCAT(?, '%')",
 | 
				
			||||||
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
 | 
					        partial,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap_or_default()
 | 
				
			||||||
 | 
					    .iter()
 | 
				
			||||||
 | 
					    .map(|s| s.name.clone())
 | 
				
			||||||
 | 
					    .collect()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Record and replay command sequences
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    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> {
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Allow other users to set reminders in your direct messages
 | 
					/// Start recording up to 5 commands to replay
 | 
				
			||||||
#[poise::command(slash_command, rename = "allow", identifying_name = "allowed_dm")]
 | 
					#[poise::command(
 | 
				
			||||||
pub async fn set_allowed_dm(ctx: Context<'_>) -> Result<(), Error> {
 | 
					    slash_command,
 | 
				
			||||||
    let mut user_data = ctx.author_data().await?;
 | 
					    rename = "record",
 | 
				
			||||||
    user_data.allowed_dm = true;
 | 
					    guild_only = true,
 | 
				
			||||||
    user_data.commit_changes(&ctx.data().database).await;
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "record_macro"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn record_macro(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    #[description = "Name for the new macro"] name: String,
 | 
				
			||||||
 | 
					    #[description = "Description for the new macro"] description: Option<String>,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let guild_id = ctx.guild_id().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(|r| {
 | 
					    let row = sqlx::query!(
 | 
				
			||||||
        r.ephemeral(true).embed(|e| {
 | 
					        "
 | 
				
			||||||
            e.title("DMs permitted")
 | 
					SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
				
			||||||
                .description("You will receive a message if a user sets a DM reminder for you.")
 | 
					        guild_id.0,
 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					        name
 | 
				
			||||||
        })
 | 
					    )
 | 
				
			||||||
    })
 | 
					    .fetch_one(&ctx.data().database)
 | 
				
			||||||
    .await?;
 | 
					    .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    if row.is_ok() {
 | 
				
			||||||
}
 | 
					        ctx.send(|m| {
 | 
				
			||||||
 | 
					            m.ephemeral(true).embed(|e| {
 | 
				
			||||||
/// Block other users from setting reminders in your direct messages
 | 
					                e.title("Unique Name Required")
 | 
				
			||||||
#[poise::command(slash_command, rename = "block", identifying_name = "allowed_dm")]
 | 
					 | 
				
			||||||
pub async fn unset_allowed_dm(ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let mut user_data = ctx.author_data().await?;
 | 
					 | 
				
			||||||
    user_data.allowed_dm = false;
 | 
					 | 
				
			||||||
    user_data.commit_changes(&ctx.data().database).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ctx.send(|r| {
 | 
					 | 
				
			||||||
        r.ephemeral(true).embed(|e| {
 | 
					 | 
				
			||||||
            e.title("DMs blocked")
 | 
					 | 
				
			||||||
                    .description(
 | 
					                    .description(
 | 
				
			||||||
                    "You can still set DM reminders for yourself or for users with DMs enabled.",
 | 
					                        "A macro already exists under this name.
 | 
				
			||||||
 | 
					Please select a unique name for your macro.",
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    .color(*THEME_COLOR)
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .await?;
 | 
					        .await?;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        let okay = {
 | 
				
			||||||
 | 
					            let mut lock = ctx.data().recording_macros.write().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Entry::Vacant(e) = lock.entry((guild_id, ctx.author().id)) {
 | 
				
			||||||
 | 
					                e.insert(CommandMacro { guild_id, name, description, commands: vec![] });
 | 
				
			||||||
 | 
					                true
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                false
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if okay {
 | 
				
			||||||
 | 
					            ctx.send(|m| {
 | 
				
			||||||
 | 
					                m.ephemeral(true).embed(|e| {
 | 
				
			||||||
 | 
					                    e.title("Macro Recording Started")
 | 
				
			||||||
 | 
					                        .description(
 | 
				
			||||||
 | 
					                            "Run up to 5 commands, or type `/macro finish` to stop at any point.
 | 
				
			||||||
 | 
					Any commands ran as part of recording will be inconsequential",
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            ctx.send(|m| {
 | 
				
			||||||
 | 
					                m.ephemeral(true).embed(|e| {
 | 
				
			||||||
 | 
					                    e.title("Macro Already Recording")
 | 
				
			||||||
 | 
					                        .description(
 | 
				
			||||||
 | 
					                            "You are already recording a macro in this server.
 | 
				
			||||||
 | 
					Please use `/macro finish` to end this recording before starting another.",
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Set defaults for commands
 | 
					/// Finish current macro recording
 | 
				
			||||||
#[poise::command(
 | 
					 | 
				
			||||||
    slash_command,
 | 
					 | 
				
			||||||
    identifying_name = "default",
 | 
					 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD"
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
pub async fn default(_ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Set a default channel for reminders to be sent to
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					#[poise::command(
 | 
				
			||||||
    slash_command,
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "finish",
 | 
				
			||||||
    guild_only = true,
 | 
					    guild_only = true,
 | 
				
			||||||
    identifying_name = "default_channel",
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD"
 | 
					    identifying_name = "finish_macro"
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
pub async fn default_channel(
 | 
					pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					    let key = (ctx.guild_id().unwrap(), ctx.author().id);
 | 
				
			||||||
    #[description = "Channel to send reminders to by default"] channel: Option<ChannelId>,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    if let Some(mut guild_data) = ctx.guild_data().await {
 | 
					 | 
				
			||||||
        guild_data.default_channel = channel.map(|c| c.0);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        guild_data.commit_changes(&ctx.data().database).await?;
 | 
					    {
 | 
				
			||||||
 | 
					        let lock = ctx.data().recording_macros.read().await;
 | 
				
			||||||
 | 
					        let contained = lock.get(&key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(channel) = channel {
 | 
					        if contained.map_or(true, |cmacro| cmacro.commands.is_empty()) {
 | 
				
			||||||
            ctx.send(|r| {
 | 
					            ctx.send(|m| {
 | 
				
			||||||
                r.ephemeral(true).content(format!("Default channel set to {}", channel.mention()))
 | 
					                m.embed(|e| {
 | 
				
			||||||
 | 
					                    e.title("No Macro Recorded")
 | 
				
			||||||
 | 
					                        .description("Use `/macro record` to start recording a macro")
 | 
				
			||||||
 | 
					                        .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .await?;
 | 
					            .await?;
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            ctx.send(|r| r.ephemeral(true).content("Default channel unset.")).await?;
 | 
					            let command_macro = contained.unwrap();
 | 
				
			||||||
 | 
					            let json = serde_json::to_string(&command_macro.commands).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
 | 
				
			||||||
 | 
					                command_macro.guild_id.0,
 | 
				
			||||||
 | 
					                command_macro.name,
 | 
				
			||||||
 | 
					                command_macro.description,
 | 
				
			||||||
 | 
					                json
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					                .execute(&ctx.data().database)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ctx.send(|m| {
 | 
				
			||||||
 | 
					                m.embed(|e| {
 | 
				
			||||||
 | 
					                    e.title("Macro Recorded")
 | 
				
			||||||
 | 
					                        .description("Use `/macro run` to execute the macro")
 | 
				
			||||||
 | 
					                        .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let mut lock = ctx.data().recording_macros.write().await;
 | 
				
			||||||
 | 
					        lock.remove(&key);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// View the webhook being used to send reminders to this channel
 | 
					/// List recorded macros
 | 
				
			||||||
#[poise::command(
 | 
					#[poise::command(
 | 
				
			||||||
    slash_command,
 | 
					    slash_command,
 | 
				
			||||||
    identifying_name = "webhook_url",
 | 
					    rename = "list",
 | 
				
			||||||
    required_permissions = "ADMINISTRATOR",
 | 
					    guild_only = true,
 | 
				
			||||||
    default_member_permissions = "ADMINISTRATOR"
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "list_macro"
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    match ctx.channel_data().await {
 | 
					    let macros = ctx.command_macros().await?;
 | 
				
			||||||
        Ok(data) => {
 | 
					
 | 
				
			||||||
            if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
 | 
					    let resp = show_macro_page(¯os, 0);
 | 
				
			||||||
                ctx.send(|b| {
 | 
					
 | 
				
			||||||
                    b.ephemeral(true).content(format!(
 | 
					    ctx.send(|m| {
 | 
				
			||||||
                        "**Warning!**
 | 
					        *m = resp;
 | 
				
			||||||
This link can be used by users to anonymously send messages, with or without permissions.
 | 
					        m
 | 
				
			||||||
Do not share it!
 | 
					 | 
				
			||||||
|| https://discord.com/api/webhooks/{}/{} ||",
 | 
					 | 
				
			||||||
                        id, token,
 | 
					 | 
				
			||||||
                    ))
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .await?;
 | 
					    .await?;
 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                ctx.say("No webhook configured on this channel.").await?;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Error fetching channel data: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ctx.say("No webhook configured on this channel.").await?;
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Run a recorded macro
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "run",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "run_macro"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn run_macro(
 | 
				
			||||||
 | 
					    ctx: poise::ApplicationContext<'_, Data, Error>,
 | 
				
			||||||
 | 
					    #[description = "Name of macro to run"]
 | 
				
			||||||
 | 
					    #[autocomplete = "macro_name_autocomplete"]
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    match guild_command_macro(&Context::Application(ctx), &name).await {
 | 
				
			||||||
 | 
					        Some(command_macro) => {
 | 
				
			||||||
 | 
					            ctx.defer_response(false).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for command in command_macro.commands {
 | 
				
			||||||
 | 
					                if let Some(action) = command.action {
 | 
				
			||||||
 | 
					                    match (action)(poise::ApplicationContext { args: &command.options, ..ctx })
 | 
				
			||||||
 | 
					                        .await
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        Ok(()) => {}
 | 
				
			||||||
 | 
					                        Err(e) => {
 | 
				
			||||||
 | 
					                            println!("{:?}", e);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Context::Application(ctx)
 | 
				
			||||||
 | 
					                        .say(format!("Command \"{}\" not found", command.command_name))
 | 
				
			||||||
 | 
					                        .await?;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        None => {
 | 
				
			||||||
 | 
					            Context::Application(ctx).say(format!("Macro \"{}\" not found", name)).await?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Delete a recorded macro
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "delete",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "delete_macro"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn delete_macro(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    #[description = "Name of macro to delete"]
 | 
				
			||||||
 | 
					    #[autocomplete = "macro_name_autocomplete"]
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    match sqlx::query!(
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
				
			||||||
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
 | 
					        name
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_one(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(row) => {
 | 
				
			||||||
 | 
					            sqlx::query!("DELETE FROM macro WHERE id = ?", row.id)
 | 
				
			||||||
 | 
					                .execute(&ctx.data().database)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ctx.say(format!("Macro \"{}\" deleted", name)).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
 | 
					            ctx.say(format!("Macro \"{}\" not found", name)).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            panic!("{}", e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize {
 | 
				
			||||||
 | 
					    let mut skipped_char_count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    macros
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .map(|m| {
 | 
				
			||||||
 | 
					            if let Some(description) = &m.description {
 | 
				
			||||||
 | 
					                format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len())
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                format!("**{}**\n- Has {} commands", m.name, m.commands.len())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .fold(1, |mut pages, p| {
 | 
				
			||||||
 | 
					            skipped_char_count += p.len();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH {
 | 
				
			||||||
 | 
					                skipped_char_count = p.len();
 | 
				
			||||||
 | 
					                pages += 1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            pages
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply {
 | 
				
			||||||
 | 
					    let pager = MacroPager::new(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if macros.is_empty() {
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return reply;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let pages = max_macro_page(macros);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut page = page;
 | 
				
			||||||
 | 
					    if page >= pages {
 | 
				
			||||||
 | 
					        page = pages - 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut char_count = 0;
 | 
				
			||||||
 | 
					    let mut skipped_char_count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut skipped_pages = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let display_vec: Vec<String> = macros
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .map(|m| {
 | 
				
			||||||
 | 
					            if let Some(description) = &m.description {
 | 
				
			||||||
 | 
					                format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len())
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                format!("**{}**\n- Has {} commands", m.name, m.commands.len())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .skip_while(|p| {
 | 
				
			||||||
 | 
					            skipped_char_count += p.len();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH {
 | 
				
			||||||
 | 
					                skipped_char_count = p.len();
 | 
				
			||||||
 | 
					                skipped_pages += 1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            skipped_pages < page
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .take_while(|p| {
 | 
				
			||||||
 | 
					            char_count += p.len();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            char_count < EMBED_DESCRIPTION_MAX_LENGTH
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .collect::<Vec<String>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let display = display_vec.join("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut reply = CreateReply::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reply
 | 
				
			||||||
 | 
					        .embed(|e| {
 | 
				
			||||||
 | 
					            e.title("Macros")
 | 
				
			||||||
 | 
					                .description(display)
 | 
				
			||||||
 | 
					                .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
				
			||||||
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .components(|comp| {
 | 
				
			||||||
 | 
					            pager.create_button_row(pages, comp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            comp
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reply
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,16 +8,12 @@ use chrono::NaiveDateTime;
 | 
				
			|||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use num_integer::Integer;
 | 
					use num_integer::Integer;
 | 
				
			||||||
use poise::{
 | 
					use poise::{
 | 
				
			||||||
    serenity_prelude::{
 | 
					    serenity::{builder::CreateEmbed, model::channel::Channel},
 | 
				
			||||||
        builder::CreateEmbed, component::ButtonStyle, model::channel::Channel, ReactionType,
 | 
					    serenity_prelude::{ButtonStyle, ReactionType},
 | 
				
			||||||
    },
 | 
					    CreateReply,
 | 
				
			||||||
    CreateReply, Modal,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::autocomplete::{
 | 
					 | 
				
			||||||
        multiline_autocomplete, time_hint_autocomplete, timezone_autocomplete,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    component_models::{
 | 
					    component_models::{
 | 
				
			||||||
        pager::{DelPager, LookPager, Pager},
 | 
					        pager::{DelPager, LookPager, Pager},
 | 
				
			||||||
        ComponentDataModel, DelSelector, UndoReminder,
 | 
					        ComponentDataModel, DelSelector, UndoReminder,
 | 
				
			||||||
@@ -40,7 +36,7 @@ use crate::{
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    time_parser::natural_parser,
 | 
					    time_parser::natural_parser,
 | 
				
			||||||
    utils::{check_guild_subscription, check_subscription},
 | 
					    utils::{check_guild_subscription, check_subscription},
 | 
				
			||||||
    ApplicationContext, Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Pause all reminders on the current channel until a certain time or indefinitely
 | 
					/// Pause all reminders on the current channel until a certain time or indefinitely
 | 
				
			||||||
@@ -552,81 +548,23 @@ pub async fn delete_timer(
 | 
				
			|||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(poise::Modal)]
 | 
					/// Create a new reminder
 | 
				
			||||||
#[name = "Reminder"]
 | 
					 | 
				
			||||||
struct ContentModal {
 | 
					 | 
				
			||||||
    #[name = "Content"]
 | 
					 | 
				
			||||||
    #[placeholder = "Message..."]
 | 
					 | 
				
			||||||
    #[paragraph]
 | 
					 | 
				
			||||||
    #[max_length = 2000]
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Create a reminder. Press "+4 more" for other options.
 | 
					 | 
				
			||||||
#[poise::command(
 | 
					#[poise::command(
 | 
				
			||||||
    slash_command,
 | 
					    slash_command,
 | 
				
			||||||
    identifying_name = "remind",
 | 
					    identifying_name = "remind",
 | 
				
			||||||
    default_member_permissions = "MANAGE_GUILD"
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
pub async fn remind(
 | 
					pub async fn remind(
 | 
				
			||||||
    ctx: ApplicationContext<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "A description of the time to set the reminder for"]
 | 
					    #[description = "A description of the time to set the reminder for"] time: String,
 | 
				
			||||||
    #[autocomplete = "time_hint_autocomplete"]
 | 
					    #[description = "The message content to send"] content: String,
 | 
				
			||||||
    time: String,
 | 
					 | 
				
			||||||
    #[description = "The message content to send"]
 | 
					 | 
				
			||||||
    #[autocomplete = "multiline_autocomplete"]
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
    #[description = "Channel or user mentions to set the reminder for"] channels: Option<String>,
 | 
					    #[description = "Channel or user mentions to set the reminder for"] channels: Option<String>,
 | 
				
			||||||
    #[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"]
 | 
					    #[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"]
 | 
				
			||||||
    interval: Option<String>,
 | 
					    interval: Option<String>,
 | 
				
			||||||
    #[description = "(Patreon only) For repeating reminders, the time at which the reminder will stop repeating"]
 | 
					    #[description = "(Patreon only) For repeating reminders, the time at which the reminder will stop sending"]
 | 
				
			||||||
    expires: Option<String>,
 | 
					    expires: Option<String>,
 | 
				
			||||||
    #[description = "Set the TTS flag on the reminder message, similar to the /tts command"]
 | 
					    #[description = "Set the TTS flag on the reminder message, similar to the /tts command"]
 | 
				
			||||||
    tts: Option<bool>,
 | 
					    tts: Option<bool>,
 | 
				
			||||||
    #[description = "Set a timezone override for this reminder only"]
 | 
					 | 
				
			||||||
    #[autocomplete = "timezone_autocomplete"]
 | 
					 | 
				
			||||||
    timezone: Option<String>,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let tz = timezone.map(|t| t.parse::<Tz>().ok()).flatten();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if content.is_empty() {
 | 
					 | 
				
			||||||
        let data = ContentModal::execute(ctx).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        create_reminder(
 | 
					 | 
				
			||||||
            Context::Application(ctx),
 | 
					 | 
				
			||||||
            time,
 | 
					 | 
				
			||||||
            data.content,
 | 
					 | 
				
			||||||
            channels,
 | 
					 | 
				
			||||||
            interval,
 | 
					 | 
				
			||||||
            expires,
 | 
					 | 
				
			||||||
            tts,
 | 
					 | 
				
			||||||
            tz,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        create_reminder(
 | 
					 | 
				
			||||||
            Context::Application(ctx),
 | 
					 | 
				
			||||||
            time,
 | 
					 | 
				
			||||||
            content,
 | 
					 | 
				
			||||||
            channels,
 | 
					 | 
				
			||||||
            interval,
 | 
					 | 
				
			||||||
            expires,
 | 
					 | 
				
			||||||
            tts,
 | 
					 | 
				
			||||||
            tz,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn create_reminder(
 | 
					 | 
				
			||||||
    ctx: Context<'_>,
 | 
					 | 
				
			||||||
    time: String,
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
    channels: Option<String>,
 | 
					 | 
				
			||||||
    interval: Option<String>,
 | 
					 | 
				
			||||||
    expires: Option<String>,
 | 
					 | 
				
			||||||
    tts: Option<bool>,
 | 
					 | 
				
			||||||
    timezone: Option<Tz>,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    if interval.is_none() && expires.is_some() {
 | 
					    if interval.is_none() && expires.is_some() {
 | 
				
			||||||
        ctx.say("`expires` can only be used with `interval`").await?;
 | 
					        ctx.say("`expires` can only be used with `interval`").await?;
 | 
				
			||||||
@@ -637,7 +575,7 @@ async fn create_reminder(
 | 
				
			|||||||
    ctx.defer().await?;
 | 
					    ctx.defer().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let user_data = ctx.author_data().await.unwrap();
 | 
					    let user_data = ctx.author_data().await.unwrap();
 | 
				
			||||||
    let timezone = timezone.unwrap_or(ctx.timezone().await);
 | 
					    let timezone = ctx.timezone().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let time = natural_parser(&time, &timezone.to_string()).await;
 | 
					    let time = natural_parser(&time, &timezone.to_string()).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -653,9 +591,7 @@ async fn create_reminder(
 | 
				
			|||||||
                let list = channels.map(|arg| parse_mention_list(&arg)).unwrap_or_default();
 | 
					                let list = channels.map(|arg| parse_mention_list(&arg)).unwrap_or_default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if list.is_empty() {
 | 
					                if list.is_empty() {
 | 
				
			||||||
                    if let Some(channel_id) = ctx.default_channel().await {
 | 
					                    if ctx.guild_id().is_some() {
 | 
				
			||||||
                        vec![ReminderScope::Channel(channel_id.0)]
 | 
					 | 
				
			||||||
                    } else if ctx.guild_id().is_some() {
 | 
					 | 
				
			||||||
                        vec![ReminderScope::Channel(ctx.channel_id().0)]
 | 
					                        vec![ReminderScope::Channel(ctx.channel_id().0)]
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        vec![ReminderScope::User(ctx.author().id.0)]
 | 
					                        vec![ReminderScope::User(ctx.author().id.0)]
 | 
				
			||||||
@@ -758,7 +694,6 @@ async fn create_reminder(
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
        None => {
 | 
					        None => {
 | 
				
			||||||
            ctx.say("Time could not be processed").await?;
 | 
					            ctx.say("Time could not be processed").await?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,6 @@ 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},
 | 
				
			||||||
    models::CtxData,
 | 
					 | 
				
			||||||
    Context, Error,
 | 
					    Context, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -47,7 +46,7 @@ pub async fn todo_guild_add(
 | 
				
			|||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    sqlx::query!(
 | 
					    sqlx::query!(
 | 
				
			||||||
        "INSERT INTO todos (guild_id, value)
 | 
					        "INSERT INTO todos (guild_id, value)
 | 
				
			||||||
VALUES (?, ?)",
 | 
					VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
        task
 | 
					        task
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@@ -70,7 +69,9 @@ VALUES (?, ?)",
 | 
				
			|||||||
)]
 | 
					)]
 | 
				
			||||||
pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
					pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    let values = sqlx::query!(
 | 
					    let values = sqlx::query!(
 | 
				
			||||||
        "SELECT todos.id, value FROM todos WHERE guild_id = ?",
 | 
					        "SELECT todos.id, value FROM todos
 | 
				
			||||||
 | 
					INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
				
			||||||
 | 
					WHERE guilds.guild = ?",
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .fetch_all(&ctx.data().database)
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
@@ -115,12 +116,9 @@ pub async fn todo_channel_add(
 | 
				
			|||||||
    ctx: Context<'_>,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    #[description = "The task to add to the todo list"] task: String,
 | 
					    #[description = "The task to add to the todo list"] task: String,
 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    // ensure channel is cached
 | 
					 | 
				
			||||||
    let _ = ctx.channel_data().await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sqlx::query!(
 | 
					    sqlx::query!(
 | 
				
			||||||
        "INSERT INTO todos (guild_id, channel_id, value)
 | 
					        "INSERT INTO todos (guild_id, channel_id, value)
 | 
				
			||||||
VALUES (?, (SELECT id FROM channels WHERE channel = ?), ?)",
 | 
					VALUES ((SELECT id FROM guilds WHERE guild = ?), (SELECT id FROM channels WHERE channel = ?), ?)",
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
        ctx.channel_id().0,
 | 
					        ctx.channel_id().0,
 | 
				
			||||||
        task
 | 
					        task
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,25 +5,25 @@ use std::io::Cursor;
 | 
				
			|||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use log::warn;
 | 
					use log::warn;
 | 
				
			||||||
use poise::{
 | 
					use poise::{
 | 
				
			||||||
    serenity_prelude as serenity,
 | 
					    serenity::{
 | 
				
			||||||
    serenity_prelude::{
 | 
					 | 
				
			||||||
        builder::CreateEmbed,
 | 
					        builder::CreateEmbed,
 | 
				
			||||||
 | 
					        client::Context,
 | 
				
			||||||
        model::{
 | 
					        model::{
 | 
				
			||||||
            application::interaction::{
 | 
					 | 
				
			||||||
                message_component::MessageComponentInteraction, InteractionResponseType,
 | 
					 | 
				
			||||||
                MessageFlags,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            channel::Channel,
 | 
					            channel::Channel,
 | 
				
			||||||
 | 
					            interactions::{
 | 
				
			||||||
 | 
					                message_component::MessageComponentInteraction, InteractionResponseType,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        Context,
 | 
					            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::{
 | 
					    commands::{
 | 
				
			||||||
        command_macro::list::{max_macro_page, show_macro_page},
 | 
					        moderation_cmds::{max_macro_page, show_macro_page},
 | 
				
			||||||
        reminder_cmds::{max_delete_page, show_delete_page},
 | 
					        reminder_cmds::{max_delete_page, show_delete_page},
 | 
				
			||||||
        todo_cmds::{max_todo_page, show_todo_page},
 | 
					        todo_cmds::{max_todo_page, show_todo_page},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -222,7 +222,9 @@ WHERE channels.channel = ?",
 | 
				
			|||||||
                        .collect::<Vec<(usize, String)>>()
 | 
					                        .collect::<Vec<(usize, String)>>()
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        sqlx::query!(
 | 
					                        sqlx::query!(
 | 
				
			||||||
                            "SELECT todos.id, value FROM todos WHERE guild_id = ?",
 | 
					                            "SELECT todos.id, value FROM todos
 | 
				
			||||||
 | 
					INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
				
			||||||
 | 
					WHERE guilds.guild = ?",
 | 
				
			||||||
                            pager.guild_id,
 | 
					                            pager.guild_id,
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .fetch_all(&data.database)
 | 
					                        .fetch_all(&data.database)
 | 
				
			||||||
@@ -258,7 +260,7 @@ WHERE channels.channel = ?",
 | 
				
			|||||||
                            r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					                            r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
				
			||||||
                                .interaction_response_data(|d| {
 | 
					                                .interaction_response_data(|d| {
 | 
				
			||||||
                                    d.flags(
 | 
					                                    d.flags(
 | 
				
			||||||
                                        MessageFlags::EPHEMERAL,
 | 
					                                        InteractionApplicationCommandCallbackDataFlags::EPHEMERAL,
 | 
				
			||||||
                                    )
 | 
					                                    )
 | 
				
			||||||
                                    .content("Only the user who performed the command can use these components")
 | 
					                                    .content("Only the user who performed the command can use these components")
 | 
				
			||||||
                                })
 | 
					                                })
 | 
				
			||||||
@@ -312,7 +314,7 @@ WHERE channels.channel = ?",
 | 
				
			|||||||
                            r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					                            r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
				
			||||||
                                .interaction_response_data(|d| {
 | 
					                                .interaction_response_data(|d| {
 | 
				
			||||||
                                    d.flags(
 | 
					                                    d.flags(
 | 
				
			||||||
                                        MessageFlags::EPHEMERAL,
 | 
					                                        InteractionApplicationCommandCallbackDataFlags::EPHEMERAL,
 | 
				
			||||||
                                    )
 | 
					                                    )
 | 
				
			||||||
                                    .content("Only the user who performed the command can use these components")
 | 
					                                    .content("Only the user who performed the command can use these components")
 | 
				
			||||||
                                })
 | 
					                                })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
// todo split pager out into a single struct
 | 
					// todo split pager out into a single struct
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use poise::serenity_prelude::{
 | 
					use poise::serenity::{
 | 
				
			||||||
    builder::CreateComponents, model::application::component::ButtonStyle,
 | 
					    builder::CreateComponents, model::interactions::message_component::ButtonStyle,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serde_repr::*;
 | 
					use serde_repr::*;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ pub const DAY: u64 = 86_400;
 | 
				
			|||||||
pub const HOUR: u64 = 3_600;
 | 
					pub const HOUR: u64 = 3_600;
 | 
				
			||||||
pub const MINUTE: u64 = 60;
 | 
					pub const MINUTE: u64 = 60;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4096;
 | 
					pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000;
 | 
				
			||||||
pub const SELECT_MAX_ENTRIES: usize = 25;
 | 
					pub const SELECT_MAX_ENTRIES: usize = 25;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
					pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
				
			||||||
@@ -12,7 +12,7 @@ pub const MACRO_MAX_COMMANDS: usize = 5;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use std::{collections::HashSet, env, iter::FromIterator};
 | 
					use std::{collections::HashSet, env, iter::FromIterator};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use poise::serenity_prelude::model::prelude::AttachmentType;
 | 
					use poise::serenity::model::prelude::AttachmentType;
 | 
				
			||||||
use regex::Regex;
 | 
					use regex::Regex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
lazy_static! {
 | 
					lazy_static! {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,12 @@
 | 
				
			|||||||
use std::{collections::HashMap, env};
 | 
					use std::{collections::HashMap, env, sync::atomic::Ordering};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use log::error;
 | 
					use log::{error, info, warn};
 | 
				
			||||||
use poise::{
 | 
					use poise::{
 | 
				
			||||||
 | 
					    serenity::{model::interactions::Interaction, utils::shard_id},
 | 
				
			||||||
    serenity_prelude as serenity,
 | 
					    serenity_prelude as serenity,
 | 
				
			||||||
    serenity_prelude::{model::application::interaction::Interaction, utils::shard_id},
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{component_models::ComponentDataModel, Data, Error};
 | 
				
			||||||
    component_models::ComponentDataModel, models::guild_data::GuildData, Data, Error, THEME_COLOR,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn listener(
 | 
					pub async fn listener(
 | 
				
			||||||
    ctx: &serenity::Context,
 | 
					    ctx: &serenity::Context,
 | 
				
			||||||
@@ -19,6 +17,45 @@ pub async fn listener(
 | 
				
			|||||||
        poise::Event::Ready { .. } => {
 | 
					        poise::Event::Ready { .. } => {
 | 
				
			||||||
            ctx.set_activity(serenity::Activity::watching("for /remind")).await;
 | 
					            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)
 | 
				
			||||||
@@ -29,74 +66,11 @@ pub async fn listener(
 | 
				
			|||||||
            if *is_new {
 | 
					            if *is_new {
 | 
				
			||||||
                let guild_id = guild.id.as_u64().to_owned();
 | 
					                let guild_id = guild.id.as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                sqlx::query!("INSERT IGNORE INTO guilds (id) VALUES (?)", guild_id)
 | 
					                sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id)
 | 
				
			||||||
                    .execute(&data.database)
 | 
					                    .execute(&data.database)
 | 
				
			||||||
                    .await?;
 | 
					                    .await
 | 
				
			||||||
 | 
					                    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if let Err(e) = post_guild_count(ctx, &data.http, guild_id).await {
 | 
					 | 
				
			||||||
                    error!("DiscordBotList: {:?}", e);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let default_channel = guild.default_channel_guaranteed();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(default_channel) = default_channel {
 | 
					 | 
				
			||||||
                    default_channel
 | 
					 | 
				
			||||||
                        .send_message(&ctx, |m| {
 | 
					 | 
				
			||||||
                            m.embed(|e| {
 | 
					 | 
				
			||||||
                                e.title("Thank you for adding Reminder Bot!").description(
 | 
					 | 
				
			||||||
                                    "To get started:
 | 
					 | 
				
			||||||
• Set your timezone with `/timezone`
 | 
					 | 
				
			||||||
• Set up permissions in Server Settings 🠚 Integrations 🠚 Reminder Bot (desktop only)
 | 
					 | 
				
			||||||
• Create your first reminder with `/remind`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__Support__
 | 
					 | 
				
			||||||
If you need any support, please come and ask us! Join our [Discord](https://discord.jellywx.com).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__Updates__
 | 
					 | 
				
			||||||
To stay up to date on the latest features and fixes, join our [Discord](https://discord.jellywx.com).
 | 
					 | 
				
			||||||
",
 | 
					 | 
				
			||||||
                                ).color(*THEME_COLOR)
 | 
					 | 
				
			||||||
                            })
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await?;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        poise::Event::GuildDelete { incomplete, .. } => {
 | 
					 | 
				
			||||||
            let _ = sqlx::query!("DELETE FROM guilds WHERE id = ?", incomplete.id.0)
 | 
					 | 
				
			||||||
                .execute(&data.database)
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        poise::Event::InteractionCreate { interaction } => {
 | 
					 | 
				
			||||||
            match interaction {
 | 
					 | 
				
			||||||
                Interaction::ApplicationCommand(app_command) => {
 | 
					 | 
				
			||||||
                    if let Some(guild_id) = app_command.guild_id {
 | 
					 | 
				
			||||||
                        // check database guild exists
 | 
					 | 
				
			||||||
                        GuildData::from_guild(guild_id, &data.database).await?;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Interaction::MessageComponent(component) => {
 | 
					 | 
				
			||||||
                    let component_model =
 | 
					 | 
				
			||||||
                        ComponentDataModel::from_custom_id(&component.data.custom_id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    component_model.act(ctx, data, component).await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                _ => {}
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        _ => {}
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn post_guild_count(
 | 
					 | 
				
			||||||
    ctx: &serenity::Context,
 | 
					 | 
				
			||||||
    http: &reqwest::Client,
 | 
					 | 
				
			||||||
    guild_id: u64,
 | 
					 | 
				
			||||||
) -> Result<(), reqwest::Error> {
 | 
					 | 
				
			||||||
                if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
 | 
					                if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
 | 
				
			||||||
                    let shard_count = ctx.cache.shard_count();
 | 
					                    let shard_count = ctx.cache.shard_count();
 | 
				
			||||||
                    let current_shard_id = shard_id(guild_id, shard_count);
 | 
					                    let current_shard_id = shard_id(guild_id, shard_count);
 | 
				
			||||||
@@ -105,7 +79,9 @@ async fn post_guild_count(
 | 
				
			|||||||
                        .cache
 | 
					                        .cache
 | 
				
			||||||
                        .guilds()
 | 
					                        .guilds()
 | 
				
			||||||
                        .iter()
 | 
					                        .iter()
 | 
				
			||||||
            .filter(|g| shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id)
 | 
					                        .filter(|g| {
 | 
				
			||||||
 | 
					                            shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
                        .count() as u64;
 | 
					                        .count() as u64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    let mut hm = HashMap::new();
 | 
					                    let mut hm = HashMap::new();
 | 
				
			||||||
@@ -113,16 +89,40 @@ async fn post_guild_count(
 | 
				
			|||||||
                    hm.insert("shard_id", current_shard_id);
 | 
					                    hm.insert("shard_id", current_shard_id);
 | 
				
			||||||
                    hm.insert("shard_count", shard_count);
 | 
					                    hm.insert("shard_count", shard_count);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        http.post(
 | 
					                    let response = data
 | 
				
			||||||
            format!("https://top.gg/api/bots/{}/stats", ctx.cache.current_user_id().as_u64())
 | 
					                        .http
 | 
				
			||||||
 | 
					                        .post(
 | 
				
			||||||
 | 
					                            format!(
 | 
				
			||||||
 | 
					                                "https://top.gg/api/bots/{}/stats",
 | 
				
			||||||
 | 
					                                ctx.cache.current_user_id().as_u64()
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
                            .as_str(),
 | 
					                            .as_str(),
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .header("Authorization", token)
 | 
					                        .header("Authorization", token)
 | 
				
			||||||
                        .json(&hm)
 | 
					                        .json(&hm)
 | 
				
			||||||
                        .send()
 | 
					                        .send()
 | 
				
			||||||
        .await
 | 
					                        .await;
 | 
				
			||||||
        .map(|_| ())
 | 
					
 | 
				
			||||||
    } else {
 | 
					                    if let Err(res) = response {
 | 
				
			||||||
        Ok(())
 | 
					                        println!("DiscordBots Response: {:?}", res);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        poise::Event::GuildDelete { incomplete, .. } => {
 | 
				
			||||||
 | 
					            let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.0)
 | 
				
			||||||
 | 
					                .execute(&data.database)
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        poise::Event::InteractionCreate { interaction } => {
 | 
				
			||||||
 | 
					            if let Interaction::MessageComponent(component) = interaction {
 | 
				
			||||||
 | 
					                let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                component_model.act(ctx, data, component).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        _ => {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,9 @@
 | 
				
			|||||||
use poise::{
 | 
					use poise::serenity::model::channel::Channel;
 | 
				
			||||||
    serenity_prelude::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::RecordedCommand, Context, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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(_) =
 | 
					 | 
				
			||||||
            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 != "finish_macro" {
 | 
				
			||||||
                let mut lock = ctx.data().recording_macros.write().await;
 | 
					                let mut lock = ctx.data().recording_macros.write().await;
 | 
				
			||||||
@@ -40,7 +35,6 @@ async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    true
 | 
					    true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										89
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										89
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -18,12 +18,12 @@ use std::{
 | 
				
			|||||||
    env,
 | 
					    env,
 | 
				
			||||||
    error::Error as StdError,
 | 
					    error::Error as StdError,
 | 
				
			||||||
    fmt::{Debug, Display, Formatter},
 | 
					    fmt::{Debug, Display, Formatter},
 | 
				
			||||||
 | 
					    sync::atomic::AtomicBool,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use dotenv::dotenv;
 | 
					use dotenv::dotenv;
 | 
				
			||||||
use log::{error, warn};
 | 
					use poise::serenity::model::{
 | 
				
			||||||
use poise::serenity_prelude::model::{
 | 
					 | 
				
			||||||
    gateway::GatewayIntents,
 | 
					    gateway::GatewayIntents,
 | 
				
			||||||
    id::{GuildId, UserId},
 | 
					    id::{GuildId, UserId},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -31,7 +31,7 @@ use sqlx::{MySql, Pool};
 | 
				
			|||||||
use tokio::sync::{broadcast, broadcast::Sender, RwLock};
 | 
					use tokio::sync::{broadcast, broadcast::Sender, RwLock};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::{command_macro, info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
 | 
					    commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
 | 
				
			||||||
    consts::THEME_COLOR,
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
    event_handlers::listener,
 | 
					    event_handlers::listener,
 | 
				
			||||||
    hooks::all_checks,
 | 
					    hooks::all_checks,
 | 
				
			||||||
@@ -43,14 +43,14 @@ type Database = MySql;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type Error = Box<dyn std::error::Error + Send + Sync>;
 | 
					type Error = Box<dyn std::error::Error + Send + Sync>;
 | 
				
			||||||
type Context<'a> = poise::Context<'a, Data, Error>;
 | 
					type Context<'a> = poise::Context<'a, Data, Error>;
 | 
				
			||||||
type ApplicationContext<'a> = poise::ApplicationContext<'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<Data, Error>>>,
 | 
				
			||||||
    popular_timezones: Vec<Tz>,
 | 
					    popular_timezones: Vec<Tz>,
 | 
				
			||||||
    _broadcast: Sender<()>,
 | 
					    is_loop_running: AtomicBool,
 | 
				
			||||||
 | 
					    broadcast: Sender<()>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Debug for Data {
 | 
					impl Debug for Data {
 | 
				
			||||||
@@ -103,26 +103,13 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
            moderation_cmds::timezone(),
 | 
					            moderation_cmds::timezone(),
 | 
				
			||||||
            poise::Command {
 | 
					            poise::Command {
 | 
				
			||||||
                subcommands: vec![
 | 
					                subcommands: vec![
 | 
				
			||||||
                    moderation_cmds::set_allowed_dm(),
 | 
					                    moderation_cmds::delete_macro(),
 | 
				
			||||||
                    moderation_cmds::unset_allowed_dm(),
 | 
					                    moderation_cmds::finish_macro(),
 | 
				
			||||||
 | 
					                    moderation_cmds::list_macro(),
 | 
				
			||||||
 | 
					                    moderation_cmds::record_macro(),
 | 
				
			||||||
 | 
					                    moderation_cmds::run_macro(),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
                ..moderation_cmds::allowed_dm()
 | 
					                ..moderation_cmds::macro_base()
 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            moderation_cmds::webhook(),
 | 
					 | 
				
			||||||
            poise::Command {
 | 
					 | 
				
			||||||
                subcommands: vec![
 | 
					 | 
				
			||||||
                    command_macro::delete::delete_macro(),
 | 
					 | 
				
			||||||
                    command_macro::record::finish_macro(),
 | 
					 | 
				
			||||||
                    command_macro::list::list_macro(),
 | 
					 | 
				
			||||||
                    command_macro::record::record_macro(),
 | 
					 | 
				
			||||||
                    command_macro::run::run_macro(),
 | 
					 | 
				
			||||||
                    command_macro::migrate::migrate_macro(),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                ..command_macro::macro_base()
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            poise::Command {
 | 
					 | 
				
			||||||
                subcommands: vec![moderation_cmds::default_channel()],
 | 
					 | 
				
			||||||
                ..moderation_cmds::default()
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            reminder_cmds::pause(),
 | 
					            reminder_cmds::pause(),
 | 
				
			||||||
            reminder_cmds::offset(),
 | 
					            reminder_cmds::offset(),
 | 
				
			||||||
@@ -172,12 +159,7 @@ 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 IFNULL(timezone, 'UTC') AS timezone
 | 
					        "SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
 | 
				
			||||||
        FROM users
 | 
					 | 
				
			||||||
        WHERE timezone IS NOT NULL
 | 
					 | 
				
			||||||
        GROUP BY timezone
 | 
					 | 
				
			||||||
        ORDER BY COUNT(timezone) DESC
 | 
					 | 
				
			||||||
        LIMIT 21"
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .fetch_all(&database)
 | 
					    .fetch_all(&database)
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
@@ -186,50 +168,27 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
    .map(|t| t.timezone.parse::<Tz>().unwrap())
 | 
					    .map(|t| t.timezone.parse::<Tz>().unwrap())
 | 
				
			||||||
    .collect::<Vec<Tz>>();
 | 
					    .collect::<Vec<Tz>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    poise::Framework::builder()
 | 
					    poise::Framework::build()
 | 
				
			||||||
        .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 {
 | 
				
			||||||
                register_application_commands(ctx, framework, None).await.unwrap();
 | 
					                register_application_commands(
 | 
				
			||||||
 | 
					                    ctx,
 | 
				
			||||||
                let kill_tx = tx.clone();
 | 
					                    framework,
 | 
				
			||||||
                let kill_recv = tx.subscribe();
 | 
					                    env::var("DEBUG_GUILD")
 | 
				
			||||||
 | 
					                        .map(|inner| GuildId(inner.parse().expect("DEBUG_GUILD not valid")))
 | 
				
			||||||
                let ctx1 = ctx.clone();
 | 
					                        .ok(),
 | 
				
			||||||
                let ctx2 = ctx.clone();
 | 
					                )
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
                let pool1 = database.clone();
 | 
					                .unwrap();
 | 
				
			||||||
                let pool2 = 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");
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Ok(Data {
 | 
					                Ok(Data {
 | 
				
			||||||
                    http: reqwest::Client::new(),
 | 
					                    http: reqwest::Client::new(),
 | 
				
			||||||
                    database,
 | 
					                    database,
 | 
				
			||||||
                    popular_timezones,
 | 
					                    popular_timezones,
 | 
				
			||||||
                    recording_macros: Default::default(),
 | 
					                    recording_macros: Default::default(),
 | 
				
			||||||
                    _broadcast: tx,
 | 
					                    is_loop_running: AtomicBool::new(false),
 | 
				
			||||||
 | 
					                    broadcast: tx,
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
use chrono::NaiveDateTime;
 | 
					use chrono::NaiveDateTime;
 | 
				
			||||||
use poise::serenity_prelude::model::channel::Channel;
 | 
					use poise::serenity::model::channel::Channel;
 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct ChannelData {
 | 
					pub struct ChannelData {
 | 
				
			||||||
@@ -38,7 +38,7 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            sqlx::query!(
 | 
					            sqlx::query!(
 | 
				
			||||||
                "
 | 
					                "
 | 
				
			||||||
INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, ?)
 | 
					INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))
 | 
				
			||||||
                ",
 | 
					                ",
 | 
				
			||||||
                channel_id,
 | 
					                channel_id,
 | 
				
			||||||
                channel_name,
 | 
					                channel_name,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,7 @@
 | 
				
			|||||||
use poise::serenity_prelude::model::{
 | 
					use poise::serenity::model::{
 | 
				
			||||||
    application::interaction::application_command::CommandDataOption, id::GuildId,
 | 
					    id::GuildId, interactions::application_command::ApplicationCommandInteractionDataOption,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serde_json::Value;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{Context, Data, Error};
 | 
					use crate::{Context, Data, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -20,7 +19,7 @@ pub struct RecordedCommand<U, E> {
 | 
				
			|||||||
    #[serde(default = "default_none::<U, E>")]
 | 
					    #[serde(default = "default_none::<U, E>")]
 | 
				
			||||||
    pub action: Option<Func<U, E>>,
 | 
					    pub action: Option<Func<U, E>>,
 | 
				
			||||||
    pub command_name: String,
 | 
					    pub command_name: String,
 | 
				
			||||||
    pub options: Vec<CommandDataOption>,
 | 
					    pub options: Vec<ApplicationCommandInteractionDataOption>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct CommandMacro<U, E> {
 | 
					pub struct CommandMacro<U, E> {
 | 
				
			||||||
@@ -30,20 +29,13 @@ pub struct CommandMacro<U, E> {
 | 
				
			|||||||
    pub commands: Vec<RecordedCommand<U, E>>,
 | 
					    pub commands: Vec<RecordedCommand<U, E>>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct RawCommandMacro {
 | 
					 | 
				
			||||||
    pub guild_id: GuildId,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub description: Option<String>,
 | 
					 | 
				
			||||||
    pub commands: Value,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn guild_command_macro(
 | 
					pub async fn guild_command_macro(
 | 
				
			||||||
    ctx: &Context<'_>,
 | 
					    ctx: &Context<'_>,
 | 
				
			||||||
    name: &str,
 | 
					    name: &str,
 | 
				
			||||||
) -> Option<CommandMacro<Data, Error>> {
 | 
					) -> Option<CommandMacro<Data, Error>> {
 | 
				
			||||||
    let row = sqlx::query!(
 | 
					    let row = sqlx::query!(
 | 
				
			||||||
        "
 | 
					        "
 | 
				
			||||||
SELECT * FROM macro WHERE guild_id = ? AND name = ?
 | 
					SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?
 | 
				
			||||||
        ",
 | 
					        ",
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
        name
 | 
					        name
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,52 +0,0 @@
 | 
				
			|||||||
use sqlx::MySqlPool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::GuildId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct GuildData {
 | 
					 | 
				
			||||||
    pub id: u64,
 | 
					 | 
				
			||||||
    pub default_channel: Option<u64>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl GuildData {
 | 
					 | 
				
			||||||
    pub async fn from_guild(guild: GuildId, pool: &MySqlPool) -> Result<Self, sqlx::Error> {
 | 
					 | 
				
			||||||
        let guild_id = guild.0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Ok(row) = sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Self,
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT id, default_channel FROM guilds WHERE id = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            guild_id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(row)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            sqlx::query!(
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
INSERT IGNORE INTO guilds (id) VALUES (?)
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                guild_id
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(&pool.clone())
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Ok(Self { id: guild_id, default_channel: None })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn commit_changes(&self, pool: &MySqlPool) -> Result<(), sqlx::Error> {
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
UPDATE guilds SET default_channel = ? WHERE id = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.default_channel,
 | 
					 | 
				
			||||||
            self.id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,28 +1,28 @@
 | 
				
			|||||||
pub mod channel_data;
 | 
					pub mod channel_data;
 | 
				
			||||||
pub mod command_macro;
 | 
					pub mod command_macro;
 | 
				
			||||||
pub mod guild_data;
 | 
					 | 
				
			||||||
pub mod reminder;
 | 
					pub mod reminder;
 | 
				
			||||||
pub mod timer;
 | 
					pub mod timer;
 | 
				
			||||||
pub mod user_data;
 | 
					pub mod user_data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use log::warn;
 | 
					use poise::serenity::{async_trait, model::id::UserId};
 | 
				
			||||||
use poise::serenity_prelude::{async_trait, model::id::UserId, ChannelId};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData},
 | 
					    models::{channel_data::ChannelData, user_data::UserData},
 | 
				
			||||||
    CommandMacro, Context, Data, Error, GuildId,
 | 
					    CommandMacro, Context, Data, Error, GuildId,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[async_trait]
 | 
					#[async_trait]
 | 
				
			||||||
pub trait CtxData {
 | 
					pub trait CtxData {
 | 
				
			||||||
    async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>;
 | 
					    async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn author_data(&self) -> Result<UserData, Error>;
 | 
					    async fn author_data(&self) -> Result<UserData, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn timezone(&self) -> Tz;
 | 
					    async fn timezone(&self) -> Tz;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn channel_data(&self) -> Result<ChannelData, Error>;
 | 
					    async fn channel_data(&self) -> Result<ChannelData, Error>;
 | 
				
			||||||
    async fn guild_data(&self) -> Option<GuildData>;
 | 
					
 | 
				
			||||||
    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>;
 | 
					    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>;
 | 
				
			||||||
    async fn default_channel(&self) -> Option<ChannelId>;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[async_trait]
 | 
					#[async_trait]
 | 
				
			||||||
@@ -51,55 +51,24 @@ impl CtxData for Context<'_> {
 | 
				
			|||||||
    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
					    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
				
			||||||
        self.data().command_macros(self.guild_id().unwrap()).await
 | 
					        self.data().command_macros(self.guild_id().unwrap()).await
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn default_channel(&self) -> Option<ChannelId> {
 | 
					 | 
				
			||||||
        match self.guild_id() {
 | 
					 | 
				
			||||||
            Some(guild_id) => {
 | 
					 | 
				
			||||||
                let guild_data = GuildData::from_guild(guild_id, &self.data().database).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                match guild_data {
 | 
					 | 
				
			||||||
                    Ok(data) => data.default_channel.map(|c| ChannelId(c)),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        warn!("SQL error: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        None
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            None => None,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn guild_data(&self) -> Option<GuildData> {
 | 
					 | 
				
			||||||
        match self.guild_id() {
 | 
					 | 
				
			||||||
            Some(guild_id) => GuildData::from_guild(guild_id, &self.data().database).await.ok(),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            None => None,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Data {
 | 
					impl Data {
 | 
				
			||||||
    pub async fn command_macros(
 | 
					    pub(crate) async fn command_macros(
 | 
				
			||||||
        &self,
 | 
					        &self,
 | 
				
			||||||
        guild_id: GuildId,
 | 
					        guild_id: GuildId,
 | 
				
			||||||
    ) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
					    ) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
				
			||||||
        let rows = sqlx::query!(
 | 
					        let rows = sqlx::query!(
 | 
				
			||||||
            "SELECT name, description, commands FROM macro WHERE guild_id = ?",
 | 
					            "SELECT name, description, commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
				
			||||||
            guild_id.0
 | 
					            guild_id.0
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .fetch_all(&self.database)
 | 
					        .fetch_all(&self.database)
 | 
				
			||||||
        .await?
 | 
					        .await?.iter().map(|row| CommandMacro {
 | 
				
			||||||
        .iter()
 | 
					 | 
				
			||||||
        .map(|row| CommandMacro {
 | 
					 | 
				
			||||||
            guild_id,
 | 
					            guild_id,
 | 
				
			||||||
            name: row.name.clone(),
 | 
					            name: row.name.clone(),
 | 
				
			||||||
            description: row.description.clone(),
 | 
					            description: row.description.clone(),
 | 
				
			||||||
            commands: serde_json::from_str(&row.commands).unwrap(),
 | 
					            commands: serde_json::from_str(&row.commands).unwrap(),
 | 
				
			||||||
        })
 | 
					        }).collect();
 | 
				
			||||||
        .collect();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(rows)
 | 
					        Ok(rows)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ use std::{collections::HashSet, fmt::Display};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use chrono::{Duration, NaiveDateTime, Utc};
 | 
					use chrono::{Duration, NaiveDateTime, Utc};
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use poise::serenity_prelude::{
 | 
					use poise::serenity::{
 | 
				
			||||||
    http::CacheHttp,
 | 
					    http::CacheHttp,
 | 
				
			||||||
    model::{
 | 
					    model::{
 | 
				
			||||||
        channel::GuildChannel,
 | 
					        channel::GuildChannel,
 | 
				
			||||||
@@ -233,10 +233,6 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
                            if let Some(guild_id) = self.guild_id {
 | 
					                            if let Some(guild_id) = self.guild_id {
 | 
				
			||||||
                                if guild_id.member(&self.ctx.discord(), user).await.is_err() {
 | 
					                                if guild_id.member(&self.ctx.discord(), user).await.is_err() {
 | 
				
			||||||
                                    Err(ReminderError::InvalidTag)
 | 
					                                    Err(ReminderError::InvalidTag)
 | 
				
			||||||
                                } else if self.set_by.map_or(true, |i| i != user_data.id)
 | 
					 | 
				
			||||||
                                    && !user_data.allowed_dm
 | 
					 | 
				
			||||||
                                {
 | 
					 | 
				
			||||||
                                    Err(ReminderError::UserBlockedDm)
 | 
					 | 
				
			||||||
                                } else {
 | 
					                                } else {
 | 
				
			||||||
                                    Ok(user_data.dm_channel)
 | 
					                                    Ok(user_data.dm_channel)
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,6 @@ pub enum ReminderError {
 | 
				
			|||||||
    PastTime,
 | 
					    PastTime,
 | 
				
			||||||
    ShortInterval,
 | 
					    ShortInterval,
 | 
				
			||||||
    InvalidTag,
 | 
					    InvalidTag,
 | 
				
			||||||
    UserBlockedDm,
 | 
					 | 
				
			||||||
    DiscordError(String),
 | 
					    DiscordError(String),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,9 +30,6 @@ impl ToString for ReminderError {
 | 
				
			|||||||
            ReminderError::InvalidTag => {
 | 
					            ReminderError::InvalidTag => {
 | 
				
			||||||
                "Couldn't find a location by your tag. Your tag must be either a channel or a user (not a role)".to_string()
 | 
					                "Couldn't find a location by your tag. Your tag must be either a channel or a user (not a role)".to_string()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ReminderError::UserBlockedDm => {
 | 
					 | 
				
			||||||
                "User has DM reminders disabled".to_string()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            ReminderError::DiscordError(s) => format!("A Discord error occurred: **{}**", s),
 | 
					            ReminderError::DiscordError(s) => format!("A Discord error occurred: **{}**", s),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
use poise::serenity_prelude::model::id::ChannelId;
 | 
					use poise::serenity::model::id::ChannelId;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serde_repr::*;
 | 
					use serde_repr::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,9 +8,9 @@ use std::hash::{Hash, Hasher};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use chrono::{NaiveDateTime, TimeZone};
 | 
					use chrono::{NaiveDateTime, TimeZone};
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use poise::serenity_prelude::{
 | 
					use poise::{
 | 
				
			||||||
    model::id::{ChannelId, GuildId, UserId},
 | 
					    serenity::model::id::{ChannelId, GuildId, UserId},
 | 
				
			||||||
    Cache,
 | 
					    serenity_prelude::Cache,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::Executor;
 | 
					use sqlx::Executor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -245,7 +245,7 @@ LEFT JOIN
 | 
				
			|||||||
ON
 | 
					ON
 | 
				
			||||||
    reminders.set_by = users.id
 | 
					    reminders.set_by = users.id
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
    channels.guild_id = ?
 | 
					    channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
 | 
				
			||||||
                ",
 | 
					                ",
 | 
				
			||||||
                    guild_id.as_u64()
 | 
					                    guild_id.as_u64()
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use log::error;
 | 
					use log::error;
 | 
				
			||||||
use poise::serenity_prelude::{http::CacheHttp, model::id::UserId};
 | 
					use poise::serenity::{http::CacheHttp, model::id::UserId};
 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::consts::LOCAL_TIMEZONE;
 | 
					use crate::consts::LOCAL_TIMEZONE;
 | 
				
			||||||
@@ -10,7 +10,6 @@ pub struct UserData {
 | 
				
			|||||||
    pub user: u64,
 | 
					    pub user: u64,
 | 
				
			||||||
    pub dm_channel: u32,
 | 
					    pub dm_channel: u32,
 | 
				
			||||||
    pub timezone: String,
 | 
					    pub timezone: String,
 | 
				
			||||||
    pub allowed_dm: bool,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl UserData {
 | 
					impl UserData {
 | 
				
			||||||
@@ -22,7 +21,7 @@ impl UserData {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        match sqlx::query!(
 | 
					        match sqlx::query!(
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?
 | 
					SELECT timezone FROM users WHERE user = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
            user_id
 | 
					            user_id
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -47,7 +46,7 @@ SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?
 | 
				
			|||||||
        match sqlx::query_as_unchecked!(
 | 
					        match sqlx::query_as_unchecked!(
 | 
				
			||||||
            Self,
 | 
					            Self,
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
SELECT id, user, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone, allowed_dm FROM users WHERE user = ?
 | 
					SELECT id, user, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
            *LOCAL_TIMEZONE,
 | 
					            *LOCAL_TIMEZONE,
 | 
				
			||||||
            user_id.0
 | 
					            user_id.0
 | 
				
			||||||
@@ -84,7 +83,7 @@ INSERT INTO users (name, user, dm_channel, timezone) VALUES ('', ?, (SELECT id F
 | 
				
			|||||||
                Ok(sqlx::query_as_unchecked!(
 | 
					                Ok(sqlx::query_as_unchecked!(
 | 
				
			||||||
                    Self,
 | 
					                    Self,
 | 
				
			||||||
                    "
 | 
					                    "
 | 
				
			||||||
SELECT id, user, dm_channel, timezone, allowed_dm FROM users WHERE user = ?
 | 
					SELECT id, user, dm_channel, timezone FROM users WHERE user = ?
 | 
				
			||||||
                    ",
 | 
					                    ",
 | 
				
			||||||
                    user_id.0
 | 
					                    user_id.0
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
@@ -103,10 +102,9 @@ SELECT id, user, dm_channel, timezone, allowed_dm FROM users WHERE user = ?
 | 
				
			|||||||
    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
					    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
				
			||||||
        sqlx::query!(
 | 
					        sqlx::query!(
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
UPDATE users SET timezone = ?, allowed_dm = ? WHERE id = ?
 | 
					UPDATE users SET timezone = ? WHERE id = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
            self.timezone,
 | 
					            self.timezone,
 | 
				
			||||||
            self.allowed_dm,
 | 
					 | 
				
			||||||
            self.id
 | 
					            self.id
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .execute(pool)
 | 
					        .execute(pool)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								src/utils.rs
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/utils.rs
									
									
									
									
									
								
							@@ -1,11 +1,10 @@
 | 
				
			|||||||
use poise::{
 | 
					use poise::{
 | 
				
			||||||
    serenity_prelude as serenity,
 | 
					    serenity::{
 | 
				
			||||||
    serenity_prelude::{
 | 
					 | 
				
			||||||
        builder::CreateApplicationCommands,
 | 
					        builder::CreateApplicationCommands,
 | 
				
			||||||
        http::CacheHttp,
 | 
					        http::CacheHttp,
 | 
				
			||||||
        interaction::MessageFlags,
 | 
					 | 
				
			||||||
        model::id::{GuildId, UserId},
 | 
					        model::id::{GuildId, UserId},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    serenity_prelude as serenity,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
@@ -14,10 +13,10 @@ use crate::{
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn register_application_commands(
 | 
					pub async fn register_application_commands(
 | 
				
			||||||
    ctx: &serenity::Context,
 | 
					    ctx: &poise::serenity::client::Context,
 | 
				
			||||||
    framework: &poise::Framework<Data, Error>,
 | 
					    framework: &poise::Framework<Data, Error>,
 | 
				
			||||||
    guild_id: Option<GuildId>,
 | 
					    guild_id: Option<GuildId>,
 | 
				
			||||||
) -> Result<(), serenity::Error> {
 | 
					) -> Result<(), poise::serenity::Error> {
 | 
				
			||||||
    let mut commands_builder = CreateApplicationCommands::default();
 | 
					    let mut commands_builder = CreateApplicationCommands::default();
 | 
				
			||||||
    let commands = &framework.options().commands;
 | 
					    let commands = &framework.options().commands;
 | 
				
			||||||
    for command in commands {
 | 
					    for command in commands {
 | 
				
			||||||
@@ -28,7 +27,7 @@ pub async fn register_application_commands(
 | 
				
			|||||||
            commands_builder.add_application_command(context_menu_command);
 | 
					            commands_builder.add_application_command(context_menu_command);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    let commands_builder = poise::serenity_prelude::json::Value::Array(commands_builder.0);
 | 
					    let commands_builder = poise::serenity::json::Value::Array(commands_builder.0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if let Some(guild_id) = guild_id {
 | 
					    if let Some(guild_id) = guild_id {
 | 
				
			||||||
        ctx.http.create_guild_application_commands(guild_id.0, &commands_builder).await?;
 | 
					        ctx.http.create_guild_application_commands(guild_id.0, &commands_builder).await?;
 | 
				
			||||||
@@ -103,6 +102,6 @@ pub fn send_as_initial_response(
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if ephemeral {
 | 
					    if ephemeral {
 | 
				
			||||||
        f.flags(MessageFlags::EPHEMERAL);
 | 
					        f.flags(serenity::InteractionApplicationCommandCallbackDataFlags::EPHEMERAL);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,10 +12,10 @@ oauth2 = "4"
 | 
				
			|||||||
log = "0.4"
 | 
					log = "0.4"
 | 
				
			||||||
reqwest = "0.11"
 | 
					reqwest = "0.11"
 | 
				
			||||||
serde = { version = "1.0", features = ["derive"] }
 | 
					serde = { version = "1.0", features = ["derive"] }
 | 
				
			||||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] }
 | 
					serde_json = "1.0"
 | 
				
			||||||
 | 
					sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] }
 | 
				
			||||||
chrono = "0.4"
 | 
					chrono = "0.4"
 | 
				
			||||||
chrono-tz = "0.5"
 | 
					chrono-tz = "0.5"
 | 
				
			||||||
lazy_static = "1.4.0"
 | 
					lazy_static = "1.4.0"
 | 
				
			||||||
rand = "0.7"
 | 
					rand = "0.7"
 | 
				
			||||||
base64 = "0.13"
 | 
					base64 = "0.13"
 | 
				
			||||||
csv = "1.1"
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,8 +26,12 @@ use serenity::model::prelude::AttachmentType;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
lazy_static! {
 | 
					lazy_static! {
 | 
				
			||||||
    pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
 | 
					    pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
 | 
				
			||||||
        include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/webhook.jpg")) as &[u8],
 | 
					        include_bytes!(concat!(
 | 
				
			||||||
        "webhook.jpg",
 | 
					            env!("CARGO_MANIFEST_DIR"),
 | 
				
			||||||
 | 
					            "/../assets/",
 | 
				
			||||||
 | 
					            env!("WEBHOOK_AVATAR", "WEBHOOK_AVATAR not provided for compilation")
 | 
				
			||||||
 | 
					        )) as &[u8],
 | 
				
			||||||
 | 
					        env!("WEBHOOK_AVATAR"),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
        .into();
 | 
					        .into();
 | 
				
			||||||
    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
					    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -146,15 +146,10 @@ pub async fn initialize(
 | 
				
			|||||||
                routes::dashboard::guild::get_reminder_templates,
 | 
					                routes::dashboard::guild::get_reminder_templates,
 | 
				
			||||||
                routes::dashboard::guild::create_reminder_template,
 | 
					                routes::dashboard::guild::create_reminder_template,
 | 
				
			||||||
                routes::dashboard::guild::delete_reminder_template,
 | 
					                routes::dashboard::guild::delete_reminder_template,
 | 
				
			||||||
                routes::dashboard::guild::create_guild_reminder,
 | 
					                routes::dashboard::guild::create_reminder,
 | 
				
			||||||
                routes::dashboard::guild::get_reminders,
 | 
					                routes::dashboard::guild::get_reminders,
 | 
				
			||||||
                routes::dashboard::guild::edit_reminder,
 | 
					                routes::dashboard::guild::edit_reminder,
 | 
				
			||||||
                routes::dashboard::guild::delete_reminder,
 | 
					                routes::dashboard::guild::delete_reminder,
 | 
				
			||||||
                routes::dashboard::export::export_reminders,
 | 
					 | 
				
			||||||
                routes::dashboard::export::export_reminder_templates,
 | 
					 | 
				
			||||||
                routes::dashboard::export::export_todos,
 | 
					 | 
				
			||||||
                routes::dashboard::export::import_reminders,
 | 
					 | 
				
			||||||
                routes::dashboard::export::import_todos,
 | 
					 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .launch()
 | 
					        .launch()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
macro_rules! check_length {
 | 
					macro_rules! check_length {
 | 
				
			||||||
    ($max:ident, $field:expr) => {
 | 
					    ($max:ident, $field:expr) => {
 | 
				
			||||||
        if $field.len() > $max {
 | 
					        if $field.len() > $max {
 | 
				
			||||||
            return Err(json!({ "error": format!("{} exceeded", stringify!($max)) }));
 | 
					            return json!({ "error": format!("{} exceeded", stringify!($max)) });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    ($max:ident, $field:expr, $($fields:expr),+) => {
 | 
					    ($max:ident, $field:expr, $($fields:expr),+) => {
 | 
				
			||||||
@@ -25,7 +25,7 @@ macro_rules! check_length_opt {
 | 
				
			|||||||
macro_rules! check_url {
 | 
					macro_rules! check_url {
 | 
				
			||||||
    ($field:expr) => {
 | 
					    ($field:expr) => {
 | 
				
			||||||
        if !($field.starts_with("http://") || $field.starts_with("https://")) {
 | 
					        if !($field.starts_with("http://") || $field.starts_with("https://")) {
 | 
				
			||||||
            return Err(json!({ "error": "URL invalid" }));
 | 
					            return json!({ "error": "URL invalid" });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    ($field:expr, $($fields:expr),+) => {
 | 
					    ($field:expr, $($fields:expr),+) => {
 | 
				
			||||||
@@ -60,7 +60,7 @@ macro_rules! check_authorization {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                        match member {
 | 
					                        match member {
 | 
				
			||||||
                            Err(_) => {
 | 
					                            Err(_) => {
 | 
				
			||||||
                                return Err(json!({"error": "User not in guild"}));
 | 
					                                return json!({"error": "User not in guild"})
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            Ok(_) => {}
 | 
					                            Ok(_) => {}
 | 
				
			||||||
@@ -68,13 +68,13 @@ macro_rules! check_authorization {
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    None => {
 | 
					                    None => {
 | 
				
			||||||
                        return Err(json!({"error": "Bot not in guild"}));
 | 
					                        return json!({"error": "Bot not in guild"})
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            None => {
 | 
					            None => {
 | 
				
			||||||
                return Err(json!({"error": "User not authorized"}));
 | 
					                return json!({"error": "User not authorized"});
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -117,9 +117,3 @@ macro_rules! update_field {
 | 
				
			|||||||
        update_field!($pool, $error, $reminder.[$($fields),+]);
 | 
					        update_field!($pool, $error, $reminder.[$($fields),+]);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
macro_rules! json_err {
 | 
					 | 
				
			||||||
    ($message:expr) => {
 | 
					 | 
				
			||||||
        Err(json!({ "error": $message }))
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,430 +0,0 @@
 | 
				
			|||||||
use csv::{QuoteStyle, WriterBuilder};
 | 
					 | 
				
			||||||
use rocket::{
 | 
					 | 
				
			||||||
    http::CookieJar,
 | 
					 | 
				
			||||||
    serde::json::{json, serde_json, Json},
 | 
					 | 
				
			||||||
    State,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    model::id::{ChannelId, GuildId},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::routes::dashboard::{
 | 
					 | 
				
			||||||
    create_reminder, generate_uid, ImportBody, JsonResult, Reminder, ReminderCsv,
 | 
					 | 
				
			||||||
    ReminderTemplateCsv, TodoCsv,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/guild/<id>/export/reminders")]
 | 
					 | 
				
			||||||
pub async fn export_reminders(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonResult {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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(",");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let result = sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
                ReminderCsv,
 | 
					 | 
				
			||||||
                "SELECT
 | 
					 | 
				
			||||||
                 reminders.attachment,
 | 
					 | 
				
			||||||
                 reminders.attachment_name,
 | 
					 | 
				
			||||||
                 reminders.avatar,
 | 
					 | 
				
			||||||
                 CONCAT('#', channels.channel) AS 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.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;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            match result {
 | 
					 | 
				
			||||||
                Ok(reminders) => {
 | 
					 | 
				
			||||||
                    reminders.iter().for_each(|reminder| {
 | 
					 | 
				
			||||||
                        csv_writer.serialize(reminder).unwrap();
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    match csv_writer.into_inner() {
 | 
					 | 
				
			||||||
                        Ok(inner) => match String::from_utf8(inner) {
 | 
					 | 
				
			||||||
                            Ok(encoded) => Ok(json!({ "body": encoded })),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Err(e) => {
 | 
					 | 
				
			||||||
                                warn!("Failed to write UTF-8: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                Err(json!({"error": "Failed to write UTF-8"}))
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        Err(e) => {
 | 
					 | 
				
			||||||
                            warn!("Failed to extract CSV: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Err(json!({"error": "Failed to extract CSV"}))
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Err(e) => {
 | 
					 | 
				
			||||||
                    warn!("Failed to complete SQL query: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Err(json!({"error": "Failed to query reminders"}))
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Could not fetch channels from {}: {:?}", id, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(json!({"error": "Failed to get guild channels"}))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[put("/api/guild/<id>/export/reminders", data = "<body>")]
 | 
					 | 
				
			||||||
pub async fn import_reminders(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    body: Json<ImportBody>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonResult {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let user_id =
 | 
					 | 
				
			||||||
        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match base64::decode(&body.body) {
 | 
					 | 
				
			||||||
        Ok(body) => {
 | 
					 | 
				
			||||||
            let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for result in reader.deserialize::<ReminderCsv>() {
 | 
					 | 
				
			||||||
                match result {
 | 
					 | 
				
			||||||
                    Ok(record) => {
 | 
					 | 
				
			||||||
                        let channel_id = record.channel.split_at(1).1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        match channel_id.parse::<u64>() {
 | 
					 | 
				
			||||||
                            Ok(channel_id) => {
 | 
					 | 
				
			||||||
                                let reminder = Reminder {
 | 
					 | 
				
			||||||
                                    attachment: record.attachment,
 | 
					 | 
				
			||||||
                                    attachment_name: record.attachment_name,
 | 
					 | 
				
			||||||
                                    avatar: record.avatar,
 | 
					 | 
				
			||||||
                                    channel: channel_id,
 | 
					 | 
				
			||||||
                                    content: record.content,
 | 
					 | 
				
			||||||
                                    embed_author: record.embed_author,
 | 
					 | 
				
			||||||
                                    embed_author_url: record.embed_author_url,
 | 
					 | 
				
			||||||
                                    embed_color: record.embed_color,
 | 
					 | 
				
			||||||
                                    embed_description: record.embed_description,
 | 
					 | 
				
			||||||
                                    embed_footer: record.embed_footer,
 | 
					 | 
				
			||||||
                                    embed_footer_url: record.embed_footer_url,
 | 
					 | 
				
			||||||
                                    embed_image_url: record.embed_image_url,
 | 
					 | 
				
			||||||
                                    embed_thumbnail_url: record.embed_thumbnail_url,
 | 
					 | 
				
			||||||
                                    embed_title: record.embed_title,
 | 
					 | 
				
			||||||
                                    embed_fields: record
 | 
					 | 
				
			||||||
                                        .embed_fields
 | 
					 | 
				
			||||||
                                        .map(|s| serde_json::from_str(&s).ok())
 | 
					 | 
				
			||||||
                                        .flatten(),
 | 
					 | 
				
			||||||
                                    enabled: record.enabled,
 | 
					 | 
				
			||||||
                                    expires: record.expires,
 | 
					 | 
				
			||||||
                                    interval_seconds: record.interval_seconds,
 | 
					 | 
				
			||||||
                                    interval_months: record.interval_months,
 | 
					 | 
				
			||||||
                                    name: record.name,
 | 
					 | 
				
			||||||
                                    restartable: record.restartable,
 | 
					 | 
				
			||||||
                                    tts: record.tts,
 | 
					 | 
				
			||||||
                                    uid: generate_uid(),
 | 
					 | 
				
			||||||
                                    username: record.username,
 | 
					 | 
				
			||||||
                                    utc_time: record.utc_time,
 | 
					 | 
				
			||||||
                                };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                create_reminder(
 | 
					 | 
				
			||||||
                                    ctx.inner(),
 | 
					 | 
				
			||||||
                                    pool.inner(),
 | 
					 | 
				
			||||||
                                    GuildId(id),
 | 
					 | 
				
			||||||
                                    UserId(user_id),
 | 
					 | 
				
			||||||
                                    reminder,
 | 
					 | 
				
			||||||
                                )
 | 
					 | 
				
			||||||
                                .await?;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Err(_) => {
 | 
					 | 
				
			||||||
                                return json_err!(format!(
 | 
					 | 
				
			||||||
                                    "Failed to parse channel {}",
 | 
					 | 
				
			||||||
                                    channel_id
 | 
					 | 
				
			||||||
                                ));
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        warn!("Couldn't deserialize CSV row: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        return json_err!("Deserialize error. Aborted");
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Ok(json!({}))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(_) => {
 | 
					 | 
				
			||||||
            json_err!("Malformed base64")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/guild/<id>/export/todos")]
 | 
					 | 
				
			||||||
pub async fn export_todos(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonResult {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
        TodoCsv,
 | 
					 | 
				
			||||||
        "SELECT value, CONCAT('#', channels.channel) AS channel_id FROM todos
 | 
					 | 
				
			||||||
        LEFT JOIN channels ON todos.channel_id = channels.id
 | 
					 | 
				
			||||||
        INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
					 | 
				
			||||||
        WHERE guilds.guild = ?",
 | 
					 | 
				
			||||||
        id
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_all(pool.inner())
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Ok(todos) => {
 | 
					 | 
				
			||||||
            todos.iter().for_each(|todo| {
 | 
					 | 
				
			||||||
                csv_writer.serialize(todo).unwrap();
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            match csv_writer.into_inner() {
 | 
					 | 
				
			||||||
                Ok(inner) => match String::from_utf8(inner) {
 | 
					 | 
				
			||||||
                    Ok(encoded) => Ok(json!({ "body": encoded })),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        warn!("Failed to write UTF-8: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        json_err!("Failed to write UTF-8")
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Err(e) => {
 | 
					 | 
				
			||||||
                    warn!("Failed to extract CSV: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    json_err!("Failed to extract CSV")
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json_err!("Failed to query templates")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[put("/api/guild/<id>/export/todos", data = "<body>")]
 | 
					 | 
				
			||||||
pub async fn import_todos(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    body: Json<ImportBody>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonResult {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let channels_res = GuildId(id).channels(&ctx.inner()).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match channels_res {
 | 
					 | 
				
			||||||
        Ok(channels) => match base64::decode(&body.body) {
 | 
					 | 
				
			||||||
            Ok(body) => {
 | 
					 | 
				
			||||||
                let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let query_placeholder = "(?, (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?))";
 | 
					 | 
				
			||||||
                let mut query_params = vec![];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                for result in reader.deserialize::<TodoCsv>() {
 | 
					 | 
				
			||||||
                    match result {
 | 
					 | 
				
			||||||
                        Ok(record) => match record.channel_id {
 | 
					 | 
				
			||||||
                            Some(channel_id) => {
 | 
					 | 
				
			||||||
                                let channel_id = channel_id.split_at(1).1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                match channel_id.parse::<u64>() {
 | 
					 | 
				
			||||||
                                    Ok(channel_id) => {
 | 
					 | 
				
			||||||
                                        if channels.contains_key(&ChannelId(channel_id)) {
 | 
					 | 
				
			||||||
                                            query_params.push((record.value, Some(channel_id), id));
 | 
					 | 
				
			||||||
                                        } else {
 | 
					 | 
				
			||||||
                                            return json_err!(format!(
 | 
					 | 
				
			||||||
                                                "Invalid channel ID {}",
 | 
					 | 
				
			||||||
                                                channel_id
 | 
					 | 
				
			||||||
                                            ));
 | 
					 | 
				
			||||||
                                        }
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    Err(_) => {
 | 
					 | 
				
			||||||
                                        return json_err!(format!(
 | 
					 | 
				
			||||||
                                            "Invalid channel ID {}",
 | 
					 | 
				
			||||||
                                            channel_id
 | 
					 | 
				
			||||||
                                        ));
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            None => {
 | 
					 | 
				
			||||||
                                query_params.push((record.value, None, id));
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        Err(e) => {
 | 
					 | 
				
			||||||
                            warn!("Couldn't deserialize CSV row: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            return json_err!("Deserialize error. Aborted");
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let _ = sqlx::query!(
 | 
					 | 
				
			||||||
                    "DELETE FROM todos WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
					 | 
				
			||||||
                    id
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .execute(pool.inner())
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let query_str = format!(
 | 
					 | 
				
			||||||
                    "INSERT INTO todos (value, channel_id, guild_id) VALUES {}",
 | 
					 | 
				
			||||||
                    vec![query_placeholder].repeat(query_params.len()).join(",")
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                let mut query = sqlx::query(&query_str);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                for param in query_params {
 | 
					 | 
				
			||||||
                    query = query.bind(param.0).bind(param.1).bind(param.2);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let res = query.execute(pool.inner()).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                match res {
 | 
					 | 
				
			||||||
                    Ok(_) => Ok(json!({})),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        warn!("Couldn't execute todo query: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        json_err!("An unexpected error occured.")
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(_) => {
 | 
					 | 
				
			||||||
                json_err!("Malformed base64")
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Couldn't fetch channels for guild {}: {:?}", id, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json_err!("Couldn't fetch channels.")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/api/guild/<id>/export/reminder_templates")]
 | 
					 | 
				
			||||||
pub async fn export_reminder_templates(
 | 
					 | 
				
			||||||
    id: u64,
 | 
					 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					 | 
				
			||||||
    ctx: &State<Context>,
 | 
					 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					 | 
				
			||||||
) -> JsonResult {
 | 
					 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
        ReminderTemplateCsv,
 | 
					 | 
				
			||||||
        "SELECT
 | 
					 | 
				
			||||||
         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
 | 
					 | 
				
			||||||
        FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
					 | 
				
			||||||
        id
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .fetch_all(pool.inner())
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        Ok(templates) => {
 | 
					 | 
				
			||||||
            templates.iter().for_each(|template| {
 | 
					 | 
				
			||||||
                csv_writer.serialize(template).unwrap();
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            match csv_writer.into_inner() {
 | 
					 | 
				
			||||||
                Ok(inner) => match String::from_utf8(inner) {
 | 
					 | 
				
			||||||
                    Ok(encoded) => Ok(json!({ "body": encoded })),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        warn!("Failed to write UTF-8: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        json_err!("Failed to write UTF-8")
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Err(e) => {
 | 
					 | 
				
			||||||
                    warn!("Failed to extract CSV: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    json_err!("Failed to extract CSV")
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            json_err!("Failed to query templates")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,8 +1,10 @@
 | 
				
			|||||||
use std::env;
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use base64;
 | 
				
			||||||
 | 
					use chrono::Utc;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
    serde::json::{json, Json},
 | 
					    serde::json::{json, Json, Value as JsonValue},
 | 
				
			||||||
    State,
 | 
					    State,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serde::Serialize;
 | 
					use serde::Serialize;
 | 
				
			||||||
@@ -16,14 +18,16 @@ use serenity::{
 | 
				
			|||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
 | 
					    check_guild_subscription, check_subscription,
 | 
				
			||||||
    consts::{
 | 
					    consts::{
 | 
				
			||||||
        MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
 | 
					        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_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH,
 | 
				
			||||||
        MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH,
 | 
					        MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH,
 | 
				
			||||||
 | 
					        MIN_INTERVAL,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    routes::dashboard::{
 | 
					    routes::dashboard::{
 | 
				
			||||||
        create_database_channel, create_reminder, template_name_default, DeleteReminder,
 | 
					        create_database_channel, generate_uid, name_default, template_name_default, DeleteReminder,
 | 
				
			||||||
        DeleteReminderTemplate, JsonResult, PatchReminder, Reminder, ReminderTemplate,
 | 
					        DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,7 +44,7 @@ pub async fn get_guild_patreon(
 | 
				
			|||||||
    id: u64,
 | 
					    id: u64,
 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
    ctx: &State<Context>,
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonValue {
 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match GuildId(id).to_guild_cached(ctx.inner()) {
 | 
					    match GuildId(id).to_guild_cached(ctx.inner()) {
 | 
				
			||||||
@@ -55,10 +59,12 @@ pub async fn get_guild_patreon(
 | 
				
			|||||||
                    .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
 | 
					                    .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Ok(json!({ "patreon": patreon }))
 | 
					            json!({ "patreon": patreon })
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        None => json_err!("Bot not in guild"),
 | 
					        None => {
 | 
				
			||||||
 | 
					            json!({"error": "Bot not in guild"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -67,7 +73,7 @@ pub async fn get_guild_channels(
 | 
				
			|||||||
    id: u64,
 | 
					    id: u64,
 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
    ctx: &State<Context>,
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonValue {
 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match GuildId(id).to_guild_cached(ctx.inner()) {
 | 
					    match GuildId(id).to_guild_cached(ctx.inner()) {
 | 
				
			||||||
@@ -91,10 +97,12 @@ pub async fn get_guild_channels(
 | 
				
			|||||||
                })
 | 
					                })
 | 
				
			||||||
                .collect::<Vec<ChannelInfo>>();
 | 
					                .collect::<Vec<ChannelInfo>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Ok(json!(channel_info))
 | 
					            json!(channel_info)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        None => json_err!("Bot not in guild"),
 | 
					        None => {
 | 
				
			||||||
 | 
					            json!({"error": "Bot not in guild"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -105,7 +113,7 @@ struct RoleInfo {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[get("/api/guild/<id>/roles")]
 | 
					#[get("/api/guild/<id>/roles")]
 | 
				
			||||||
pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
 | 
					pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonValue {
 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let roles_res = ctx.cache.guild_roles(id);
 | 
					    let roles_res = ctx.cache.guild_roles(id);
 | 
				
			||||||
@@ -117,12 +125,12 @@ pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Conte
 | 
				
			|||||||
                .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() })
 | 
					                .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() })
 | 
				
			||||||
                .collect::<Vec<RoleInfo>>();
 | 
					                .collect::<Vec<RoleInfo>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Ok(json!(roles))
 | 
					            json!(roles)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        None => {
 | 
					        None => {
 | 
				
			||||||
            warn!("Could not fetch roles from {}", id);
 | 
					            warn!("Could not fetch roles from {}", id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            json_err!("Could not get roles")
 | 
					            json!({"error": "Could not get roles"})
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -133,7 +141,7 @@ pub async fn get_reminder_templates(
 | 
				
			|||||||
    cookies: &CookieJar<'_>,
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
    ctx: &State<Context>,
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonValue {
 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match sqlx::query_as_unchecked!(
 | 
					    match sqlx::query_as_unchecked!(
 | 
				
			||||||
@@ -144,11 +152,13 @@ pub async fn get_reminder_templates(
 | 
				
			|||||||
    .fetch_all(pool.inner())
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Ok(templates) => Ok(json!(templates)),
 | 
					        Ok(templates) => {
 | 
				
			||||||
 | 
					            json!(templates)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
					            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            json_err!("Could not get templates")
 | 
					            json!({"error": "Could not get templates"})
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -160,7 +170,7 @@ pub async fn create_reminder_template(
 | 
				
			|||||||
    cookies: &CookieJar<'_>,
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
    ctx: &State<Context>,
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonValue {
 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // validate lengths
 | 
					    // validate lengths
 | 
				
			||||||
@@ -244,12 +254,12 @@ pub async fn create_reminder_template(
 | 
				
			|||||||
    .await
 | 
					    .await
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Ok(_) => {
 | 
					        Ok(_) => {
 | 
				
			||||||
            Ok(json!({}))
 | 
					            json!({})
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
					            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            json_err!("Could not get templates")
 | 
					            json!({"error": "Could not get templates"})
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -261,7 +271,7 @@ pub async fn delete_reminder_template(
 | 
				
			|||||||
    cookies: &CookieJar<'_>,
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
    ctx: &State<Context>,
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonValue {
 | 
				
			||||||
    check_authorization!(cookies, ctx.inner(), id);
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match sqlx::query!(
 | 
					    match sqlx::query!(
 | 
				
			||||||
@@ -272,41 +282,230 @@ pub async fn delete_reminder_template(
 | 
				
			|||||||
    .await
 | 
					    .await
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Ok(_) => {
 | 
					        Ok(_) => {
 | 
				
			||||||
            Ok(json!({}))
 | 
					            json!({})
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            warn!("Could not delete template from {}: {:?}", id, e);
 | 
					            warn!("Could not delete template from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            json_err!("Could not delete template")
 | 
					            json!({"error": "Could not delete template"})
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[post("/api/guild/<id>/reminders", data = "<reminder>")]
 | 
					#[post("/api/guild/<id>/reminders", data = "<reminder>")]
 | 
				
			||||||
pub async fn create_guild_reminder(
 | 
					pub async fn create_reminder(
 | 
				
			||||||
    id: u64,
 | 
					    id: u64,
 | 
				
			||||||
    reminder: Json<Reminder>,
 | 
					    reminder: Json<Reminder>,
 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
    serenity_context: &State<Context>,
 | 
					    serenity_context: &State<Context>,
 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonValue {
 | 
				
			||||||
    check_authorization!(cookies, serenity_context.inner(), id);
 | 
					    check_authorization!(cookies, serenity_context.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let user_id =
 | 
					    let user_id =
 | 
				
			||||||
        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
					        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    create_reminder(
 | 
					    // 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(),
 | 
					        serenity_context.inner(),
 | 
				
			||||||
 | 
					        ChannelId(reminder.channel),
 | 
				
			||||||
        pool.inner(),
 | 
					        pool.inner(),
 | 
				
			||||||
        GuildId(id),
 | 
					 | 
				
			||||||
        UserId(user_id),
 | 
					 | 
				
			||||||
        reminder.into_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
 | 
					    .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")]
 | 
					#[get("/api/guild/<id>/reminders")]
 | 
				
			||||||
pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySql>>) -> JsonResult {
 | 
					pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySql>>) -> JsonValue {
 | 
				
			||||||
    let channels_res = GuildId(id).channels(&ctx.inner()).await;
 | 
					    let channels_res = GuildId(id).channels(&ctx.inner()).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match channels_res {
 | 
					    match channels_res {
 | 
				
			||||||
@@ -353,17 +552,17 @@ pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySq
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
            .fetch_all(pool.inner())
 | 
					            .fetch_all(pool.inner())
 | 
				
			||||||
            .await
 | 
					            .await
 | 
				
			||||||
            .map(|r| Ok(json!(r)))
 | 
					            .map(|r| json!(r))
 | 
				
			||||||
            .unwrap_or_else(|e| {
 | 
					            .unwrap_or_else(|e| {
 | 
				
			||||||
                warn!("Failed to complete SQL query: {:?}", e);
 | 
					                warn!("Failed to complete SQL query: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                json_err!("Could not load reminders")
 | 
					                json!({"error": "Could not load reminders"})
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            warn!("Could not fetch channels from {}: {:?}", id, e);
 | 
					            warn!("Could not fetch channels from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Ok(json!([]))
 | 
					            json!([])
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -374,7 +573,7 @@ pub async fn edit_reminder(
 | 
				
			|||||||
    reminder: Json<PatchReminder>,
 | 
					    reminder: Json<PatchReminder>,
 | 
				
			||||||
    serenity_context: &State<Context>,
 | 
					    serenity_context: &State<Context>,
 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonValue {
 | 
				
			||||||
    let mut error = vec![];
 | 
					    let mut error = vec![];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    update_field!(pool.inner(), error, reminder.[
 | 
					    update_field!(pool.inner(), error, reminder.[
 | 
				
			||||||
@@ -415,7 +614,7 @@ pub async fn edit_reminder(
 | 
				
			|||||||
                        reminder.channel, id
 | 
					                        reminder.channel, id
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    return Err(json!({"error": "Channel not found"}));
 | 
					                    return json!({"error": "Channel not found"});
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let channel = create_database_channel(
 | 
					                let channel = create_database_channel(
 | 
				
			||||||
@@ -428,9 +627,7 @@ pub async fn edit_reminder(
 | 
				
			|||||||
                if let Err(e) = channel {
 | 
					                if let Err(e) = channel {
 | 
				
			||||||
                    warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
					                    warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    return Err(
 | 
					                    return json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"});
 | 
				
			||||||
                        json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let channel = channel.unwrap();
 | 
					                let channel = channel.unwrap();
 | 
				
			||||||
@@ -458,7 +655,7 @@ pub async fn edit_reminder(
 | 
				
			|||||||
                    reminder.channel, id
 | 
					                    reminder.channel, id
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return Err(json!({"error": "Channel not found"}));
 | 
					                return json!({"error": "Channel not found"});
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -498,12 +695,12 @@ pub async fn edit_reminder(
 | 
				
			|||||||
    .fetch_one(pool.inner())
 | 
					    .fetch_one(pool.inner())
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})),
 | 
					        Ok(reminder) => json!({"reminder": reminder, "errors": error}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            warn!("Error exiting `edit_reminder': {:?}", e);
 | 
					            warn!("Error exiting `edit_reminder': {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]}))
 | 
					            json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]})
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -512,17 +709,19 @@ pub async fn edit_reminder(
 | 
				
			|||||||
pub async fn delete_reminder(
 | 
					pub async fn delete_reminder(
 | 
				
			||||||
    reminder: Json<DeleteReminder>,
 | 
					    reminder: Json<DeleteReminder>,
 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonValue {
 | 
				
			||||||
    match sqlx::query!("DELETE FROM reminders WHERE uid = ?", reminder.uid)
 | 
					    match sqlx::query!("DELETE FROM reminders WHERE uid = ?", reminder.uid)
 | 
				
			||||||
        .execute(pool.inner())
 | 
					        .execute(pool.inner())
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Ok(_) => Ok(json!({})),
 | 
					        Ok(_) => {
 | 
				
			||||||
 | 
					            json!({})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            warn!("Error in `delete_reminder`: {:?}", e);
 | 
					            warn!("Error in `delete_reminder`: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Err(json!({"error": "Could not delete reminder"}))
 | 
					            json!({"error": "Could not delete reminder"})
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,37 +1,21 @@
 | 
				
			|||||||
use std::collections::HashMap;
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono::{naive::NaiveDateTime, Utc};
 | 
					use chrono::naive::NaiveDateTime;
 | 
				
			||||||
use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
					use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{http::CookieJar, response::Redirect};
 | 
				
			||||||
    http::CookieJar,
 | 
					 | 
				
			||||||
    response::Redirect,
 | 
					 | 
				
			||||||
    serde::json::{json, Value as JsonValue},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use rocket_dyn_templates::Template;
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serenity::{
 | 
					use serenity::{http::Http, model::id::ChannelId};
 | 
				
			||||||
    client::Context,
 | 
					use sqlx::{types::Json, Executor};
 | 
				
			||||||
    http::Http,
 | 
					 | 
				
			||||||
    model::id::{ChannelId, GuildId, UserId},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::{types::Json, Executor, MySql, Pool};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    check_guild_subscription, check_subscription,
 | 
					    consts::{CHARACTERS, DEFAULT_AVATAR},
 | 
				
			||||||
    consts::{
 | 
					 | 
				
			||||||
        CHARACTERS, DAY, DEFAULT_AVATAR, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH,
 | 
					 | 
				
			||||||
        MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH,
 | 
					 | 
				
			||||||
        MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH,
 | 
					 | 
				
			||||||
        MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Database, Error,
 | 
					    Database, Error,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub mod export;
 | 
					 | 
				
			||||||
pub mod guild;
 | 
					pub mod guild;
 | 
				
			||||||
pub mod user;
 | 
					pub mod user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub type JsonResult = Result<JsonValue, JsonValue>;
 | 
					 | 
				
			||||||
type Unset<T> = Option<T>;
 | 
					type Unset<T> = Option<T>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn name_default() -> String {
 | 
					fn name_default() -> String {
 | 
				
			||||||
@@ -76,28 +60,6 @@ pub struct ReminderTemplate {
 | 
				
			|||||||
    username: Option<String>,
 | 
					    username: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct ReminderTemplateCsv {
 | 
					 | 
				
			||||||
    #[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<String>,
 | 
					 | 
				
			||||||
    tts: bool,
 | 
					 | 
				
			||||||
    username: Option<String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
pub struct DeleteReminderTemplate {
 | 
					pub struct DeleteReminderTemplate {
 | 
				
			||||||
    id: u32,
 | 
					    id: u32,
 | 
				
			||||||
@@ -143,36 +105,6 @@ pub struct Reminder {
 | 
				
			|||||||
    utc_time: NaiveDateTime,
 | 
					    utc_time: NaiveDateTime,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct ReminderCsv {
 | 
					 | 
				
			||||||
    #[serde(with = "base64s")]
 | 
					 | 
				
			||||||
    attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    attachment_name: Option<String>,
 | 
					 | 
				
			||||||
    avatar: Option<String>,
 | 
					 | 
				
			||||||
    channel: 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<String>,
 | 
					 | 
				
			||||||
    enabled: bool,
 | 
					 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    interval_seconds: Option<u32>,
 | 
					 | 
				
			||||||
    interval_months: Option<u32>,
 | 
					 | 
				
			||||||
    #[serde(default = "name_default")]
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
    restartable: bool,
 | 
					 | 
				
			||||||
    tts: bool,
 | 
					 | 
				
			||||||
    username: Option<String>,
 | 
					 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
pub struct PatchReminder {
 | 
					pub struct PatchReminder {
 | 
				
			||||||
    uid: String,
 | 
					    uid: String,
 | 
				
			||||||
@@ -288,225 +220,13 @@ pub struct DeleteReminder {
 | 
				
			|||||||
    uid: String,
 | 
					    uid: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Deserialize)]
 | 
					 | 
				
			||||||
pub struct ImportBody {
 | 
					 | 
				
			||||||
    body: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct TodoCsv {
 | 
					 | 
				
			||||||
    value: String,
 | 
					 | 
				
			||||||
    channel_id: Option<String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn create_reminder(
 | 
					 | 
				
			||||||
    ctx: &Context,
 | 
					 | 
				
			||||||
    pool: &Pool<MySql>,
 | 
					 | 
				
			||||||
    guild_id: GuildId,
 | 
					 | 
				
			||||||
    user_id: UserId,
 | 
					 | 
				
			||||||
    reminder: Reminder,
 | 
					 | 
				
			||||||
) -> JsonResult {
 | 
					 | 
				
			||||||
    // validate channel
 | 
					 | 
				
			||||||
    let channel = ChannelId(reminder.channel).to_channel_cached(&ctx);
 | 
					 | 
				
			||||||
    let channel_exists = channel.is_some();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let channel_matches_guild =
 | 
					 | 
				
			||||||
        channel.map_or(false, |c| c.guild().map_or(false, |c| c.guild_id == guild_id));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if !channel_matches_guild || !channel_exists {
 | 
					 | 
				
			||||||
        warn!(
 | 
					 | 
				
			||||||
            "Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})",
 | 
					 | 
				
			||||||
            reminder.channel, guild_id, channel_exists
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Err(json!({"error": "Channel not found"}));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let channel = create_database_channel(&ctx, ChannelId(reminder.channel), pool).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if let Err(e) = channel {
 | 
					 | 
				
			||||||
        warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Err(
 | 
					 | 
				
			||||||
            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 Err(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 Err(json!({"error": "Interval too short"}));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // check patreon if necessary
 | 
					 | 
				
			||||||
    if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() {
 | 
					 | 
				
			||||||
        if !check_guild_subscription(&ctx, guild_id).await
 | 
					 | 
				
			||||||
            && !check_subscription(&ctx, user_id).await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return Err(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)
 | 
					 | 
				
			||||||
    .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)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .map(|r| Ok(json!(r)))
 | 
					 | 
				
			||||||
        .unwrap_or_else(|e| {
 | 
					 | 
				
			||||||
            warn!("Failed to complete SQL query: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(json!({"error": "Could not load reminder"}))
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            warn!("Error in `create_reminder`: Could not execute query: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(json!({"error": "Unknown error"}))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn create_database_channel(
 | 
					async fn create_database_channel(
 | 
				
			||||||
    ctx: impl AsRef<Http>,
 | 
					    ctx: impl AsRef<Http>,
 | 
				
			||||||
    channel: ChannelId,
 | 
					    channel: ChannelId,
 | 
				
			||||||
    pool: impl Executor<'_, Database = Database> + Copy,
 | 
					    pool: impl Executor<'_, Database = Database> + Copy,
 | 
				
			||||||
) -> Result<u32, crate::Error> {
 | 
					) -> Result<u32, crate::Error> {
 | 
				
			||||||
 | 
					    println!("{:?}", channel);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let row =
 | 
					    let row =
 | 
				
			||||||
        sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0)
 | 
					        sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0)
 | 
				
			||||||
            .fetch_one(pool)
 | 
					            .fetch_one(pool)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,10 +61,7 @@ pub async fn get_user_info(
 | 
				
			|||||||
            .member(&ctx.inner(), user_id)
 | 
					            .member(&ctx.inner(), user_id)
 | 
				
			||||||
            .await;
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let timezone = sqlx::query!(
 | 
					        let timezone = sqlx::query!("SELECT timezone FROM users WHERE user = ?", user_id)
 | 
				
			||||||
            "SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?",
 | 
					 | 
				
			||||||
            user_id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
            .fetch_one(pool.inner())
 | 
					            .fetch_one(pool.inner())
 | 
				
			||||||
            .await
 | 
					            .await
 | 
				
			||||||
            .map_or(None, |q| Some(q.timezone));
 | 
					            .map_or(None, |q| Some(q.timezone));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,7 @@ pub async fn discord_login(
 | 
				
			|||||||
        // Set the desired scopes.
 | 
					        // Set the desired scopes.
 | 
				
			||||||
        .add_scope(Scope::new("identify".to_string()))
 | 
					        .add_scope(Scope::new("identify".to_string()))
 | 
				
			||||||
        .add_scope(Scope::new("guilds".to_string()))
 | 
					        .add_scope(Scope::new("guilds".to_string()))
 | 
				
			||||||
 | 
					        .add_scope(Scope::new("email".to_string()))
 | 
				
			||||||
        // Set the PKCE code challenge.
 | 
					        // Set the PKCE code challenge.
 | 
				
			||||||
        .set_pkce_challenge(pkce_challenge)
 | 
					        .set_pkce_challenge(pkce_challenge)
 | 
				
			||||||
        .url();
 | 
					        .url();
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 44 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 40 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 44 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 18 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 20 KiB  | 
@@ -12,10 +12,6 @@ const $createTemplateBtn = $createReminder.querySelector("button#createTemplate"
 | 
				
			|||||||
const $loadTemplateBtn = document.querySelector("button#load-template");
 | 
					const $loadTemplateBtn = document.querySelector("button#load-template");
 | 
				
			||||||
const $deleteTemplateBtn = document.querySelector("button#delete-template");
 | 
					const $deleteTemplateBtn = document.querySelector("button#delete-template");
 | 
				
			||||||
const $templateSelect = document.querySelector("select#templateSelect");
 | 
					const $templateSelect = document.querySelector("select#templateSelect");
 | 
				
			||||||
const $exportBtn = document.querySelector("button#export-data");
 | 
					 | 
				
			||||||
const $importBtn = document.querySelector("button#import-data");
 | 
					 | 
				
			||||||
const $downloader = document.querySelector("a#downloader");
 | 
					 | 
				
			||||||
const $uploader = document.querySelector("input#uploader");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
let channels = [];
 | 
					let channels = [];
 | 
				
			||||||
let guildNames = {};
 | 
					let guildNames = {};
 | 
				
			||||||
@@ -674,39 +670,6 @@ function has_source(string) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$uploader.addEventListener("change", (ev) => {
 | 
					 | 
				
			||||||
    const urlTail = document.querySelector('input[name="exportSelect"]:checked').value;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    new Promise((resolve) => {
 | 
					 | 
				
			||||||
        let fileReader = new FileReader();
 | 
					 | 
				
			||||||
        fileReader.onload = (e) => resolve(fileReader.result);
 | 
					 | 
				
			||||||
        fileReader.readAsDataURL($uploader.files[0]);
 | 
					 | 
				
			||||||
    }).then((dataUrl) => {
 | 
					 | 
				
			||||||
        fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, {
 | 
					 | 
				
			||||||
            method: "PUT",
 | 
					 | 
				
			||||||
            body: JSON.stringify({ body: dataUrl.split(",")[1] }),
 | 
					 | 
				
			||||||
        }).then(() => {
 | 
					 | 
				
			||||||
            delete $uploader.files[0];
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$importBtn.addEventListener("click", () => {
 | 
					 | 
				
			||||||
    $uploader.click();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$exportBtn.addEventListener("click", () => {
 | 
					 | 
				
			||||||
    const urlTail = document.querySelector('input[name="exportSelect"]:checked').value;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`)
 | 
					 | 
				
			||||||
        .then((response) => response.json())
 | 
					 | 
				
			||||||
        .then((data) => {
 | 
					 | 
				
			||||||
            $downloader.href =
 | 
					 | 
				
			||||||
                "data:text/plain;charset=utf-8," + encodeURIComponent(data.body);
 | 
					 | 
				
			||||||
            $downloader.click();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$createReminderBtn.addEventListener("click", async () => {
 | 
					$createReminderBtn.addEventListener("click", async () => {
 | 
				
			||||||
    $createReminderBtn.querySelector("span.icon > i").classList = [
 | 
					    $createReminderBtn.querySelector("span.icon > i").classList = [
 | 
				
			||||||
        "fas fa-spinner fa-spin",
 | 
					        "fas fa-spinner fa-spin",
 | 
				
			||||||
@@ -871,7 +834,7 @@ document.addEventListener("remindersLoaded", () => {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const fileInput = document.querySelectorAll("input.file-input[type=file]");
 | 
					    const fileInput = document.querySelectorAll("input[type=file]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fileInput.forEach((element) => {
 | 
					    fileInput.forEach((element) => {
 | 
				
			||||||
        element.addEventListener("change", () => {
 | 
					        element.addEventListener("change", () => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -177,41 +177,38 @@
 | 
				
			|||||||
        <section class="modal-card-body">
 | 
					        <section class="modal-card-body">
 | 
				
			||||||
            <div class="control">
 | 
					            <div class="control">
 | 
				
			||||||
                <div class="field">
 | 
					                <div class="field">
 | 
				
			||||||
                    <label>
 | 
					                    <input type="checkbox" class="default-width">
 | 
				
			||||||
                        <input type="radio" class="default-width" name="exportSelect" value="reminders" checked>
 | 
					                    <label>Reminders</label>
 | 
				
			||||||
                        Reminders
 | 
					 | 
				
			||||||
                    </label>
 | 
					 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="control">
 | 
					            <div class="control">
 | 
				
			||||||
                <div class="field">
 | 
					                <div class="field">
 | 
				
			||||||
                    <label>
 | 
					                    <input type="checkbox" class="default-width">
 | 
				
			||||||
                        <input type="radio" class="default-width" name="exportSelect" value="todos">
 | 
					                    <label>Todo Lists</label>
 | 
				
			||||||
                        Todo Lists
 | 
					 | 
				
			||||||
                    </label>
 | 
					 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="control">
 | 
					            <div class="control">
 | 
				
			||||||
                <div class="field">
 | 
					                <div class="field">
 | 
				
			||||||
                    <label>
 | 
					                    <input type="checkbox" class="default-width">
 | 
				
			||||||
                        <input type="radio" class="default-width" name="exportSelect" value="reminder_templates">
 | 
					                    <label>Timers</label>
 | 
				
			||||||
                        Reminder templates
 | 
					                </div>
 | 
				
			||||||
                    </label>
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="control">
 | 
				
			||||||
 | 
					                <div class="field">
 | 
				
			||||||
 | 
					                    <input type="checkbox" class="default-width">
 | 
				
			||||||
 | 
					                    <label>Reminder templates</label>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="control">
 | 
				
			||||||
 | 
					                <div class="field">
 | 
				
			||||||
 | 
					                    <input type="checkbox" class="default-width">
 | 
				
			||||||
 | 
					                    <label>Macros</label>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <br>
 | 
					 | 
				
			||||||
            <div class="has-text-centered">
 | 
					            <div class="has-text-centered">
 | 
				
			||||||
                <div style="color: red; font-weight: bold;">
 | 
					 | 
				
			||||||
                    By selecting "Import", you understand that this will overwrite existing data.
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <div style="color: red">
 | 
					 | 
				
			||||||
                    Please first read the <a href="/help/iemanager">support page</a>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <button class="button is-success is-outlined" id="import-data">Import Data</button>
 | 
					                <button class="button is-success is-outlined" id="import-data">Import Data</button>
 | 
				
			||||||
                <button class="button is-success" id="export-data">Export Data</button>
 | 
					                <button class="button is-success" id="export-data">Export Data</button>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <a id="downloader" download="export.csv" class="is-hidden"></a>
 | 
					 | 
				
			||||||
            <input id="uploader" type="file" hidden></input>
 | 
					 | 
				
			||||||
        </section>
 | 
					        </section>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <button class="modal-close is-large close-modal" aria-label="close"></button>
 | 
					    <button class="modal-close is-large close-modal" aria-label="close"></button>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,5 +5,5 @@
 | 
				
			|||||||
    {% set show_contact = True %}
 | 
					    {% set show_contact = True %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {% set page_title = "An Error Has Occurred" %}
 | 
					    {% set page_title = "An Error Has Occurred" %}
 | 
				
			||||||
    {% set page_subtitle = "A server error has occurred. Please retry, or ask in our Discord." %}
 | 
					    {% set page_subtitle = "A server error has occurred. Please contact me and I will try and resolve this" %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,7 +49,7 @@
 | 
				
			|||||||
        <div class="container">
 | 
					        <div class="container">
 | 
				
			||||||
            <h2 class="title">Who your data is shared with</h2>
 | 
					            <h2 class="title">Who your data is shared with</h2>
 | 
				
			||||||
            <p class="is-size-5 pl-6">
 | 
					            <p class="is-size-5 pl-6">
 | 
				
			||||||
                Your data is also guarded by the privacy policies of <strong>MEGA</strong>, our backup provider, and
 | 
					                Your data may also be guarded by the privacy policies of <strong>MEGA</strong>, our backup provider, and
 | 
				
			||||||
                <strong>Hetzner</strong>, our hosting provider.
 | 
					                <strong>Hetzner</strong>, our hosting provider.
 | 
				
			||||||
            </p>
 | 
					            </p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@@ -68,7 +68,7 @@
 | 
				
			|||||||
                <br>
 | 
					                <br>
 | 
				
			||||||
                <br>
 | 
					                <br>
 | 
				
			||||||
                Reminders deleted with <strong>/del</strong> or via the dashboard are removed from the live database
 | 
					                Reminders deleted with <strong>/del</strong> or via the dashboard are removed from the live database
 | 
				
			||||||
                instantly, but may persist in backups for up to a year.
 | 
					                instantly, but may persist in backups.
 | 
				
			||||||
            </p>
 | 
					            </p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,75 +14,13 @@
 | 
				
			|||||||
            <div class="container">
 | 
					            <div class="container">
 | 
				
			||||||
                <p class="title">Export your data</p>
 | 
					                <p class="title">Export your data</p>
 | 
				
			||||||
                <p class="content">
 | 
					                <p class="content">
 | 
				
			||||||
                    You can export data associated with your server from the dashboard. The data will export as a CSV
 | 
					                    You can create reminders with the <code>/remind</code> command.
 | 
				
			||||||
                    file. The CSV file can then be edited and imported to bulk edit server data.
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    Fill out the "time" and "content" fields. If you wish, press on "Optional" to view other options
 | 
				
			||||||
 | 
					                    for the reminder.
 | 
				
			||||||
                </p>
 | 
					                </p>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <section class="hero is-small">
 | 
					 | 
				
			||||||
        <div class="hero-body">
 | 
					 | 
				
			||||||
            <div class="container">
 | 
					 | 
				
			||||||
                <p class="title">Import data</p>
 | 
					 | 
				
			||||||
                <p class="content">
 | 
					 | 
				
			||||||
                    You can import previous exports or modified exports. When importing a file, <strong>existing data
 | 
					 | 
				
			||||||
                    will be overwritten</strong>.
 | 
					 | 
				
			||||||
                </p>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </section>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <section class="hero is-small">
 | 
					 | 
				
			||||||
        <div class="hero-body">
 | 
					 | 
				
			||||||
            <div class="container content">
 | 
					 | 
				
			||||||
                <p class="title">Edit your data</p>
 | 
					 | 
				
			||||||
                <p>
 | 
					 | 
				
			||||||
                    The CSV can be edited either as a text file or in a spreadsheet editor such as LibreOffice Calc. To
 | 
					 | 
				
			||||||
                    set up LibreOffice Calc for editing, do the following:
 | 
					 | 
				
			||||||
                </p>
 | 
					 | 
				
			||||||
                <ol>
 | 
					 | 
				
			||||||
                    <li>
 | 
					 | 
				
			||||||
                        Export data from dashboard.
 | 
					 | 
				
			||||||
                        <figure>
 | 
					 | 
				
			||||||
                            <img src="/static/img/support/iemanager/select_export.png" alt="Selecting export button">
 | 
					 | 
				
			||||||
                        </figure>
 | 
					 | 
				
			||||||
                    </li>
 | 
					 | 
				
			||||||
                    <li>
 | 
					 | 
				
			||||||
                        Open the file in LibreOffice. <strong>During the import dialogue, select "Format quoted field as text".</strong>
 | 
					 | 
				
			||||||
                        <figure>
 | 
					 | 
				
			||||||
                            <img src="/static/img/support/iemanager/format_text.png" alt="Selecting format button">
 | 
					 | 
				
			||||||
                        </figure>
 | 
					 | 
				
			||||||
                    </li>
 | 
					 | 
				
			||||||
                    <li>
 | 
					 | 
				
			||||||
                        Make edits to the spreadsheet. You can add, edit, and remove rows for reminders. Don't remove the title row.
 | 
					 | 
				
			||||||
                        <figure>
 | 
					 | 
				
			||||||
                            <img src="/static/img/support/iemanager/edit_spreadsheet.png" alt="Editing spreadsheet">
 | 
					 | 
				
			||||||
                        </figure>
 | 
					 | 
				
			||||||
                    </li>
 | 
					 | 
				
			||||||
                    <li>
 | 
					 | 
				
			||||||
                        Save the edited CSV file and import it on the dashboard.
 | 
					 | 
				
			||||||
                        <figure>
 | 
					 | 
				
			||||||
                            <img src="/static/img/support/iemanager/import.png" alt="Import new reminders">
 | 
					 | 
				
			||||||
                        </figure>
 | 
					 | 
				
			||||||
                    </li>
 | 
					 | 
				
			||||||
                </ol>
 | 
					 | 
				
			||||||
                Other spreadsheet tools can also be used to edit exports, as long as they are properly configured:
 | 
					 | 
				
			||||||
                <ul>
 | 
					 | 
				
			||||||
                    <li>
 | 
					 | 
				
			||||||
                        <strong>Google Sheets</strong>: Create a new blank spreadsheet. Select <strong>File >> Import >> Upload >> export.csv</strong>.
 | 
					 | 
				
			||||||
                        Use the following import settings:
 | 
					 | 
				
			||||||
                        <figure>
 | 
					 | 
				
			||||||
                            <img src="/static/img/support/iemanager/sheets_settings.png" alt="Google sheets import settings">
 | 
					 | 
				
			||||||
                        </figure>
 | 
					 | 
				
			||||||
                    </li>
 | 
					 | 
				
			||||||
                    <li>
 | 
					 | 
				
			||||||
                        <strong>Excel (including Excel Online)</strong>: Avoid using Excel. Excel will not correctly import channels, or give
 | 
					 | 
				
			||||||
                        clear options to correct imports.
 | 
					 | 
				
			||||||
                    </li>
 | 
					 | 
				
			||||||
                </ul>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </section>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,12 +20,11 @@
 | 
				
			|||||||
                <br>
 | 
					                <br>
 | 
				
			||||||
                Violating the Terms of Service may result in receiving a permanent ban from the Discord server,
 | 
					                Violating the Terms of Service may result in receiving a permanent ban from the Discord server,
 | 
				
			||||||
                permanent restriction on your usage of Reminder Bot, or removal of some or all of your content on
 | 
					                permanent restriction on your usage of Reminder Bot, or removal of some or all of your content on
 | 
				
			||||||
                Reminder Bot or the Discord server. None of these will necessarily be preceded or succeeded by a warning
 | 
					                Reminder Bot or the Discord server.
 | 
				
			||||||
                or notice.
 | 
					 | 
				
			||||||
                <br>
 | 
					                <br>
 | 
				
			||||||
                <br>
 | 
					                <br>
 | 
				
			||||||
                The Terms of Service may be updated. Notice will be provided via the Discord server. You
 | 
					                The Terms of Service may be updated at any time. Notice will be provided via the Discord server. You
 | 
				
			||||||
                should consider the Terms of Service to be a strong for appropriate behaviour.
 | 
					                should consider the Terms of Service to be a guideline for appropriate behaviour.
 | 
				
			||||||
            </p>
 | 
					            </p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
@@ -38,12 +37,6 @@
 | 
				
			|||||||
                <li>Do not use the bot to harass other Discord users</li>
 | 
					                <li>Do not use the bot to harass other Discord users</li>
 | 
				
			||||||
                <li>Do not use the bot to transmit malware or other illegal content</li>
 | 
					                <li>Do not use the bot to transmit malware or other illegal content</li>
 | 
				
			||||||
                <li>Do not use the bot to send more than 15 messages during a 60 second period</li>
 | 
					                <li>Do not use the bot to send more than 15 messages during a 60 second period</li>
 | 
				
			||||||
                <li>
 | 
					 | 
				
			||||||
                    Do not attempt to circumvent restrictions imposed by the bot or website, including trying to access
 | 
					 | 
				
			||||||
                    data of other users, circumvent Patreon restrictions, or uploading files and creating reminders that
 | 
					 | 
				
			||||||
                    are too large for the bot to send or process. Some or all of these actions may be illegal in your
 | 
					 | 
				
			||||||
                    country
 | 
					 | 
				
			||||||
                </li>
 | 
					 | 
				
			||||||
            </ul>
 | 
					            </ul>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user