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 {
|
||||||
|
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(())
|
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