Compare commits
3 Commits
094d210f64
...
jellywx/gu
Author | SHA1 | Date | |
---|---|---|---|
b0a04bb289 | |||
eef1f6f3e8 | |||
3d08027325 |
1211
Cargo.lock
generated
1211
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@ -1,20 +1,20 @@
|
||||
[package]
|
||||
name = "reminder_rs"
|
||||
version = "1.6.10"
|
||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
||||
edition = "2021"
|
||||
version = "1.6.6"
|
||||
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
poise = "0.4"
|
||||
poise = "0.3"
|
||||
dotenv = "0.15"
|
||||
tokio = { version = "1", features = ["process", "full"] }
|
||||
reqwest = "0.11"
|
||||
lazy-regex = "2.3.0"
|
||||
regex = "1.6"
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
env_logger = "0.9"
|
||||
chrono = "0.4"
|
||||
chrono-tz = { version = "0.8", features = ["serde"] }
|
||||
chrono-tz = { version = "0.6", features = ["serde"] }
|
||||
lazy_static = "1.4"
|
||||
num-integer = "0.1"
|
||||
serde = "1.0"
|
||||
@ -23,7 +23,7 @@ serde_repr = "0.1"
|
||||
rmp-serde = "1.1"
|
||||
rand = "0.8"
|
||||
levenshtein = "1.0"
|
||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]}
|
||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]}
|
||||
base64 = "0.13"
|
||||
|
||||
[dependencies.postman]
|
||||
@ -31,16 +31,3 @@ path = "postman"
|
||||
|
||||
[dependencies.reminder_web]
|
||||
path = "web"
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "$auto, nginx, python3, python3-venv"
|
||||
suggests = "mysql-server-8.0"
|
||||
maintainer-scripts = "debian"
|
||||
assets = [
|
||||
["target/release/reminder-rs", "usr/bin/reminder-rs", "755"],
|
||||
["conf/default.env", "etc/reminder-rs/default.env", "600"]
|
||||
]
|
||||
|
||||
[package.metadata.deb.systemd-units]
|
||||
unit-scripts = "systemd"
|
||||
start = false
|
||||
|
@ -22,15 +22,8 @@ These environment variables must be provided when compiling the bot
|
||||
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
|
||||
* `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size**
|
||||
|
||||
### Setting up database
|
||||
Use MySQL 8. MariaDB is confirmed not working at the moment.
|
||||
|
||||
Load the SQL files in order from "migrations" to generate the database schema.
|
||||
|
||||
### Setting up Python
|
||||
Reminder Bot by default looks for a venv within it's working directory to run Python out of. To set up a venv, install `python3-venv` and run `python3 -m venv venv`. Then, run `source venv/bin/activate` to activate the venv, and do `pip install dateparser` to install the required library.
|
||||
|
||||
Remember where you create the venv! You may need to change the `PYTHON_LOCATION` variable in the next step to point to your Python binary if the venv is not in your working directory.
|
||||
Reminder Bot by default looks for a venv within it's working directory to run Python out of. To set up a venv, install `python3-venv` and run `python3 -m venv venv`. Then, run `source venv/bin/activate` to activate the venv, and do `pip install dateparser` to install the required library
|
||||
|
||||
### Environment Variables
|
||||
Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory.
|
||||
|
10
Rocket.toml
10
Rocket.toml
@ -1,6 +1,6 @@
|
||||
[default]
|
||||
address = "0.0.0.0"
|
||||
port = 18920
|
||||
port = 5000
|
||||
template_dir = "web/templates"
|
||||
limits = { json = "10MiB" }
|
||||
|
||||
@ -11,18 +11,18 @@ secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
|
||||
certs = "web/private/rsa_sha256_cert.pem"
|
||||
key = "web/private/rsa_sha256_key.pem"
|
||||
|
||||
[debug.rsa_sha256.tls]
|
||||
[rsa_sha256.tls]
|
||||
certs = "web/private/rsa_sha256_cert.pem"
|
||||
key = "web/private/rsa_sha256_key.pem"
|
||||
|
||||
[debug.ecdsa_nistp256_sha256.tls]
|
||||
[ecdsa_nistp256_sha256.tls]
|
||||
certs = "web/private/ecdsa_nistp256_sha256_cert.pem"
|
||||
key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem"
|
||||
|
||||
[debug.ecdsa_nistp384_sha384.tls]
|
||||
[ecdsa_nistp384_sha384.tls]
|
||||
certs = "web/private/ecdsa_nistp384_sha384_cert.pem"
|
||||
key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem"
|
||||
|
||||
[debug.ed25519.tls]
|
||||
[ed25519.tls]
|
||||
certs = "web/private/ed25519_cert.pem"
|
||||
key = "eb/private/ed25519_key.pem"
|
||||
|
@ -1,15 +0,0 @@
|
||||
DATABASE_URL=
|
||||
|
||||
DISCORD_TOKEN=
|
||||
PATREON_GUILD_ID=
|
||||
PATREON_ROLE_ID=
|
||||
|
||||
LOCAL_TIMEZONE=
|
||||
MIN_INTERVAL=
|
||||
PYTHON_LOCATION=
|
||||
SECRET_KEY=
|
||||
|
||||
REMIND_INTERVAL=
|
||||
OAUTH2_DISCORD_CALLBACK=
|
||||
OAUTH2_CLIENT_ID=
|
||||
OAUTH2_CLIENT_SECRET=
|
2
debian/.gitignore
vendored
2
debian/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
@ -1,6 +1,10 @@
|
||||
CREATE DATABASE IF NOT EXISTS reminders;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
CREATE TABLE guilds (
|
||||
USE reminders;
|
||||
|
||||
CREATE TABLE reminders.guilds (
|
||||
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
||||
guild BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||
|
||||
@ -17,7 +21,7 @@ CREATE TABLE guilds (
|
||||
FOREIGN KEY (default_channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE channels (
|
||||
CREATE TABLE reminders.channels (
|
||||
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
||||
channel BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||
|
||||
@ -38,7 +42,7 @@ CREATE TABLE channels (
|
||||
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
CREATE TABLE reminders.users (
|
||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||
user BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||
|
||||
@ -58,7 +62,7 @@ CREATE TABLE users (
|
||||
FOREIGN KEY (dm_channel) REFERENCES reminders.channels(id) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE TABLE roles (
|
||||
CREATE TABLE reminders.roles (
|
||||
id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
|
||||
role BIGINT UNSIGNED UNIQUE NOT NULL,
|
||||
|
||||
@ -70,7 +74,7 @@ CREATE TABLE roles (
|
||||
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE embeds (
|
||||
CREATE TABLE reminders.embeds (
|
||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||
|
||||
title VARCHAR(256) NOT NULL DEFAULT '',
|
||||
@ -87,7 +91,7 @@ CREATE TABLE embeds (
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE embed_fields (
|
||||
CREATE TABLE reminders.embed_fields (
|
||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||
|
||||
title VARCHAR(256) NOT NULL DEFAULT '',
|
||||
@ -96,10 +100,10 @@ CREATE TABLE embed_fields (
|
||||
embed_id INT UNSIGNED NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE
|
||||
FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE messages (
|
||||
CREATE TABLE reminders.messages (
|
||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||
|
||||
content VARCHAR(2048) NOT NULL DEFAULT '',
|
||||
@ -110,10 +114,10 @@ CREATE TABLE messages (
|
||||
attachment_name VARCHAR(260),
|
||||
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL
|
||||
FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE reminders (
|
||||
CREATE TABLE reminders.reminders (
|
||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||
uid VARCHAR(64) UNIQUE NOT NULL,
|
||||
|
||||
@ -136,20 +140,20 @@ CREATE TABLE reminders (
|
||||
set_by INT UNSIGNED,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
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
|
||||
CREATE TRIGGER message_cleanup AFTER DELETE ON reminders.reminders
|
||||
FOR EACH ROW
|
||||
DELETE FROM messages WHERE id = OLD.message_id;
|
||||
DELETE FROM reminders.messages WHERE id = OLD.message_id;
|
||||
|
||||
CREATE TRIGGER embed_cleanup AFTER DELETE ON messages
|
||||
CREATE TRIGGER embed_cleanup AFTER DELETE ON reminders.messages
|
||||
FOR EACH ROW
|
||||
DELETE FROM embeds WHERE id = OLD.embed_id;
|
||||
DELETE FROM reminders.embeds WHERE id = OLD.embed_id;
|
||||
|
||||
CREATE TABLE todos (
|
||||
CREATE TABLE reminders.todos (
|
||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||
user_id INT UNSIGNED,
|
||||
guild_id INT UNSIGNED,
|
||||
@ -157,23 +161,23 @@ CREATE TABLE todos (
|
||||
value VARCHAR(2000) NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL
|
||||
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 command_restrictions (
|
||||
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 roles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (role_id) REFERENCES reminders.roles(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY (`role_id`, `command`)
|
||||
);
|
||||
|
||||
CREATE TABLE timers (
|
||||
CREATE TABLE reminders.timers (
|
||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||
start_time TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
name VARCHAR(32) NOT NULL,
|
||||
@ -182,7 +186,7 @@ CREATE TABLE timers (
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE events (
|
||||
CREATE TABLE reminders.events (
|
||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||
`time` TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
|
||||
@ -194,12 +198,12 @@ CREATE TABLE events (
|
||||
reminder_id INT UNSIGNED,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL
|
||||
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 command_aliases (
|
||||
CREATE TABLE reminders.command_aliases (
|
||||
id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
|
||||
|
||||
guild_id INT UNSIGNED NOT NULL,
|
||||
@ -208,22 +212,22 @@ CREATE TABLE command_aliases (
|
||||
command VARCHAR(2048) NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY (`guild_id`, `name`)
|
||||
);
|
||||
|
||||
CREATE TABLE guild_users (
|
||||
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 guilds(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE,
|
||||
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 event_cleanup
|
||||
CREATE EVENT reminders.event_cleanup
|
||||
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
|
||||
ON COMPLETION PRESERVE
|
||||
DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
|
||||
DO DELETE FROM reminders.events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
|
@ -1,3 +1,5 @@
|
||||
USE reminders;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
DROP TABLE IF EXISTS reminders_new;
|
@ -1,3 +1,5 @@
|
||||
USE reminders;
|
||||
|
||||
CREATE TABLE macro (
|
||||
id INT UNSIGNED AUTO_INCREMENT,
|
||||
guild_id INT UNSIGNED NOT NULL,
|
4
migration/03-reminder_variable_intervals.sql
Normal file
4
migration/03-reminder_variable_intervals.sql
Normal file
@ -0,0 +1,4 @@
|
||||
USE reminders;
|
||||
|
||||
ALTER TABLE reminders.reminders RENAME COLUMN `interval` TO `interval_seconds`;
|
||||
ALTER TABLE reminders.reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL;
|
@ -1,3 +1,5 @@
|
||||
USE reminders;
|
||||
|
||||
CREATE TABLE reminder_template (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
92
migration/05-restructure-guild-table.sql
Normal file
92
migration/05-restructure-guild-table.sql
Normal file
@ -0,0 +1,92 @@
|
||||
SET foreign_key_checks = 0;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
-- drop existing constraints
|
||||
ALTER TABLE channels DROP FOREIGN KEY `channels_ibfk_1`;
|
||||
ALTER TABLE command_aliases DROP FOREIGN KEY `command_aliases_ibfk_1`;
|
||||
ALTER TABLE events DROP FOREIGN KEY `events_ibfk_1`;
|
||||
ALTER TABLE guild_users DROP FOREIGN KEY `guild_users_ibfk_1`;
|
||||
ALTER TABLE macro DROP FOREIGN KEY `macro_ibfk_1`;
|
||||
ALTER TABLE roles DROP FOREIGN KEY `roles_ibfk_1`;
|
||||
ALTER TABLE todos DROP FOREIGN KEY `todos_ibfk_2`;
|
||||
ALTER TABLE reminder_template DROP FOREIGN KEY `reminder_template_ibfk_1`;
|
||||
|
||||
-- update foreign key types
|
||||
ALTER TABLE channels MODIFY `guild_id` BIGINT UNSIGNED;
|
||||
ALTER TABLE command_aliases MODIFY `guild_id` BIGINT UNSIGNED;
|
||||
ALTER TABLE events MODIFY `guild_id` BIGINT UNSIGNED;
|
||||
ALTER TABLE guild_users MODIFY `guild` BIGINT UNSIGNED;
|
||||
ALTER TABLE macro MODIFY `guild_id` BIGINT UNSIGNED;
|
||||
ALTER TABLE roles MODIFY `guild_id` BIGINT UNSIGNED;
|
||||
ALTER TABLE todos MODIFY `guild_id` BIGINT UNSIGNED;
|
||||
ALTER TABLE reminder_template MODIFY `guild_id` BIGINT UNSIGNED;
|
||||
|
||||
-- update foreign key values
|
||||
UPDATE channels SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
|
||||
UPDATE command_aliases SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
|
||||
UPDATE events SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
|
||||
UPDATE guild_users SET `guild` = (SELECT `guild` FROM guilds WHERE guilds.`id` = guild_users.`guild`);
|
||||
UPDATE macro SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
|
||||
UPDATE roles SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
|
||||
UPDATE todos SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
|
||||
UPDATE reminder_template SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
|
||||
|
||||
-- update guilds table
|
||||
ALTER TABLE guilds MODIFY `id` BIGINT UNSIGNED NOT NULL;
|
||||
UPDATE guilds SET `id` = `guild`;
|
||||
ALTER TABLE guilds DROP COLUMN `guild`;
|
||||
ALTER TABLE guilds ADD COLUMN `default_channel` BIGINT UNSIGNED;
|
||||
ALTER TABLE guilds ADD CONSTRAINT `default_channel_fk`
|
||||
FOREIGN KEY (`default_channel`)
|
||||
REFERENCES channels(`channel`)
|
||||
ON DELETE SET NULL
|
||||
ON UPDATE CASCADE;
|
||||
|
||||
-- re-add constraints
|
||||
ALTER TABLE channels ADD CONSTRAINT
|
||||
FOREIGN KEY (`guild_id`)
|
||||
REFERENCES guilds(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE command_aliases ADD CONSTRAINT
|
||||
FOREIGN KEY (`guild_id`)
|
||||
REFERENCES guilds(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE events ADD CONSTRAINT
|
||||
FOREIGN KEY (`guild_id`)
|
||||
REFERENCES guilds(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE guild_users ADD CONSTRAINT
|
||||
FOREIGN KEY (`guild`)
|
||||
REFERENCES guilds(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE macro ADD CONSTRAINT
|
||||
FOREIGN KEY (`guild_id`)
|
||||
REFERENCES guilds(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE roles ADD CONSTRAINT
|
||||
FOREIGN KEY (`guild_id`)
|
||||
REFERENCES guilds(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE todos ADD CONSTRAINT
|
||||
FOREIGN KEY (`guild_id`)
|
||||
REFERENCES guilds(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE;
|
||||
|
||||
|
||||
COMMIT;
|
||||
|
||||
SET foreign_key_checks = 1;
|
@ -1,2 +0,0 @@
|
||||
ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`;
|
||||
ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL;
|
@ -1 +0,0 @@
|
||||
ALTER TABLE reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL;
|
@ -1,41 +0,0 @@
|
||||
server {
|
||||
server_name www.reminder-bot.com;
|
||||
|
||||
return 301 $scheme://reminder-bot.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name reminder-bot.com;
|
||||
|
||||
return 301 https://reminder-bot.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name reminder-bot.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:18920;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /static {
|
||||
alias /var/www/reminder-rs/static;
|
||||
expires 30d;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use chrono::{DateTime, Days, Duration, Months};
|
||||
use chrono::Duration;
|
||||
use chrono_tz::Tz;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{error, info, warn};
|
||||
@ -62,23 +62,18 @@ pub fn substitute(string: &str) -> String {
|
||||
let format = caps.name("format").map(|m| m.as_str());
|
||||
|
||||
if let (Some(final_time), Some(format)) = (final_time, format) {
|
||||
match NaiveDateTime::from_timestamp_opt(final_time, 0) {
|
||||
Some(dt) => {
|
||||
let now = Utc::now().naive_utc();
|
||||
let dt = NaiveDateTime::from_timestamp(final_time, 0);
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
let difference = {
|
||||
if now < dt {
|
||||
dt - Utc::now().naive_utc()
|
||||
} else {
|
||||
Utc::now().naive_utc() - dt
|
||||
}
|
||||
};
|
||||
|
||||
fmt_displacement(format, difference.num_seconds() as u64)
|
||||
let difference = {
|
||||
if now < dt {
|
||||
dt - Utc::now().naive_utc()
|
||||
} else {
|
||||
Utc::now().naive_utc() - dt
|
||||
}
|
||||
};
|
||||
|
||||
None => String::new(),
|
||||
}
|
||||
fmt_displacement(format, difference.num_seconds() as u64)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
@ -248,12 +243,11 @@ pub struct Reminder {
|
||||
attachment: Option<Vec<u8>>,
|
||||
attachment_name: Option<String>,
|
||||
|
||||
utc_time: DateTime<Utc>,
|
||||
utc_time: NaiveDateTime,
|
||||
timezone: String,
|
||||
restartable: bool,
|
||||
expires: Option<DateTime<Utc>>,
|
||||
expires: Option<NaiveDateTime>,
|
||||
interval_seconds: Option<u32>,
|
||||
interval_days: Option<u32>,
|
||||
interval_months: Option<u32>,
|
||||
|
||||
avatar: Option<String>,
|
||||
@ -287,7 +281,6 @@ SELECT
|
||||
reminders.`restartable` AS restartable,
|
||||
reminders.`expires` AS 'expires',
|
||||
reminders.`interval_seconds` AS 'interval_seconds',
|
||||
reminders.`interval_days` AS 'interval_days',
|
||||
reminders.`interval_months` AS 'interval_months',
|
||||
|
||||
reminders.`avatar` AS avatar,
|
||||
@ -337,7 +330,9 @@ WHERE
|
||||
|
||||
async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) {
|
||||
let _ = sqlx::query!(
|
||||
"UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?",
|
||||
"
|
||||
UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?
|
||||
",
|
||||
self.channel_id
|
||||
)
|
||||
.execute(pool)
|
||||
@ -346,43 +341,55 @@ WHERE
|
||||
|
||||
async fn refresh(&self, pool: impl Executor<'_, Database = Database> + Copy) {
|
||||
if self.interval_seconds.is_some() || self.interval_months.is_some() {
|
||||
let now = Utc::now();
|
||||
let mut updated_reminder_time =
|
||||
self.utc_time.with_timezone(&self.timezone.parse().unwrap_or(Tz::UTC));
|
||||
let now = Utc::now().naive_local();
|
||||
let mut updated_reminder_time = self.utc_time;
|
||||
|
||||
while updated_reminder_time < now {
|
||||
if let Some(interval) = self.interval_months {
|
||||
updated_reminder_time = updated_reminder_time
|
||||
.checked_add_months(Months::new(interval))
|
||||
.unwrap_or_else(|| {
|
||||
warn!("Could not add months to a reminder");
|
||||
if let Some(interval) = self.interval_months {
|
||||
match sqlx::query!(
|
||||
// use the second date_add to force return value to datetime
|
||||
"SELECT DATE_ADD(DATE_ADD(?, INTERVAL ? MONTH), INTERVAL 0 SECOND) AS new_time",
|
||||
updated_reminder_time,
|
||||
interval
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
{
|
||||
Ok(row) => match row.new_time {
|
||||
Some(datetime) => {
|
||||
updated_reminder_time = datetime;
|
||||
}
|
||||
None => {
|
||||
warn!("Could not update interval by months: got NULL");
|
||||
|
||||
updated_reminder_time
|
||||
});
|
||||
}
|
||||
updated_reminder_time += Duration::days(30);
|
||||
}
|
||||
},
|
||||
|
||||
if let Some(interval) = self.interval_days {
|
||||
updated_reminder_time = updated_reminder_time
|
||||
.checked_add_days(Days::new(interval as u64))
|
||||
.unwrap_or_else(|| {
|
||||
warn!("Could not add days to a reminder");
|
||||
Err(e) => {
|
||||
warn!("Could not update interval by months: {:?}", e);
|
||||
|
||||
updated_reminder_time
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(interval) = self.interval_seconds {
|
||||
updated_reminder_time =
|
||||
updated_reminder_time + Duration::seconds(interval as i64);
|
||||
// naively fallback to adding 30 days
|
||||
updated_reminder_time += Duration::days(30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.expires.map_or(false, |expires| updated_reminder_time > expires) {
|
||||
if let Some(interval) = self.interval_seconds {
|
||||
while updated_reminder_time < now {
|
||||
updated_reminder_time += Duration::seconds(interval as i64);
|
||||
}
|
||||
}
|
||||
|
||||
if self.expires.map_or(false, |expires| {
|
||||
NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires
|
||||
}) {
|
||||
self.force_delete(pool).await;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE reminders SET `utc_time` = ? WHERE `id` = ?",
|
||||
updated_reminder_time.with_timezone(&Utc),
|
||||
"
|
||||
UPDATE reminders SET `utc_time` = ? WHERE `id` = ?
|
||||
",
|
||||
updated_reminder_time,
|
||||
self.id
|
||||
)
|
||||
.execute(pool)
|
||||
@ -395,10 +402,15 @@ WHERE
|
||||
}
|
||||
|
||||
async fn force_delete(&self, pool: impl Executor<'_, Database = Database> + Copy) {
|
||||
sqlx::query!("DELETE FROM reminders WHERE `id` = ?", self.id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect(&format!("Could not delete Reminder {}", self.id));
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM reminders WHERE `id` = ?
|
||||
",
|
||||
self.id
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect(&format!("Could not delete Reminder {}", self.id));
|
||||
}
|
||||
|
||||
async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) {
|
||||
@ -492,9 +504,7 @@ WHERE
|
||||
w.content(&reminder.content).tts(reminder.tts);
|
||||
|
||||
if let Some(username) = &reminder.username {
|
||||
if !username.is_empty() {
|
||||
w.username(username);
|
||||
}
|
||||
w.username(username);
|
||||
}
|
||||
|
||||
if let Some(avatar) = &reminder.avatar {
|
||||
@ -538,7 +548,9 @@ WHERE
|
||||
.map_or(true, |inner| inner >= Utc::now().naive_local()))
|
||||
{
|
||||
let _ = sqlx::query!(
|
||||
"UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?",
|
||||
"
|
||||
UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?
|
||||
",
|
||||
self.channel_id
|
||||
)
|
||||
.execute(pool)
|
||||
|
@ -24,7 +24,7 @@ pub async fn macro_name_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<Str
|
||||
SELECT name
|
||||
FROM macro
|
||||
WHERE
|
||||
guild_id = (SELECT id FROM guilds WHERE guild = ?)
|
||||
guild_id = ?
|
||||
AND name LIKE CONCAT(?, '%')",
|
||||
ctx.guild_id().unwrap().0,
|
||||
partial,
|
||||
@ -37,6 +37,20 @@ WHERE
|
||||
.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,
|
||||
|
@ -17,7 +17,7 @@ pub async fn delete_macro(
|
||||
) -> Result<(), Error> {
|
||||
match sqlx::query!(
|
||||
"
|
||||
SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
|
||||
SELECT id FROM macro WHERE guild_id = ? AND name = ?",
|
||||
ctx.guild_id().unwrap().0,
|
||||
name
|
||||
)
|
||||
|
@ -24,7 +24,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
|
||||
|
||||
let aliases = sqlx::query_as!(
|
||||
Alias,
|
||||
"SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||
"SELECT name, command FROM command_aliases WHERE guild_id = ?",
|
||||
guild_id.0
|
||||
)
|
||||
.fetch_all(&mut transaction)
|
||||
@ -36,7 +36,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
|
||||
match parse_text_command(guild_id, alias.name, &alias.command) {
|
||||
Some(cmd_macro) => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
|
||||
"INSERT INTO macro (guild_id, name, description, commands) VALUES (?, ?, ?, ?)",
|
||||
cmd_macro.guild_id.0,
|
||||
cmd_macro.name,
|
||||
cmd_macro.description,
|
||||
|
@ -31,7 +31,7 @@ pub async fn record_macro(
|
||||
|
||||
let row = sqlx::query!(
|
||||
"
|
||||
SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
|
||||
SELECT 1 as _e FROM macro WHERE guild_id = ? AND name = ?",
|
||||
guild_id.0,
|
||||
name
|
||||
)
|
||||
@ -121,15 +121,15 @@ pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
|
||||
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 = ?), ?, ?, ?)",
|
||||
"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();
|
||||
.execute(&ctx.data().database)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
ctx.send(|m| {
|
||||
m.embed(|e| {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::super::autocomplete::macro_name_autocomplete;
|
||||
use crate::{models::command_macro::guild_command_macro, Context, Data, Error, THEME_COLOR};
|
||||
use crate::{models::command_macro::guild_command_macro, Context, Data, Error};
|
||||
|
||||
/// Run a recorded macro
|
||||
#[poise::command(
|
||||
@ -17,17 +17,7 @@ pub async fn run_macro(
|
||||
) -> Result<(), Error> {
|
||||
match guild_command_macro(&Context::Application(ctx), &name).await {
|
||||
Some(command_macro) => {
|
||||
Context::Application(ctx)
|
||||
.send(|b| {
|
||||
b.embed(|e| {
|
||||
e.title("Running Macro").color(*THEME_COLOR).description(format!(
|
||||
"Running macro {} ({} commands)",
|
||||
command_macro.name,
|
||||
command_macro.commands.len()
|
||||
))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
ctx.defer_response(false).await?;
|
||||
|
||||
for command in command_macro.commands {
|
||||
if let Some(action) = command.action {
|
||||
|
@ -2,6 +2,7 @@ use chrono::offset::Utc;
|
||||
use chrono_tz::{Tz, TZ_VARIANTS};
|
||||
use levenshtein::levenshtein;
|
||||
use log::warn;
|
||||
use poise::serenity_prelude::{ChannelId, Mentionable};
|
||||
|
||||
use super::autocomplete::timezone_autocomplete;
|
||||
use crate::{consts::THEME_COLOR, models::CtxData, Context, Error};
|
||||
@ -148,11 +149,51 @@ pub async fn unset_allowed_dm(ctx: Context<'_>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set defaults for commands
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
identifying_name = "default",
|
||||
default_member_permissions = "MANAGE_GUILD"
|
||||
)]
|
||||
pub async fn default(_ctx: Context<'_>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set a default channel for reminders to be sent to
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
guild_only = true,
|
||||
identifying_name = "default_channel",
|
||||
default_member_permissions = "MANAGE_GUILD"
|
||||
)]
|
||||
pub async fn default_channel(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Channel to send reminders to by default"] channel: Option<ChannelId>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(mut guild_data) = ctx.guild_data().await {
|
||||
guild_data.default_channel = channel.map(|c| c.0);
|
||||
|
||||
guild_data.commit_changes(&ctx.data().database).await?;
|
||||
|
||||
if let Some(channel) = channel {
|
||||
ctx.send(|r| {
|
||||
r.ephemeral(true).content(format!("Default channel set to {}", channel.mention()))
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
ctx.send(|r| r.ephemeral(true).content("Default channel unset.")).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// View the webhook being used to send reminders to this channel
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
identifying_name = "webhook_url",
|
||||
required_permissions = "ADMINISTRATOR"
|
||||
required_permissions = "ADMINISTRATOR",
|
||||
default_member_permissions = "ADMINISTRATOR"
|
||||
)]
|
||||
pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> {
|
||||
match ctx.channel_data().await {
|
||||
|
@ -1,6 +1,10 @@
|
||||
use std::{collections::HashSet, string::ToString};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
string::ToString,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono_tz::Tz;
|
||||
use num_integer::Integer;
|
||||
use poise::{
|
||||
@ -11,7 +15,9 @@ use poise::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete},
|
||||
commands::autocomplete::{
|
||||
multiline_autocomplete, time_hint_autocomplete, timezone_autocomplete,
|
||||
},
|
||||
component_models::{
|
||||
pager::{DelPager, LookPager, Pager},
|
||||
ComponentDataModel, DelSelector, UndoReminder,
|
||||
@ -56,27 +62,18 @@ pub async fn pause(
|
||||
let parsed = natural_parser(&until, &timezone.to_string()).await;
|
||||
|
||||
if let Some(timestamp) = parsed {
|
||||
match NaiveDateTime::from_timestamp_opt(timestamp, 0) {
|
||||
Some(dt) => {
|
||||
channel.paused = true;
|
||||
channel.paused_until = Some(dt);
|
||||
let dt = NaiveDateTime::from_timestamp(timestamp, 0);
|
||||
|
||||
channel.commit_changes(&ctx.data().database).await;
|
||||
channel.paused = true;
|
||||
channel.paused_until = Some(dt);
|
||||
|
||||
ctx.say(format!(
|
||||
"Reminders in this channel have been silenced until **<t:{}:D>**",
|
||||
timestamp
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
channel.commit_changes(&ctx.data().database).await;
|
||||
|
||||
None => {
|
||||
ctx.say(format!(
|
||||
"Time processed could not be interpreted as `DateTime`. Please write the time as clearly as possible",
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
ctx.say(format!(
|
||||
"Reminders in this channel have been silenced until **<t:{}:D>**",
|
||||
timestamp
|
||||
))
|
||||
.await?;
|
||||
} else {
|
||||
ctx.say(
|
||||
"Time could not be processed. Please write the time as clearly as possible",
|
||||
@ -250,7 +247,7 @@ pub async fn look(
|
||||
char_count < EMBED_DESCRIPTION_MAX_LENGTH
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
.join("\n");
|
||||
|
||||
let pages = reminders
|
||||
.iter()
|
||||
@ -437,8 +434,11 @@ pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> Cr
|
||||
reply
|
||||
}
|
||||
|
||||
fn time_difference(start_time: DateTime<Utc>) -> String {
|
||||
let delta = (Utc::now() - start_time).num_seconds();
|
||||
fn time_difference(start_time: NaiveDateTime) -> String {
|
||||
let unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
|
||||
let now = NaiveDateTime::from_timestamp(unix_time, 0);
|
||||
|
||||
let delta = (now - start_time).num_seconds();
|
||||
|
||||
let (minutes, seconds) = delta.div_rem(&60);
|
||||
let (hours, minutes) = minutes.div_rem(&60);
|
||||
@ -562,45 +562,7 @@ struct ContentModal {
|
||||
content: String,
|
||||
}
|
||||
|
||||
/// Create a reminder with multi-line content. Press "+4 more" for other options.
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
identifying_name = "multiline",
|
||||
default_member_permissions = "MANAGE_GUILD"
|
||||
)]
|
||||
pub async fn multiline(
|
||||
ctx: ApplicationContext<'_>,
|
||||
#[description = "A description of the time to set the reminder for"]
|
||||
#[autocomplete = "time_hint_autocomplete"]
|
||||
time: String,
|
||||
#[description = "Channel or user mentions to set the reminder for"] channels: Option<String>,
|
||||
#[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"]
|
||||
interval: Option<String>,
|
||||
#[description = "(Patreon only) For repeating reminders, the time at which the reminder will stop repeating"]
|
||||
expires: Option<String>,
|
||||
#[description = "Set the TTS flag on the reminder message, similar to the /tts command"]
|
||||
tts: Option<bool>,
|
||||
#[description = "Set a timezone override for this reminder only"]
|
||||
#[autocomplete = "timezone_autocomplete"]
|
||||
timezone: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let tz = timezone.map(|t| t.parse::<Tz>().ok()).flatten();
|
||||
let data = ContentModal::execute(ctx).await?;
|
||||
|
||||
create_reminder(
|
||||
Context::Application(ctx),
|
||||
time,
|
||||
data.content,
|
||||
channels,
|
||||
interval,
|
||||
expires,
|
||||
tts,
|
||||
tz,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create a reminder. Press "+4 more" for other options. Use "/multiline" for multiline content.
|
||||
/// Create a reminder. Press "+4 more" for other options.
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
identifying_name = "remind",
|
||||
@ -611,7 +573,9 @@ pub async fn remind(
|
||||
#[description = "A description of the time to set the reminder for"]
|
||||
#[autocomplete = "time_hint_autocomplete"]
|
||||
time: String,
|
||||
#[description = "The message content to send"] content: String,
|
||||
#[description = "The message content to send"]
|
||||
#[autocomplete = "multiline_autocomplete"]
|
||||
content: String,
|
||||
#[description = "Channel or user mentions to set the reminder for"] channels: Option<String>,
|
||||
#[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"]
|
||||
interval: Option<String>,
|
||||
@ -625,8 +589,33 @@ pub async fn remind(
|
||||
) -> Result<(), Error> {
|
||||
let tz = timezone.map(|t| t.parse::<Tz>().ok()).flatten();
|
||||
|
||||
create_reminder(Context::Application(ctx), time, content, channels, interval, expires, tts, tz)
|
||||
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(
|
||||
@ -664,7 +653,9 @@ async fn create_reminder(
|
||||
let list = channels.map(|arg| parse_mention_list(&arg)).unwrap_or_default();
|
||||
|
||||
if list.is_empty() {
|
||||
if ctx.guild_id().is_some() {
|
||||
if let Some(channel_id) = ctx.default_channel().await {
|
||||
vec![ReminderScope::Channel(channel_id.0)]
|
||||
} else if ctx.guild_id().is_some() {
|
||||
vec![ReminderScope::Channel(ctx.channel_id().0)]
|
||||
} else {
|
||||
vec![ReminderScope::User(ctx.author().id.0)]
|
||||
|
@ -47,7 +47,7 @@ pub async fn todo_guild_add(
|
||||
) -> Result<(), Error> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO todos (guild_id, value)
|
||||
VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
|
||||
VALUES (?, ?)",
|
||||
ctx.guild_id().unwrap().0,
|
||||
task
|
||||
)
|
||||
@ -70,9 +70,7 @@ VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
|
||||
)]
|
||||
pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let values = sqlx::query!(
|
||||
"SELECT todos.id, value FROM todos
|
||||
INNER JOIN guilds ON todos.guild_id = guilds.id
|
||||
WHERE guilds.guild = ?",
|
||||
"SELECT todos.id, value FROM todos WHERE guild_id = ?",
|
||||
ctx.guild_id().unwrap().0,
|
||||
)
|
||||
.fetch_all(&ctx.data().database)
|
||||
@ -122,7 +120,7 @@ pub async fn todo_channel_add(
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO todos (guild_id, channel_id, value)
|
||||
VALUES ((SELECT id FROM guilds WHERE guild = ?), (SELECT id FROM channels WHERE channel = ?), ?)",
|
||||
VALUES (?, (SELECT id FROM channels WHERE channel = ?), ?)",
|
||||
ctx.guild_id().unwrap().0,
|
||||
ctx.channel_id().0,
|
||||
task
|
||||
@ -340,18 +338,7 @@ pub fn show_todo_page(
|
||||
opt.create_option(|o| {
|
||||
o.label(format!("Mark {} complete", count + first_num))
|
||||
.value(id)
|
||||
.description({
|
||||
let c = disp.split_once(' ').unwrap_or(("", "")).1;
|
||||
|
||||
if c.len() > 100 {
|
||||
format!(
|
||||
"{}...",
|
||||
c.chars().take(97).collect::<String>()
|
||||
)
|
||||
} else {
|
||||
c.to_string()
|
||||
}
|
||||
})
|
||||
.description(disp.split_once(' ').unwrap_or(("", "")).1)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -113,7 +113,7 @@ impl ComponentDataModel {
|
||||
char_count < EMBED_DESCRIPTION_MAX_LENGTH
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
.join("\n");
|
||||
|
||||
let mut embed = CreateEmbed::default();
|
||||
embed
|
||||
@ -222,9 +222,7 @@ WHERE channels.channel = ?",
|
||||
.collect::<Vec<(usize, String)>>()
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"SELECT todos.id, value FROM todos
|
||||
INNER JOIN guilds ON todos.guild_id = guilds.id
|
||||
WHERE guilds.guild = ?",
|
||||
"SELECT todos.id, value FROM todos WHERE guild_id = ?",
|
||||
pager.guild_id,
|
||||
)
|
||||
.fetch_all(&data.database)
|
||||
|
@ -27,7 +27,7 @@ lazy_static! {
|
||||
.into();
|
||||
pub static ref REGEX_CHANNEL_USER: Regex = Regex::new(r#"\s*<(#|@)(?:!)?(\d+)>\s*"#).unwrap();
|
||||
pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
|
||||
env::var("PATREON_ROLE_ID")
|
||||
env::var("SUBSCRIPTION_ROLES")
|
||||
.map(|var| var
|
||||
.split(',')
|
||||
.filter_map(|item| { item.parse::<u64>().ok() })
|
||||
@ -35,7 +35,7 @@ lazy_static! {
|
||||
.unwrap_or_else(|_| Vec::new())
|
||||
);
|
||||
pub static ref CNC_GUILD: Option<u64> =
|
||||
env::var("PATREON_GUILD_ID").map(|var| var.parse::<u64>().ok()).ok().flatten();
|
||||
env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten();
|
||||
pub static ref MIN_INTERVAL: i64 =
|
||||
env::var("MIN_INTERVAL").ok().and_then(|inner| inner.parse::<i64>().ok()).unwrap_or(600);
|
||||
pub static ref MAX_TIME: i64 = env::var("MAX_TIME")
|
||||
|
@ -6,7 +6,9 @@ use poise::{
|
||||
serenity_prelude::{model::application::interaction::Interaction, utils::shard_id},
|
||||
};
|
||||
|
||||
use crate::{component_models::ComponentDataModel, Data, Error, THEME_COLOR};
|
||||
use crate::{
|
||||
component_models::ComponentDataModel, models::guild_data::GuildData, Data, Error, THEME_COLOR,
|
||||
};
|
||||
|
||||
pub async fn listener(
|
||||
ctx: &serenity::Context,
|
||||
@ -27,7 +29,7 @@ pub async fn listener(
|
||||
if *is_new {
|
||||
let guild_id = guild.id.as_u64().to_owned();
|
||||
|
||||
sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id)
|
||||
sqlx::query!("INSERT IGNORE INTO guilds (id) VALUES (?)", guild_id)
|
||||
.execute(&data.database)
|
||||
.await?;
|
||||
|
||||
@ -61,15 +63,27 @@ To stay up to date on the latest features and fixes, join our [Discord](https://
|
||||
}
|
||||
}
|
||||
poise::Event::GuildDelete { incomplete, .. } => {
|
||||
let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.0)
|
||||
let _ = sqlx::query!("DELETE FROM guilds WHERE id = ?", 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);
|
||||
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?;
|
||||
}
|
||||
}
|
||||
|
||||
component_model.act(ctx, data, component).await;
|
||||
Interaction::MessageComponent(component) => {
|
||||
let component_model =
|
||||
ComponentDataModel::from_custom_id(&component.data.custom_id);
|
||||
|
||||
component_model.act(ctx, data, component).await;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -110,14 +110,13 @@ impl OverflowOp for u64 {
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Interval {
|
||||
pub month: u64,
|
||||
pub day: u64,
|
||||
pub sec: u64,
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
iter: Chars<'a>,
|
||||
src: &'a str,
|
||||
current: (u64, u64, u64, u64),
|
||||
current: (u64, u64, u64),
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
@ -141,17 +140,17 @@ impl<'a> Parser<'a> {
|
||||
Ok(None)
|
||||
}
|
||||
fn parse_unit(&mut self, n: u64, start: usize, end: usize) -> Result<(), Error> {
|
||||
let (mut month, mut day, mut sec, nsec) = match &self.src[start..end] {
|
||||
"nanos" | "nsec" | "ns" => (0, 0u64, 0u64, n),
|
||||
"usec" | "us" => (0, 0, 0u64, n.mul(1000)?),
|
||||
"millis" | "msec" | "ms" => (0, 0, 0u64, n.mul(1_000_000)?),
|
||||
"seconds" | "second" | "secs" | "sec" | "s" => (0, 0, n, 0),
|
||||
"minutes" | "minute" | "min" | "mins" | "m" => (0, 0, n.mul(60)?, 0),
|
||||
"hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0),
|
||||
"days" | "day" | "d" => (0, n, 0, 0),
|
||||
"weeks" | "week" | "w" => (0, n.mul(7)?, 0, 0),
|
||||
"months" | "month" | "M" => (n, 0, 0, 0),
|
||||
"years" | "year" | "y" => (n.mul(12)?, 0, 0, 0),
|
||||
let (mut month, mut sec, nsec) = match &self.src[start..end] {
|
||||
"nanos" | "nsec" | "ns" => (0u64, 0u64, n),
|
||||
"usec" | "us" => (0, 0u64, n.mul(1000)?),
|
||||
"millis" | "msec" | "ms" => (0, 0u64, n.mul(1_000_000)?),
|
||||
"seconds" | "second" | "secs" | "sec" | "s" => (0, n, 0),
|
||||
"minutes" | "minute" | "min" | "mins" | "m" => (0, n.mul(60)?, 0),
|
||||
"hours" | "hour" | "hr" | "hrs" | "h" => (0, n.mul(3600)?, 0),
|
||||
"days" | "day" | "d" => (0, n.mul(86400)?, 0),
|
||||
"weeks" | "week" | "w" => (0, n.mul(86400 * 7)?, 0),
|
||||
"months" | "month" | "M" => (n, 0, 0),
|
||||
"years" | "year" | "y" => (12, 0, 0),
|
||||
_ => {
|
||||
return Err(Error::UnknownUnit {
|
||||
start,
|
||||
@ -161,16 +160,15 @@ impl<'a> Parser<'a> {
|
||||
});
|
||||
}
|
||||
};
|
||||
let mut nsec = self.current.3 + nsec;
|
||||
let mut nsec = self.current.2 + nsec;
|
||||
if nsec > 1_000_000_000 {
|
||||
sec += nsec / 1_000_000_000;
|
||||
nsec %= 1_000_000_000;
|
||||
}
|
||||
sec += self.current.2;
|
||||
day += self.current.1;
|
||||
sec += self.current.1;
|
||||
month += self.current.0;
|
||||
|
||||
self.current = (month, day, sec, nsec);
|
||||
self.current = (month, sec, nsec);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -217,13 +215,7 @@ impl<'a> Parser<'a> {
|
||||
self.parse_unit(n, start, off)?;
|
||||
n = match self.parse_first_char()? {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
return Ok(Interval {
|
||||
month: self.current.0,
|
||||
day: self.current.1,
|
||||
sec: self.current.2,
|
||||
})
|
||||
}
|
||||
None => return Ok(Interval { month: self.current.0, sec: self.current.1 }),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -255,73 +247,5 @@ impl<'a> Parser<'a> {
|
||||
/// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000)));
|
||||
/// ```
|
||||
pub fn parse_duration(s: &str) -> Result<Interval, Error> {
|
||||
Parser { iter: s.chars(), src: s, current: (0, 0, 0, 0) }.parse()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_seconds() {
|
||||
let interval = parse_duration("10 seconds").unwrap();
|
||||
|
||||
assert_eq!(interval.sec, 10);
|
||||
assert_eq!(interval.day, 0);
|
||||
assert_eq!(interval.month, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_minutes() {
|
||||
let interval = parse_duration("10 minutes").unwrap();
|
||||
|
||||
assert_eq!(interval.sec, 600);
|
||||
assert_eq!(interval.day, 0);
|
||||
assert_eq!(interval.month, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_hours() {
|
||||
let interval = parse_duration("10 hours").unwrap();
|
||||
|
||||
assert_eq!(interval.sec, 36_000);
|
||||
assert_eq!(interval.day, 0);
|
||||
assert_eq!(interval.month, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_days() {
|
||||
let interval = parse_duration("10 days").unwrap();
|
||||
|
||||
assert_eq!(interval.sec, 0);
|
||||
assert_eq!(interval.day, 10);
|
||||
assert_eq!(interval.month, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_weeks() {
|
||||
let interval = parse_duration("10 weeks").unwrap();
|
||||
|
||||
assert_eq!(interval.sec, 0);
|
||||
assert_eq!(interval.day, 70);
|
||||
assert_eq!(interval.month, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_months() {
|
||||
let interval = parse_duration("10 months").unwrap();
|
||||
|
||||
assert_eq!(interval.sec, 0);
|
||||
assert_eq!(interval.day, 0);
|
||||
assert_eq!(interval.month, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_years() {
|
||||
let interval = parse_duration("10 years").unwrap();
|
||||
|
||||
assert_eq!(interval.sec, 0);
|
||||
assert_eq!(interval.day, 0);
|
||||
assert_eq!(interval.month, 120);
|
||||
}
|
||||
Parser { iter: s.chars(), src: s, current: (0, 0, 0) }.parse()
|
||||
}
|
||||
|
15
src/main.rs
15
src/main.rs
@ -18,10 +18,10 @@ use std::{
|
||||
env,
|
||||
error::Error as StdError,
|
||||
fmt::{Debug, Display, Formatter},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use chrono_tz::Tz;
|
||||
use dotenv::dotenv;
|
||||
use log::{error, warn};
|
||||
use poise::serenity_prelude::model::{
|
||||
gateway::GatewayIntents,
|
||||
@ -75,7 +75,7 @@ impl Display for Ended {
|
||||
|
||||
impl StdError for Ended {}
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
|
||||
let (tx, mut rx) = broadcast::channel(16);
|
||||
|
||||
@ -88,9 +88,7 @@ async fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
|
||||
async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
|
||||
env_logger::init();
|
||||
|
||||
if Path::new("/etc/soundfx-rs/default.env").exists() {
|
||||
dotenv::from_path("/etc/soundfx-rs/default.env")?;
|
||||
}
|
||||
dotenv()?;
|
||||
|
||||
let discord_token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment");
|
||||
|
||||
@ -122,6 +120,10 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
|
||||
],
|
||||
..command_macro::macro_base()
|
||||
},
|
||||
poise::Command {
|
||||
subcommands: vec![moderation_cmds::default_channel()],
|
||||
..moderation_cmds::default()
|
||||
},
|
||||
reminder_cmds::pause(),
|
||||
reminder_cmds::offset(),
|
||||
reminder_cmds::nudge(),
|
||||
@ -135,7 +137,6 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
|
||||
],
|
||||
..reminder_cmds::timer_base()
|
||||
},
|
||||
reminder_cmds::multiline(),
|
||||
reminder_cmds::remind(),
|
||||
poise::Command {
|
||||
subcommands: vec![
|
||||
@ -170,8 +171,6 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
|
||||
let database =
|
||||
Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
|
||||
|
||||
sqlx::migrate!().run(&database).await?;
|
||||
|
||||
let popular_timezones = sqlx::query!(
|
||||
"SELECT IFNULL(timezone, 'UTC') AS timezone
|
||||
FROM users
|
||||
|
@ -38,7 +38,7 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))
|
||||
INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, ?)
|
||||
",
|
||||
channel_id,
|
||||
channel_name,
|
||||
|
@ -43,7 +43,7 @@ pub async fn guild_command_macro(
|
||||
) -> Option<CommandMacro<Data, Error>> {
|
||||
let row = sqlx::query!(
|
||||
"
|
||||
SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?
|
||||
SELECT * FROM macro WHERE guild_id = ? AND name = ?
|
||||
",
|
||||
ctx.guild_id().unwrap().0,
|
||||
name
|
||||
|
52
src/models/guild_data.rs
Normal file
52
src/models/guild_data.rs
Normal file
@ -0,0 +1,52 @@
|
||||
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 command_macro;
|
||||
pub mod guild_data;
|
||||
pub mod reminder;
|
||||
pub mod timer;
|
||||
pub mod user_data;
|
||||
|
||||
use chrono_tz::Tz;
|
||||
use poise::serenity_prelude::{async_trait, model::id::UserId};
|
||||
use log::warn;
|
||||
use poise::serenity_prelude::{async_trait, model::id::UserId, ChannelId};
|
||||
|
||||
use crate::{
|
||||
models::{channel_data::ChannelData, user_data::UserData},
|
||||
models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData},
|
||||
CommandMacro, Context, Data, Error, GuildId,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait CtxData {
|
||||
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 timezone(&self) -> Tz;
|
||||
|
||||
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 default_channel(&self) -> Option<ChannelId>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -51,24 +51,55 @@ impl CtxData for Context<'_> {
|
||||
async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
|
||||
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 {
|
||||
pub(crate) async fn command_macros(
|
||||
pub async fn command_macros(
|
||||
&self,
|
||||
guild_id: GuildId,
|
||||
) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
|
||||
let rows = sqlx::query!(
|
||||
"SELECT name, description, commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||
"SELECT name, description, commands FROM macro WHERE guild_id = ?",
|
||||
guild_id.0
|
||||
)
|
||||
.fetch_all(&self.database)
|
||||
.await?.iter().map(|row| CommandMacro {
|
||||
.await?
|
||||
.iter()
|
||||
.map(|row| CommandMacro {
|
||||
guild_id,
|
||||
name: row.name.clone(),
|
||||
description: row.description.clone(),
|
||||
commands: serde_json::from_str(&row.commands).unwrap(),
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
@ -53,8 +53,7 @@ pub struct ReminderBuilder {
|
||||
channel: u32,
|
||||
utc_time: NaiveDateTime,
|
||||
timezone: String,
|
||||
interval_seconds: Option<i64>,
|
||||
interval_days: Option<i64>,
|
||||
interval_secs: Option<i64>,
|
||||
interval_months: Option<i64>,
|
||||
expires: Option<NaiveDateTime>,
|
||||
content: String,
|
||||
@ -88,7 +87,6 @@ INSERT INTO reminders (
|
||||
`utc_time`,
|
||||
`timezone`,
|
||||
`interval_seconds`,
|
||||
`interval_days`,
|
||||
`interval_months`,
|
||||
`expires`,
|
||||
`content`,
|
||||
@ -108,7 +106,6 @@ INSERT INTO reminders (
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?
|
||||
)
|
||||
",
|
||||
@ -116,8 +113,7 @@ INSERT INTO reminders (
|
||||
self.channel,
|
||||
utc_time,
|
||||
self.timezone,
|
||||
self.interval_seconds,
|
||||
self.interval_days,
|
||||
self.interval_secs,
|
||||
self.interval_months,
|
||||
self.expires,
|
||||
self.content,
|
||||
@ -179,15 +175,17 @@ impl<'a> MultiReminderBuilder<'a> {
|
||||
}
|
||||
|
||||
pub fn time<T: Into<i64>>(mut self, time: T) -> Self {
|
||||
if let Some(utc_time) = NaiveDateTime::from_timestamp_opt(time.into(), 0) {
|
||||
self.utc_time = utc_time;
|
||||
}
|
||||
self.utc_time = NaiveDateTime::from_timestamp(time.into(), 0);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn expires<T: Into<i64>>(mut self, time: Option<T>) -> Self {
|
||||
self.expires = time.map(|t| NaiveDateTime::from_timestamp_opt(t.into(), 0)).flatten();
|
||||
if let Some(t) = time {
|
||||
self.expires = Some(NaiveDateTime::from_timestamp(t.into(), 0));
|
||||
} else {
|
||||
self.expires = None;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
@ -214,14 +212,9 @@ impl<'a> MultiReminderBuilder<'a> {
|
||||
|
||||
let mut ok_locs = HashSet::new();
|
||||
|
||||
if self
|
||||
.interval
|
||||
.map_or(false, |i| ((i.sec + i.day * DAY + i.month * 30 * DAY) as i64) < *MIN_INTERVAL)
|
||||
{
|
||||
if self.interval.map_or(false, |i| ((i.sec + i.month * 30 * DAY) as i64) < *MIN_INTERVAL) {
|
||||
errors.insert(ReminderError::ShortInterval);
|
||||
} else if self
|
||||
.interval
|
||||
.map_or(false, |i| ((i.sec + i.day * DAY + i.month * 30 * DAY) as i64) > *MAX_TIME)
|
||||
} else if self.interval.map_or(false, |i| ((i.sec + i.month * 30 * DAY) as i64) > *MAX_TIME)
|
||||
{
|
||||
errors.insert(ReminderError::LongInterval);
|
||||
} else {
|
||||
@ -309,8 +302,7 @@ impl<'a> MultiReminderBuilder<'a> {
|
||||
channel: c,
|
||||
utc_time: self.utc_time,
|
||||
timezone: self.timezone.to_string(),
|
||||
interval_seconds: self.interval.map(|i| i.sec as i64),
|
||||
interval_days: self.interval.map(|i| i.day as i64),
|
||||
interval_secs: self.interval.map(|i| i.sec as i64),
|
||||
interval_months: self.interval.map(|i| i.month as i64),
|
||||
expires: self.expires,
|
||||
content: self.content.content.clone(),
|
||||
|
@ -6,7 +6,7 @@ pub mod look_flags;
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use chrono::{NaiveDateTime, TimeZone};
|
||||
use chrono_tz::Tz;
|
||||
use poise::serenity_prelude::{
|
||||
model::id::{ChannelId, GuildId, UserId},
|
||||
@ -24,9 +24,8 @@ pub struct Reminder {
|
||||
pub id: u32,
|
||||
pub uid: String,
|
||||
pub channel: u64,
|
||||
pub utc_time: DateTime<Utc>,
|
||||
pub utc_time: NaiveDateTime,
|
||||
pub interval_seconds: Option<u32>,
|
||||
pub interval_days: Option<u32>,
|
||||
pub interval_months: Option<u32>,
|
||||
pub expires: Option<NaiveDateTime>,
|
||||
pub enabled: bool,
|
||||
@ -60,7 +59,6 @@ SELECT
|
||||
channels.channel,
|
||||
reminders.utc_time,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.expires,
|
||||
reminders.enabled,
|
||||
@ -97,7 +95,6 @@ SELECT
|
||||
channels.channel,
|
||||
reminders.utc_time,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.expires,
|
||||
reminders.enabled,
|
||||
@ -141,7 +138,6 @@ SELECT
|
||||
channels.channel,
|
||||
reminders.utc_time,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.expires,
|
||||
reminders.enabled,
|
||||
@ -199,7 +195,6 @@ SELECT
|
||||
channels.channel,
|
||||
reminders.utc_time,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.expires,
|
||||
reminders.enabled,
|
||||
@ -233,7 +228,6 @@ SELECT
|
||||
channels.channel,
|
||||
reminders.utc_time,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.expires,
|
||||
reminders.enabled,
|
||||
@ -251,7 +245,7 @@ LEFT JOIN
|
||||
ON
|
||||
reminders.set_by = users.id
|
||||
WHERE
|
||||
channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
|
||||
channels.guild_id = ?
|
||||
",
|
||||
guild_id.as_u64()
|
||||
)
|
||||
@ -268,7 +262,6 @@ SELECT
|
||||
channels.channel,
|
||||
reminders.utc_time,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.expires,
|
||||
reminders.enabled,
|
||||
@ -317,32 +310,30 @@ WHERE
|
||||
count + 1,
|
||||
self.display_content(),
|
||||
self.channel,
|
||||
self.utc_time.with_timezone(timezone).format("%Y-%m-%d %H:%M:%S")
|
||||
timezone.timestamp(self.utc_time.timestamp(), 0).format("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
}
|
||||
|
||||
pub fn display(&self, flags: &LookFlags, timezone: &Tz) -> String {
|
||||
let time_display = match flags.time_display {
|
||||
TimeDisplayType::Absolute => {
|
||||
self.utc_time.with_timezone(timezone).format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
||||
TimeDisplayType::Absolute => timezone
|
||||
.timestamp(self.utc_time.timestamp(), 0)
|
||||
.format("%Y-%m-%d %H:%M:%S")
|
||||
.to_string(),
|
||||
|
||||
TimeDisplayType::Relative => format!("<t:{}:R>", self.utc_time.timestamp()),
|
||||
};
|
||||
|
||||
if self.interval_seconds.is_some()
|
||||
|| self.interval_days.is_some()
|
||||
|| self.interval_months.is_some()
|
||||
{
|
||||
if self.interval_seconds.is_some() || self.interval_months.is_some() {
|
||||
format!(
|
||||
"'{}' *occurs next at* **{}**, repeating (set by {})\n",
|
||||
"'{}' *occurs next at* **{}**, repeating (set by {})",
|
||||
self.display_content(),
|
||||
time_display,
|
||||
self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string())
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"'{}' *occurs next at* **{}** (set by {})\n",
|
||||
"'{}' *occurs next at* **{}** (set by {})",
|
||||
self.display_content(),
|
||||
time_display,
|
||||
self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string())
|
||||
|
@ -1,9 +1,9 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::NaiveDateTime;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
pub struct Timer {
|
||||
pub name: String,
|
||||
pub start_time: DateTime<Utc>,
|
||||
pub start_time: NaiveDateTime,
|
||||
pub owner: u64,
|
||||
}
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=Reminder Bot
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/reminder-rs
|
||||
Restart=always
|
||||
RestartSec=4
|
||||
# Environment="RUST_LOG=warn,reminder_rs=info,postman=info"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -31,7 +31,7 @@ lazy_static! {
|
||||
)
|
||||
.into();
|
||||
pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
|
||||
env::var("PATREON_ROLE_ID")
|
||||
env::var("SUBSCRIPTION_ROLES")
|
||||
.map(|var| var
|
||||
.split(',')
|
||||
.filter_map(|item| { item.parse::<u64>().ok() })
|
||||
@ -39,7 +39,7 @@ lazy_static! {
|
||||
.unwrap_or_else(|_| Vec::new())
|
||||
);
|
||||
pub static ref CNC_GUILD: Option<u64> =
|
||||
env::var("PATREON_GUILD_ID").map(|var| var.parse::<u64>().ok()).ok().flatten();
|
||||
env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten();
|
||||
pub static ref MIN_INTERVAL: u32 = env::var("MIN_INTERVAL")
|
||||
.ok()
|
||||
.map(|inner| inner.parse::<u32>().ok())
|
||||
|
@ -75,7 +75,7 @@ pub async fn initialize(
|
||||
env::var("OAUTH2_CLIENT_ID").expect("`OAUTH2_CLIENT_ID' not supplied");
|
||||
env::var("OAUTH2_CLIENT_SECRET").expect("`OAUTH2_CLIENT_SECRET' not supplied");
|
||||
env::var("OAUTH2_DISCORD_CALLBACK").expect("`OAUTH2_DISCORD_CALLBACK' not supplied");
|
||||
env::var("PATREON_GUILD_ID").expect("`PATREON_GUILD_ID' not supplied");
|
||||
env::var("PATREON_GUILD_ID").expect("`PATREON_GUILD' not supplied");
|
||||
info!("Done!");
|
||||
|
||||
let oauth2_client = BasicClient::new(
|
||||
|
@ -58,7 +58,6 @@ pub async fn export_reminders(
|
||||
reminders.enabled,
|
||||
reminders.expires,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.restartable,
|
||||
@ -160,7 +159,6 @@ pub async fn import_reminders(
|
||||
enabled: record.enabled,
|
||||
expires: record.expires,
|
||||
interval_seconds: record.interval_seconds,
|
||||
interval_days: record.interval_days,
|
||||
interval_months: record.interval_months,
|
||||
name: record.name,
|
||||
restartable: record.restartable,
|
||||
@ -320,6 +318,13 @@ pub async fn import_todos(
|
||||
}
|
||||
}
|
||||
|
||||
let _ = sqlx::query!(
|
||||
"DELETE FROM todos WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||
id
|
||||
)
|
||||
.execute(pool.inner())
|
||||
.await;
|
||||
|
||||
let query_str = format!(
|
||||
"INSERT INTO todos (value, channel_id, guild_id) VALUES {}",
|
||||
vec![query_placeholder].repeat(query_params.len()).join(",")
|
||||
|
@ -16,12 +16,10 @@ use serenity::{
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::{
|
||||
check_guild_subscription, check_subscription,
|
||||
consts::{
|
||||
MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
|
||||
MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH,
|
||||
MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH,
|
||||
MIN_INTERVAL,
|
||||
},
|
||||
routes::dashboard::{
|
||||
create_database_channel, create_reminder, template_name_default, DeleteReminder,
|
||||
@ -249,9 +247,9 @@ pub async fn create_reminder_template(
|
||||
Ok(json!({}))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not create template for {}: {:?}", id, e);
|
||||
warn!("Could not fetch templates from {}: {:?}", id, e);
|
||||
|
||||
json_err!("Could not create template")
|
||||
json_err!("Could not get templates")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -341,7 +339,6 @@ pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySq
|
||||
reminders.enabled,
|
||||
reminders.expires,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.restartable,
|
||||
@ -377,109 +374,35 @@ pub async fn edit_reminder(
|
||||
reminder: Json<PatchReminder>,
|
||||
serenity_context: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
cookies: &CookieJar<'_>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, serenity_context.inner(), id);
|
||||
|
||||
let mut error = vec![];
|
||||
|
||||
let user_id =
|
||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||
|
||||
if reminder.message_ok() {
|
||||
update_field!(pool.inner(), error, reminder.[
|
||||
content,
|
||||
embed_author,
|
||||
embed_description,
|
||||
embed_footer,
|
||||
embed_title,
|
||||
embed_fields,
|
||||
username
|
||||
]);
|
||||
} else {
|
||||
error.push("Message exceeds limits.".to_string());
|
||||
}
|
||||
|
||||
update_field!(pool.inner(), error, reminder.[
|
||||
attachment,
|
||||
attachment_name,
|
||||
avatar,
|
||||
content,
|
||||
embed_author,
|
||||
embed_author_url,
|
||||
embed_color,
|
||||
embed_description,
|
||||
embed_footer,
|
||||
embed_footer_url,
|
||||
embed_image_url,
|
||||
embed_thumbnail_url,
|
||||
embed_title,
|
||||
embed_fields,
|
||||
enabled,
|
||||
expires,
|
||||
interval_seconds,
|
||||
interval_months,
|
||||
name,
|
||||
restartable,
|
||||
tts,
|
||||
username,
|
||||
utc_time
|
||||
]);
|
||||
|
||||
if reminder.interval_days.flatten().is_some()
|
||||
|| reminder.interval_months.flatten().is_some()
|
||||
|| reminder.interval_seconds.flatten().is_some()
|
||||
{
|
||||
if check_guild_subscription(&serenity_context.inner(), id).await
|
||||
|| check_subscription(&serenity_context.inner(), user_id).await
|
||||
{
|
||||
let new_interval_length = match reminder.interval_days {
|
||||
Some(interval) => interval.unwrap_or(0),
|
||||
None => sqlx::query!(
|
||||
"SELECT interval_days AS days FROM reminders WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.days
|
||||
.unwrap_or(0),
|
||||
} + match reminder.interval_months {
|
||||
Some(interval) => interval.unwrap_or(0),
|
||||
None => sqlx::query!(
|
||||
"SELECT interval_months AS months FROM reminders WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.months
|
||||
.unwrap_or(0),
|
||||
} + match reminder.interval_seconds {
|
||||
Some(interval) => interval.unwrap_or(0),
|
||||
None => sqlx::query!(
|
||||
"SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.seconds
|
||||
.unwrap_or(0),
|
||||
};
|
||||
|
||||
if new_interval_length < *MIN_INTERVAL {
|
||||
error.push(String::from("New interval is too short."));
|
||||
} else {
|
||||
update_field!(pool.inner(), error, reminder.[
|
||||
interval_days,
|
||||
interval_months,
|
||||
interval_seconds
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if reminder.channel > 0 {
|
||||
let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner());
|
||||
match channel {
|
||||
@ -560,7 +483,6 @@ pub async fn edit_reminder(
|
||||
reminders.enabled,
|
||||
reminders.expires,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.restartable,
|
||||
|
@ -8,13 +8,13 @@ use rocket::{
|
||||
serde::json::{json, Value as JsonValue},
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::{
|
||||
client::Context,
|
||||
http::Http,
|
||||
model::id::{ChannelId, GuildId, UserId},
|
||||
};
|
||||
use sqlx::{types::Json, Executor};
|
||||
use sqlx::{types::Json, Executor, MySql, Pool};
|
||||
|
||||
use crate::{
|
||||
check_guild_subscription, check_subscription,
|
||||
@ -50,18 +50,6 @@ fn id_default() -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
fn interval_default() -> Unset<Option<u32>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
Ok(Some(Option::deserialize(deserializer)?))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ReminderTemplate {
|
||||
#[serde(default = "id_default")]
|
||||
@ -144,7 +132,6 @@ pub struct Reminder {
|
||||
enabled: bool,
|
||||
expires: Option<NaiveDateTime>,
|
||||
interval_seconds: Option<u32>,
|
||||
interval_days: Option<u32>,
|
||||
interval_months: Option<u32>,
|
||||
#[serde(default = "name_default")]
|
||||
name: String,
|
||||
@ -177,7 +164,6 @@ pub struct ReminderCsv {
|
||||
enabled: bool,
|
||||
expires: Option<NaiveDateTime>,
|
||||
interval_seconds: Option<u32>,
|
||||
interval_days: Option<u32>,
|
||||
interval_months: Option<u32>,
|
||||
#[serde(default = "name_default")]
|
||||
name: String,
|
||||
@ -191,13 +177,10 @@ pub struct ReminderCsv {
|
||||
pub struct PatchReminder {
|
||||
uid: String,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
attachment: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
attachment_name: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
avatar: Unset<Option<String>>,
|
||||
#[serde(default = "channel_default")]
|
||||
#[serde(with = "string")]
|
||||
@ -207,7 +190,6 @@ pub struct PatchReminder {
|
||||
#[serde(default)]
|
||||
embed_author: Unset<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
embed_author_url: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
embed_color: Unset<u32>,
|
||||
@ -216,13 +198,10 @@ pub struct PatchReminder {
|
||||
#[serde(default)]
|
||||
embed_footer: Unset<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
embed_footer_url: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
embed_image_url: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
embed_thumbnail_url: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
embed_title: Unset<String>,
|
||||
@ -231,16 +210,10 @@ pub struct PatchReminder {
|
||||
#[serde(default)]
|
||||
enabled: Unset<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
expires: Unset<Option<NaiveDateTime>>,
|
||||
#[serde(default = "interval_default")]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
#[serde(default)]
|
||||
interval_seconds: Unset<Option<u32>>,
|
||||
#[serde(default = "interval_default")]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
interval_days: Unset<Option<u32>>,
|
||||
#[serde(default = "interval_default")]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
#[serde(default)]
|
||||
interval_months: Unset<Option<u32>>,
|
||||
#[serde(default)]
|
||||
name: Unset<String>,
|
||||
@ -249,36 +222,11 @@ pub struct PatchReminder {
|
||||
#[serde(default)]
|
||||
tts: Unset<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
username: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
utc_time: Unset<NaiveDateTime>,
|
||||
}
|
||||
|
||||
impl PatchReminder {
|
||||
fn message_ok(&self) -> bool {
|
||||
self.content.as_ref().map_or(true, |c| c.len() <= MAX_CONTENT_LENGTH)
|
||||
&& self.embed_author.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_AUTHOR_LENGTH)
|
||||
&& self
|
||||
.embed_description
|
||||
.as_ref()
|
||||
.map_or(true, |c| c.len() <= MAX_EMBED_DESCRIPTION_LENGTH)
|
||||
&& self.embed_footer.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_FOOTER_LENGTH)
|
||||
&& self.embed_title.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_TITLE_LENGTH)
|
||||
&& self.embed_fields.as_ref().map_or(true, |c| {
|
||||
c.0.len() <= MAX_EMBED_FIELDS
|
||||
&& c.0.iter().all(|f| {
|
||||
f.title.len() <= MAX_EMBED_FIELD_TITLE_LENGTH
|
||||
&& f.value.len() <= MAX_EMBED_FIELD_VALUE_LENGTH
|
||||
})
|
||||
})
|
||||
&& self
|
||||
.username
|
||||
.as_ref()
|
||||
.map_or(true, |c| c.as_ref().map_or(true, |v| v.len() <= MAX_USERNAME_LENGTH))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_uid() -> String {
|
||||
let mut generator: OsRng = Default::default();
|
||||
|
||||
@ -353,28 +301,11 @@ pub struct TodoCsv {
|
||||
|
||||
pub async fn create_reminder(
|
||||
ctx: &Context,
|
||||
pool: impl sqlx::Executor<'_, Database = Database> + Copy,
|
||||
pool: &Pool<MySql>,
|
||||
guild_id: GuildId,
|
||||
user_id: UserId,
|
||||
reminder: Reminder,
|
||||
) -> JsonResult {
|
||||
// check guild in db
|
||||
match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
{
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.0)
|
||||
.execute(pool)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err(json!({"error": "Guild could not be created"}));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// validate channel
|
||||
let channel = ChannelId(reminder.channel).to_channel_cached(&ctx);
|
||||
let channel_exists = channel.is_some();
|
||||
@ -439,12 +370,8 @@ pub async fn create_reminder(
|
||||
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_days.is_some()
|
||||
|| reminder.interval_months.is_some()
|
||||
{
|
||||
if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() {
|
||||
if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32
|
||||
+ reminder.interval_days.unwrap_or(0) * DAY as u32
|
||||
+ reminder.interval_seconds.unwrap_or(0)
|
||||
< *MIN_INTERVAL
|
||||
{
|
||||
@ -453,10 +380,7 @@ pub async fn create_reminder(
|
||||
}
|
||||
|
||||
// check patreon if necessary
|
||||
if reminder.interval_seconds.is_some()
|
||||
|| reminder.interval_days.is_some()
|
||||
|| reminder.interval_months.is_some()
|
||||
{
|
||||
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
|
||||
{
|
||||
@ -467,11 +391,6 @@ pub async fn create_reminder(
|
||||
// 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 username = if reminder.username.as_ref().map(|s| s.is_empty()).unwrap_or(true) {
|
||||
None
|
||||
} else {
|
||||
reminder.username
|
||||
};
|
||||
|
||||
let new_uid = generate_uid();
|
||||
|
||||
@ -497,14 +416,13 @@ pub async fn create_reminder(
|
||||
enabled,
|
||||
expires,
|
||||
interval_seconds,
|
||||
interval_days,
|
||||
interval_months,
|
||||
name,
|
||||
restartable,
|
||||
tts,
|
||||
username,
|
||||
`utc_time`
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
new_uid,
|
||||
attachment_data,
|
||||
reminder.attachment_name,
|
||||
@ -524,12 +442,11 @@ pub async fn create_reminder(
|
||||
reminder.enabled,
|
||||
reminder.expires,
|
||||
reminder.interval_seconds,
|
||||
reminder.interval_days,
|
||||
reminder.interval_months,
|
||||
name,
|
||||
reminder.restartable,
|
||||
reminder.tts,
|
||||
username,
|
||||
reminder.username,
|
||||
reminder.utc_time,
|
||||
)
|
||||
.execute(pool)
|
||||
@ -556,7 +473,6 @@ pub async fn create_reminder(
|
||||
reminders.enabled,
|
||||
reminders.expires,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.restartable,
|
||||
|
@ -135,14 +135,14 @@ pub async fn discord_callback(
|
||||
Err(Flash::new(
|
||||
Redirect::to(uri!(super::return_to_same_site(""))),
|
||||
"warning",
|
||||
"Your login request was rejected. The server may be misconfigured. Please retry or alert us in Discord.",
|
||||
"Your login request was rejected",
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "danger", "Your request failed to validate, and so has been rejected (CSRF Validation Failure)"))
|
||||
Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "danger", "Your request failed to validate, and so has been rejected (error: CSRF Validation Failure)"))
|
||||
}
|
||||
} else {
|
||||
Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "warning", "Your request was missing information, and so has been rejected (CSRF Validation Tokens Missing)"))
|
||||
Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "warning", "Your request was missing information, and so has been rejected (error: CSRF Validation Tokens Missing)"))
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ function get_interval(element) {
|
||||
|
||||
return {
|
||||
months: parseInt(months) || null,
|
||||
days: parseInt(days) || null,
|
||||
seconds:
|
||||
(parseInt(hours) || 0) * 3600 +
|
||||
(parseInt(days) || 0) * 86400 +
|
||||
(parseInt(hours) || 0) * 3600 +
|
||||
(parseInt(minutes) || 0) * 60 +
|
||||
(parseInt(seconds) || 0) || null,
|
||||
};
|
||||
@ -22,38 +22,32 @@ function update_interval(element) {
|
||||
let minutes = element.querySelector('input[name="interval_minutes"]');
|
||||
let seconds = element.querySelector('input[name="interval_seconds"]');
|
||||
|
||||
let interval = get_interval(element);
|
||||
months.value = months.value.padStart(1, "0");
|
||||
days.value = days.value.padStart(1, "0");
|
||||
hours.value = hours.value.padStart(2, "0");
|
||||
minutes.value = minutes.value.padStart(2, "0");
|
||||
seconds.value = seconds.value.padStart(2, "0");
|
||||
|
||||
if (interval.months === null && interval.days === null && interval.seconds === null) {
|
||||
months.value = "";
|
||||
days.value = "";
|
||||
hours.value = "";
|
||||
minutes.value = "";
|
||||
seconds.value = "";
|
||||
} else {
|
||||
months.value = months.value.padStart(1, "0");
|
||||
days.value = days.value.padStart(1, "0");
|
||||
hours.value = hours.value.padStart(2, "0");
|
||||
minutes.value = minutes.value.padStart(2, "0");
|
||||
seconds.value = seconds.value.padStart(2, "0");
|
||||
if (seconds.value >= 60) {
|
||||
let quotient = Math.floor(seconds.value / 60);
|
||||
let remainder = seconds.value % 60;
|
||||
|
||||
if (seconds.value >= 60) {
|
||||
let quotient = Math.floor(seconds.value / 60);
|
||||
let remainder = seconds.value % 60;
|
||||
seconds.value = String(remainder).padStart(2, "0");
|
||||
minutes.value = String(Number(minutes.value) + Number(quotient)).padStart(2, "0");
|
||||
}
|
||||
if (minutes.value >= 60) {
|
||||
let quotient = Math.floor(minutes.value / 60);
|
||||
let remainder = minutes.value % 60;
|
||||
|
||||
seconds.value = String(remainder).padStart(2, "0");
|
||||
minutes.value = String(Number(minutes.value) + Number(quotient)).padStart(
|
||||
2,
|
||||
"0"
|
||||
);
|
||||
}
|
||||
if (minutes.value >= 60) {
|
||||
let quotient = Math.floor(minutes.value / 60);
|
||||
let remainder = minutes.value % 60;
|
||||
minutes.value = String(remainder).padStart(2, "0");
|
||||
hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0");
|
||||
}
|
||||
if (hours.value >= 24) {
|
||||
let quotient = Math.floor(hours.value / 24);
|
||||
let remainder = hours.value % 24;
|
||||
|
||||
minutes.value = String(remainder).padStart(2, "0");
|
||||
hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0");
|
||||
}
|
||||
hours.value = String(remainder).padStart(2, "0");
|
||||
days.value = Number(days.value) + Number(quotient);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,15 +60,14 @@ function update_select(sel) {
|
||||
sel.closest("div.reminderContent").querySelector("img.discord-avatar").src =
|
||||
sel.selectedOptions[0].dataset["webhookAvatar"];
|
||||
} else {
|
||||
sel.closest("div.reminderContent").querySelector("img.discord-avatar").src =
|
||||
"/static/img/icon.png";
|
||||
sel.closest("div.reminderContent").querySelector("img.discord-avatar").src = "";
|
||||
}
|
||||
if (sel.selectedOptions[0].dataset["webhookName"]) {
|
||||
sel.closest("div.reminderContent").querySelector("input.discord-username").value =
|
||||
sel.selectedOptions[0].dataset["webhookName"];
|
||||
} else {
|
||||
sel.closest("div.reminderContent").querySelector("input.discord-username").value =
|
||||
"Reminder";
|
||||
"";
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,7 +319,6 @@ async function serialize_reminder(node, mode) {
|
||||
embed_fields: fields,
|
||||
expires: expiration_time,
|
||||
interval_seconds: mode !== "template" ? interval.seconds : null,
|
||||
interval_days: mode !== "template" ? interval.days : null,
|
||||
interval_months: mode !== "template" ? interval.months : null,
|
||||
name: node.querySelector('input[name="name"]').value,
|
||||
tts: node.querySelector('input[name="tts"]').checked,
|
||||
@ -333,9 +331,6 @@ function deserialize_reminder(reminder, frame, mode) {
|
||||
// populate channels
|
||||
set_channels(frame.querySelector("select.channel-selector"));
|
||||
|
||||
frame.querySelector(`*[name="interval_hours"]`).value = 0;
|
||||
frame.querySelector(`*[name="interval_minutes"]`).value = 0;
|
||||
|
||||
// populate majority of items
|
||||
for (let prop in reminder) {
|
||||
if (reminder.hasOwnProperty(prop) && reminder[prop] !== null) {
|
||||
@ -356,8 +351,6 @@ function deserialize_reminder(reminder, frame, mode) {
|
||||
}
|
||||
}
|
||||
|
||||
update_interval(frame);
|
||||
|
||||
const lastChild = frame.querySelector("div.embed-multifield-box .embed-field-box");
|
||||
|
||||
for (let field of reminder["embed_fields"]) {
|
||||
@ -504,8 +497,6 @@ document.addEventListener("remindersLoaded", (event) => {
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
for (let error of data.errors) show_error(error);
|
||||
|
||||
deserialize_reminder(data.reminder, node, "reload");
|
||||
});
|
||||
|
||||
$saveBtn.querySelector("span.icon > i").classList = ["fas fa-check"];
|
||||
@ -724,7 +715,6 @@ $createReminderBtn.addEventListener("click", async () => {
|
||||
let reminder = await serialize_reminder($createReminder, "create");
|
||||
if (reminder.error) {
|
||||
show_error(reminder.error);
|
||||
$createReminderBtn.querySelector("span.icon > i").classList = ["fas fa-sparkles"];
|
||||
return;
|
||||
}
|
||||
|
||||
@ -842,6 +832,13 @@ $deleteTemplateBtn.addEventListener("click", (ev) => {
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("textarea.autoresize").forEach((element) => {
|
||||
element.addEventListener("input", () => {
|
||||
element.style.height = "";
|
||||
element.style.height = element.scrollHeight + 3 + "px";
|
||||
});
|
||||
});
|
||||
|
||||
let $img;
|
||||
const $urlModal = document.querySelector("div#addImageModal");
|
||||
const $urlInput = $urlModal.querySelector("input");
|
||||
@ -897,13 +894,6 @@ document.addEventListener("remindersLoaded", () => {
|
||||
window.getComputedStyle($discordFrame).borderLeftColor;
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("textarea.autoresize").forEach((element) => {
|
||||
element.addEventListener("input", () => {
|
||||
element.style.height = "";
|
||||
element.style.height = element.scrollHeight + 3 + "px";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function check_embed_fields() {
|
||||
|
@ -191,8 +191,19 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="field">
|
||||
<label>
|
||||
<input type="radio" class="default-width" name="exportSelect" value="reminder_templates">
|
||||
Reminder templates
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<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>
|
||||
|
@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<div class="tile is-parent">
|
||||
<article class="tile is-child notification">
|
||||
<p class="title">Create reminders</p>
|
||||
<p class="title">Creating reminders</p>
|
||||
<p class="subtitle">Learn to create reminders for your server</p>
|
||||
<div class="content has-text-centered">
|
||||
<a class="button is-size-4 is-rounded is-light" href="/help/create_reminder">
|
||||
@ -52,47 +52,47 @@
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="tile is-ancestor">-->
|
||||
<!-- <div class="tile is-parent">-->
|
||||
<!-- <article class="tile is-child notification">-->
|
||||
<!-- <p class="title">Timers</p>-->
|
||||
<!-- <p class="subtitle">Learn to manage timers</p>-->
|
||||
<!-- <div class="content has-text-centered">-->
|
||||
<!-- <a class="button is-size-4 is-rounded is-light" href="/help/timers">-->
|
||||
<!-- <p class="is-size-4">-->
|
||||
<!-- Read <span class="icon"><i class="fas fa-chevron-right"></i></span>-->
|
||||
<!-- </p>-->
|
||||
<!-- </a>-->
|
||||
<!-- </div>-->
|
||||
<!-- </article>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="tile is-parent">-->
|
||||
<!-- <article class="tile is-child notification">-->
|
||||
<!-- <p class="title">Todo Lists</p>-->
|
||||
<!-- <p class="subtitle">Learn to manage various todo lists</p>-->
|
||||
<!-- <div class="content has-text-centered">-->
|
||||
<!-- <a class="button is-size-4 is-rounded is-light" href="/help/todo_lists">-->
|
||||
<!-- <p class="is-size-4">-->
|
||||
<!-- Read <span class="icon"><i class="fas fa-chevron-right"></i></span>-->
|
||||
<!-- </p>-->
|
||||
<!-- </a>-->
|
||||
<!-- </div>-->
|
||||
<!-- </article>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="tile is-parent is-vertical">-->
|
||||
<!-- <article class="tile is-child notification">-->
|
||||
<!-- <p class="title">Macros</p>-->
|
||||
<!-- <p class="subtitle">Learn how to create combination commands called macros, to suit advanced use cases</p>-->
|
||||
<!-- <div class="content has-text-centered">-->
|
||||
<!-- <a class="button is-size-4 is-rounded is-light" href="/help/macros">-->
|
||||
<!-- <p class="is-size-4">-->
|
||||
<!-- Read <span class="icon"><i class="fas fa-chevron-right"></i></span>-->
|
||||
<!-- </p>-->
|
||||
<!-- </a>-->
|
||||
<!-- </div>-->
|
||||
<!-- </article>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-parent">
|
||||
<article class="tile is-child notification">
|
||||
<p class="title">Timers</p>
|
||||
<p class="subtitle">Learn to manage timers</p>
|
||||
<div class="content has-text-centered">
|
||||
<a class="button is-size-4 is-rounded is-light" href="/help/timers">
|
||||
<p class="is-size-4">
|
||||
Read <span class="icon"><i class="fas fa-chevron-right"></i></span>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="tile is-parent">
|
||||
<article class="tile is-child notification">
|
||||
<p class="title">Todo Lists</p>
|
||||
<p class="subtitle">Learn to manage various todo lists</p>
|
||||
<div class="content has-text-centered">
|
||||
<a class="button is-size-4 is-rounded is-light" href="/help/todo_lists">
|
||||
<p class="is-size-4">
|
||||
Read <span class="icon"><i class="fas fa-chevron-right"></i></span>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="tile is-parent is-vertical">
|
||||
<article class="tile is-child notification">
|
||||
<p class="title">Macros</p>
|
||||
<p class="subtitle">Learn how to create combination commands called macros, to suit advanced use cases</p>
|
||||
<div class="content has-text-centered">
|
||||
<a class="button is-size-4 is-rounded is-light" href="/help/macros">
|
||||
<p class="is-size-4">
|
||||
Read <span class="icon"><i class="fas fa-chevron-right"></i></span>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-parent">
|
||||
<article class="tile is-child notification">
|
||||
@ -107,6 +107,19 @@
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="tile is-parent">
|
||||
<article class="tile is-child notification">
|
||||
<p class="title">Dashboard</p>
|
||||
<p class="subtitle">Learn to use the interactive web dashboard</p>
|
||||
<div class="content has-text-centered">
|
||||
<a class="button is-size-4 is-rounded is-light" href="/help/dashboard">
|
||||
<p class="is-size-4">
|
||||
Read <span class="icon"><i class="fas fa-chevron-right"></i></span>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="tile is-parent is-vertical">
|
||||
<article class="tile is-child notification">
|
||||
<p class="title">Import/Export</p>
|
||||
@ -120,19 +133,6 @@
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="tile is-parent">
|
||||
<!-- <article class="tile is-child notification">-->
|
||||
<!-- <p class="title">Dashboard</p>-->
|
||||
<!-- <p class="subtitle">Learn to use the interactive web dashboard</p>-->
|
||||
<!-- <div class="content has-text-centered">-->
|
||||
<!-- <a class="button is-size-4 is-rounded is-light" href="/help/dashboard">-->
|
||||
<!-- <p class="is-size-4">-->
|
||||
<!-- Read <span class="icon"><i class="fas fa-chevron-right"></i></span>-->
|
||||
<!-- </p>-->
|
||||
<!-- </a>-->
|
||||
<!-- </div>-->
|
||||
<!-- </article>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -28,10 +28,7 @@
|
||||
<div class="container">
|
||||
<p class="title">Create reminders via the dashboard</p>
|
||||
<p class="content">
|
||||
Reminders can also be created on the dashboard. The dashboard offers more options for configuring
|
||||
reminders, and offers templates for quick recreation of reminders.
|
||||
|
||||
<a href="/dashboard">Access the dashboard.</a>
|
||||
Reminders can also be created on the dashboard.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,7 +12,7 @@
|
||||
<section class="hero is-small">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<p class="title">Export data</p>
|
||||
<p class="title">Export your data</p>
|
||||
<p class="content">
|
||||
You can export data associated with your server from the dashboard. The data will export as a CSV
|
||||
file. The CSV file can then be edited and imported to bulk edit server data.
|
||||
@ -26,7 +26,8 @@
|
||||
<div class="container">
|
||||
<p class="title">Import data</p>
|
||||
<p class="content">
|
||||
You can import previous exports or modified exports. When importing a file, the new data will be added alongside existing data.
|
||||
You can import previous exports or modified exports. When importing a file, <strong>existing data
|
||||
will be overwritten</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -54,7 +55,7 @@
|
||||
</figure>
|
||||
</li>
|
||||
<li>
|
||||
Make edits to the spreadsheet. You can add, edit, and remove rows for reminders. Don't remove the top-most (title) row.
|
||||
Make edits to the spreadsheet. You can add, edit, and remove rows for reminders. Don't remove the title row.
|
||||
<figure>
|
||||
<img src="/static/img/support/iemanager/edit_spreadsheet.png" alt="Editing spreadsheet">
|
||||
</figure>
|
||||
@ -69,7 +70,7 @@
|
||||
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>.
|
||||
<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">
|
||||
|
@ -49,7 +49,7 @@
|
||||
<p class="content">
|
||||
Monthly or yearly intervals are configured the same as fixed intervals. Instead of a fixed time
|
||||
interval, these reminders repeat on a certain day each month or each year. This makes them ideal
|
||||
for marking calendar events.
|
||||
for marking certain dates.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -61,8 +61,7 @@
|
||||
<p class="title">Interval expiration</p>
|
||||
<p class="content">
|
||||
An expiration time can also be specified, both via commands and dashboard, for repeating reminders.
|
||||
This is optional, and if omitted, the reminder will repeat indefinitely. Otherwise, the reminder
|
||||
will be deleted once the expiration date is reached.
|
||||
This is optional, and if omitted, the reminder will repeat indefinitely.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user