Compare commits
26 Commits
next
...
jude/new-c
Author | SHA1 | Date | |
---|---|---|---|
e7160215b0 | |||
6eaa6f0f28 | |||
9db0fa2513 | |||
ca13fd4fa7 | |||
55acc8fd16 | |||
145711fa5d | |||
5524215786 | |||
e8bd05893f | |||
e3d3418f99 | |||
2681280a39 | |||
00579428a1 | |||
b8ef999710 | |||
e8f84e281a | |||
8ddff698e5 | |||
541633270c | |||
25286da5e0 | |||
4bad1324b9 | |||
bd1462a00c | |||
56ffc43616 | |||
52cf642455 | |||
0bf578357a | |||
6e9eccb62e | |||
6ea28284ce | |||
a6525f3052 | |||
348639270d | |||
37177c2431 |
106
Cargo.lock
generated
106
Cargo.lock
generated
@ -59,7 +59,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.2.10",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
@ -345,7 +345,7 @@ dependencies = [
|
||||
"base64 0.21.4",
|
||||
"hkdf",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"time",
|
||||
@ -437,7 +437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"rand_core",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
@ -910,17 +910,6 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.10"
|
||||
@ -930,7 +919,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@ -1507,7 +1496,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
@ -1619,7 +1608,7 @@ dependencies = [
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
]
|
||||
@ -1673,9 +1662,9 @@ checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"chrono",
|
||||
"getrandom 0.2.10",
|
||||
"getrandom",
|
||||
"http",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1912,7 +1901,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2079,19 +2068,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
"libc",
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@ -2099,18 +2075,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.5.1",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2120,16 +2086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2138,16 +2095,7 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2225,7 +2173,7 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||
|
||||
[[package]]
|
||||
name = "reminder-rs"
|
||||
version = "1.6.40"
|
||||
version = "1.6.47"
|
||||
dependencies = [
|
||||
"base64 0.21.4",
|
||||
"chrono",
|
||||
@ -2239,7 +2187,7 @@ dependencies = [
|
||||
"num-integer",
|
||||
"poise",
|
||||
"postman",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"regex",
|
||||
"reminder_web",
|
||||
"reqwest",
|
||||
@ -2253,7 +2201,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reminder_web"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"chrono",
|
||||
@ -2262,7 +2210,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"oauth2",
|
||||
"rand 0.7.3",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"rocket",
|
||||
"rocket_dyn_templates",
|
||||
@ -2373,7 +2321,7 @@ dependencies = [
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"ref-cast",
|
||||
"rocket_codegen",
|
||||
"rocket_http",
|
||||
@ -2462,7 +2410,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"rand_core",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle",
|
||||
@ -2779,7 +2727,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"rand_core 0.6.4",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2996,7 +2944,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"rsa",
|
||||
"serde",
|
||||
"sha1",
|
||||
@ -3038,7 +2986,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"num-bigint",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
@ -3164,7 +3112,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -3461,7 +3409,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"rustls 0.20.9",
|
||||
"sha-1",
|
||||
"thiserror",
|
||||
@ -3676,12 +3624,6 @@ dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reminder-rs"
|
||||
version = "1.6.40"
|
||||
version = "1.6.48"
|
||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0 only"
|
||||
@ -11,7 +11,7 @@ poise = "0.5"
|
||||
dotenv = "0.15"
|
||||
tokio = { version = "1", features = ["process", "full"] }
|
||||
reqwest = "0.11"
|
||||
lazy-regex = "3.0"
|
||||
lazy-regex = "3.0.2"
|
||||
regex = "1.9"
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
|
@ -1,19 +0,0 @@
|
||||
-- Drop existing constraint
|
||||
ALTER TABLE `reminders` DROP CONSTRAINT `reminders_ibfk_1`;
|
||||
|
||||
ALTER TABLE `reminders` MODIFY COLUMN `channel_id` INT UNSIGNED;
|
||||
ALTER TABLE `reminders` ADD COLUMN `guild_id` INT UNSIGNED;
|
||||
|
||||
ALTER TABLE `reminders`
|
||||
ADD CONSTRAINT `guild_id_fk`
|
||||
FOREIGN KEY (`guild_id`)
|
||||
REFERENCES `guilds`(`id`)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE `reminders`
|
||||
ADD CONSTRAINT `channel_id_fk`
|
||||
FOREIGN KEY (`channel_id`)
|
||||
REFERENCES `channels`(`id`)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
UPDATE `reminders` SET `guild_id` = (SELECT guilds.`id` FROM `channels` INNER JOIN `guilds` ON channels.guild_id = guilds.id WHERE reminders.channel_id = channels.id);
|
@ -1,4 +0,0 @@
|
||||
ALTER TABLE reminders ADD COLUMN `status_change_time` DATETIME;
|
||||
|
||||
-- This is a best-guess as to the status change time.
|
||||
UPDATE reminders SET `status_change_time` = `utc_time` WHERE `status` != 'pending';
|
@ -0,0 +1,3 @@
|
||||
ALTER TABLE `reminder_template` ADD COLUMN `interval_seconds` INT UNSIGNED;
|
||||
ALTER TABLE `reminder_template` ADD COLUMN `interval_days` INT UNSIGNED;
|
||||
ALTER TABLE `reminder_template` ADD COLUMN `interval_months` INT UNSIGNED;
|
@ -237,11 +237,11 @@ impl Into<CreateEmbed> for Embed {
|
||||
pub struct Reminder {
|
||||
id: u32,
|
||||
|
||||
channel_id: Option<u64>,
|
||||
channel_id: u64,
|
||||
webhook_id: Option<u64>,
|
||||
webhook_token: Option<String>,
|
||||
|
||||
channel_paused: Option<bool>,
|
||||
channel_paused: bool,
|
||||
channel_paused_until: Option<NaiveDateTime>,
|
||||
enabled: bool,
|
||||
|
||||
@ -297,7 +297,7 @@ SELECT
|
||||
reminders.`username` AS username
|
||||
FROM
|
||||
reminders
|
||||
LEFT JOIN
|
||||
INNER JOIN
|
||||
channels
|
||||
ON
|
||||
reminders.channel_id = channels.id
|
||||
@ -343,10 +343,7 @@ 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)
|
||||
@ -418,9 +415,7 @@ WHERE
|
||||
self.set_sent(pool).await;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE reminders SET `utc_time` = ? WHERE `id` = ?
|
||||
",
|
||||
"UPDATE reminders SET `utc_time` = ? WHERE `id` = ?",
|
||||
updated_reminder_time.with_timezone(&Utc),
|
||||
self.id
|
||||
)
|
||||
@ -453,10 +448,7 @@ WHERE
|
||||
|
||||
if *LOG_TO_DATABASE {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO stat (type, reminder_id, message)
|
||||
VALUES ('reminder_failed', ?, ?)
|
||||
",
|
||||
"INSERT INTO stat (type, reminder_id, message) VALUES ('reminder_failed', ?, ?)",
|
||||
self.id,
|
||||
message,
|
||||
)
|
||||
@ -469,10 +461,7 @@ WHERE
|
||||
async fn log_success(&self, pool: impl Executor<'_, Database = Database> + Copy) {
|
||||
if *LOG_TO_DATABASE {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO stat (type, reminder_id)
|
||||
VALUES ('reminder_sent', ?)
|
||||
",
|
||||
"INSERT INTO stat (type, reminder_id) VALUES ('reminder_sent', ?)",
|
||||
self.id,
|
||||
)
|
||||
.execute(pool)
|
||||
@ -482,14 +471,7 @@ WHERE
|
||||
}
|
||||
|
||||
async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE reminders
|
||||
SET `status` = 'sent', `status_change_time` = NOW()
|
||||
WHERE `id` = ?
|
||||
",
|
||||
self.id
|
||||
)
|
||||
sqlx::query!("UPDATE reminders SET `status` = 'sent' WHERE `id` = ?", self.id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect(&format!("Could not delete Reminder {}", self.id));
|
||||
@ -501,11 +483,7 @@ WHERE
|
||||
message: &'static str,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE reminders
|
||||
SET `status` = 'failed', `status_message` = ?, `status_change_time` = NOW()
|
||||
WHERE `id` = ?
|
||||
",
|
||||
"UPDATE reminders SET `status` = 'failed', `status_message` = ? WHERE `id` = ?",
|
||||
message,
|
||||
self.id
|
||||
)
|
||||
@ -515,9 +493,7 @@ WHERE
|
||||
}
|
||||
|
||||
async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) {
|
||||
if let Some(channel_id) = self.channel_id {
|
||||
let _ = http.as_ref().pin_message(channel_id, message_id.into(), None).await;
|
||||
}
|
||||
let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await;
|
||||
}
|
||||
|
||||
pub async fn send(
|
||||
@ -527,11 +503,10 @@ WHERE
|
||||
) {
|
||||
async fn send_to_channel(
|
||||
cache_http: impl CacheHttp,
|
||||
channel_id: u64,
|
||||
reminder: &Reminder,
|
||||
embed: Option<CreateEmbed>,
|
||||
) -> Result<()> {
|
||||
let channel = ChannelId(channel_id).to_channel(&cache_http).await;
|
||||
let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await;
|
||||
|
||||
match channel {
|
||||
Ok(Channel::Guild(channel)) => {
|
||||
@ -563,7 +538,6 @@ WHERE
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Channel::Private(channel)) => {
|
||||
match channel
|
||||
.send_message(&cache_http.http(), |m| {
|
||||
@ -593,9 +567,7 @@ WHERE
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => Err(e),
|
||||
|
||||
_ => Err(Error::Other("Channel not of valid type")),
|
||||
}
|
||||
}
|
||||
@ -650,20 +622,14 @@ WHERE
|
||||
}
|
||||
}
|
||||
|
||||
match self.channel_id {
|
||||
Some(channel_id) => {
|
||||
if self.enabled
|
||||
&& !(self.channel_paused.unwrap_or(false)
|
||||
&& !(self.channel_paused
|
||||
&& self
|
||||
.channel_paused_until
|
||||
.map_or(true, |inner| inner >= Utc::now().naive_local()))
|
||||
{
|
||||
let _ = sqlx::query!(
|
||||
"
|
||||
UPDATE `channels`
|
||||
SET paused = 0, paused_until = NULL
|
||||
WHERE `channel` = ?
|
||||
",
|
||||
"UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?",
|
||||
self.channel_id
|
||||
)
|
||||
.execute(pool)
|
||||
@ -674,10 +640,8 @@ WHERE
|
||||
let result = if let (Some(webhook_id), Some(webhook_token)) =
|
||||
(self.webhook_id, &self.webhook_token)
|
||||
{
|
||||
let webhook_res = cache_http
|
||||
.http()
|
||||
.get_webhook_with_token(webhook_id, webhook_token)
|
||||
.await;
|
||||
let webhook_res =
|
||||
cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await;
|
||||
|
||||
if let Ok(webhook) = webhook_res {
|
||||
send_to_webhook(cache_http, &self, webhook, embed).await
|
||||
@ -685,10 +649,10 @@ WHERE
|
||||
warn!("Webhook vanished for reminder {}: {:?}", self.id, webhook_res);
|
||||
|
||||
self.reset_webhook(pool).await;
|
||||
send_to_channel(cache_http, channel_id, &self, embed).await
|
||||
send_to_channel(cache_http, &self, embed).await
|
||||
}
|
||||
} else {
|
||||
send_to_channel(cache_http, channel_id, &self, embed).await
|
||||
send_to_channel(cache_http, &self, embed).await
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
@ -715,10 +679,7 @@ WHERE
|
||||
None::<&'static str>,
|
||||
)
|
||||
.await;
|
||||
self.set_failed(
|
||||
pool,
|
||||
"Could not be sent as guild does not exist",
|
||||
)
|
||||
self.set_failed(pool, "Could not be sent as guild does not exist")
|
||||
.await;
|
||||
}
|
||||
50001 => {
|
||||
@ -728,11 +689,7 @@ WHERE
|
||||
None::<&'static str>,
|
||||
)
|
||||
.await;
|
||||
self.set_failed(
|
||||
pool,
|
||||
"Could not be sent as missing access",
|
||||
)
|
||||
.await;
|
||||
self.set_failed(pool, "Could not be sent as missing access").await;
|
||||
}
|
||||
50007 => {
|
||||
self.log_error(
|
||||
@ -741,10 +698,7 @@ WHERE
|
||||
None::<&'static str>,
|
||||
)
|
||||
.await;
|
||||
self.set_failed(
|
||||
pool,
|
||||
"Could not be sent as user has DMs disabled",
|
||||
)
|
||||
self.set_failed(pool, "Could not be sent as user has DMs disabled")
|
||||
.await;
|
||||
}
|
||||
50013 => {
|
||||
@ -788,13 +742,4 @@ WHERE
|
||||
self.refresh(pool).await;
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
info!("Reminder {} is orphaned", self.id);
|
||||
|
||||
self.log_error(pool, "Orphaned", Option::<u8>::None).await;
|
||||
self.set_failed(pool, "Could not be sent as channel was deleted").await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,6 +114,8 @@ pub async fn offset(
|
||||
#[description = "Number of minutes to offset by"] minutes: Option<isize>,
|
||||
#[description = "Number of seconds to offset by"] seconds: Option<isize>,
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
let combined_time = hours.map_or(0, |h| h * HOUR as isize)
|
||||
+ minutes.map_or(0, |m| m * MINUTE as isize)
|
||||
+ seconds.map_or(0, |s| s);
|
||||
@ -619,7 +621,7 @@ pub async fn multiline(
|
||||
)]
|
||||
pub async fn remind(
|
||||
ctx: ApplicationContext<'_>,
|
||||
#[description = "A description of the time to set the reminder for"]
|
||||
#[description = "The time (and optionally date) to set the reminder for"]
|
||||
#[autocomplete = "time_hint_autocomplete"]
|
||||
time: String,
|
||||
#[description = "The message content to send"] content: String,
|
||||
@ -773,7 +775,7 @@ async fn create_reminder(
|
||||
b.emoji(ReactionType::Unicode("📝".to_string()))
|
||||
.label("Edit")
|
||||
.style(ButtonStyle::Link)
|
||||
.url("https://reminder-bot.com/dashboard")
|
||||
.url("https://beta.reminder-bot.com/dashboard")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -166,21 +166,15 @@ impl ComponentDataModel {
|
||||
.await;
|
||||
}
|
||||
ComponentDataModel::DelSelector(selector) => {
|
||||
for id in &component.data.values {
|
||||
match id.parse::<u32>() {
|
||||
Ok(id) => {
|
||||
if let Some(reminder) = Reminder::from_id(&data.database, id).await {
|
||||
reminder.delete(&data.database).await.unwrap();
|
||||
} else {
|
||||
warn!("Attempt to delete non-existent reminder");
|
||||
}
|
||||
}
|
||||
let selected_id = component.data.values.join(",");
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error casting ID to integer: {:?}.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
sqlx::query!(
|
||||
"UPDATE reminders SET `status` = 'deleted' WHERE FIND_IN_SET(id, ?)",
|
||||
selected_id
|
||||
)
|
||||
.execute(&data.database)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let reminders = Reminder::from_guild(
|
||||
&ctx,
|
||||
|
@ -150,7 +150,7 @@ impl<'a> Parser<'a> {
|
||||
"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),
|
||||
"months" | "month" => (n, 0, 0, 0),
|
||||
"years" | "year" | "y" => (n.mul(12)?, 0, 0, 0),
|
||||
_ => {
|
||||
return Err(Error::UnknownUnit {
|
||||
@ -255,7 +255,7 @@ 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()
|
||||
Parser { iter: s.to_lowercase().chars(), src: &s.to_lowercase(), current: (0, 0, 0, 0) }.parse()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -324,4 +324,13 @@ mod tests {
|
||||
assert_eq!(interval.day, 0);
|
||||
assert_eq!(interval.month, 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_case() {
|
||||
let interval = parse_duration("200 Seconds").unwrap();
|
||||
|
||||
assert_eq!(interval.sec, 200);
|
||||
assert_eq!(interval.day, 0);
|
||||
assert_eq!(interval.month, 0);
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ pub struct ChannelData {
|
||||
pub webhook_id: Option<u64>,
|
||||
pub webhook_token: Option<String>,
|
||||
pub paused: bool,
|
||||
pub db_guild_id: Option<u32>,
|
||||
pub paused_until: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
@ -23,11 +22,7 @@ impl ChannelData {
|
||||
|
||||
if let Ok(c) = sqlx::query_as_unchecked!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until,
|
||||
guild_id AS db_guild_id
|
||||
FROM channels WHERE channel = ?
|
||||
",
|
||||
"SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?",
|
||||
channel_id
|
||||
)
|
||||
.fetch_one(pool)
|
||||
@ -35,18 +30,12 @@ impl ChannelData {
|
||||
{
|
||||
Ok(c)
|
||||
} else {
|
||||
let props =
|
||||
channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name));
|
||||
let props = channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name));
|
||||
|
||||
let (guild_id, channel_name) =
|
||||
if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
|
||||
let (guild_id, channel_name) = if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
|
||||
|
||||
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 (?, ?, (SELECT id FROM guilds WHERE guild = ?))",
|
||||
channel_id,
|
||||
channel_name,
|
||||
guild_id
|
||||
@ -57,10 +46,7 @@ impl ChannelData {
|
||||
Ok(sqlx::query_as_unchecked!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused,
|
||||
paused_until, guild_id AS db_guild_id
|
||||
FROM channels
|
||||
WHERE channel = ?
|
||||
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?
|
||||
",
|
||||
channel_id
|
||||
)
|
||||
@ -72,10 +58,8 @@ impl ChannelData {
|
||||
pub async fn commit_changes(&self, pool: &MySqlPool) {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE channels
|
||||
SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?,
|
||||
paused = ?, paused_until = ?
|
||||
WHERE id = ?
|
||||
UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until \
|
||||
= ? WHERE id = ?
|
||||
",
|
||||
self.name,
|
||||
self.nudge,
|
||||
|
@ -51,7 +51,6 @@ pub struct ReminderBuilder {
|
||||
pool: MySqlPool,
|
||||
uid: String,
|
||||
channel: u32,
|
||||
guild: Option<u32>,
|
||||
thread_id: Option<u64>,
|
||||
utc_time: NaiveDateTime,
|
||||
timezone: String,
|
||||
@ -87,7 +86,6 @@ impl ReminderBuilder {
|
||||
INSERT INTO reminders (
|
||||
`uid`,
|
||||
`channel_id`,
|
||||
`guild_id`,
|
||||
`utc_time`,
|
||||
`timezone`,
|
||||
`interval_seconds`,
|
||||
@ -112,13 +110,11 @@ INSERT INTO reminders (
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?
|
||||
)
|
||||
",
|
||||
self.uid,
|
||||
self.channel,
|
||||
self.guild,
|
||||
utc_time,
|
||||
self.timezone,
|
||||
self.interval_seconds,
|
||||
@ -251,10 +247,10 @@ impl<'a> MultiReminderBuilder<'a> {
|
||||
{
|
||||
Err(ReminderError::UserBlockedDm)
|
||||
} else {
|
||||
Ok((user_data.dm_channel, None))
|
||||
Ok(user_data.dm_channel)
|
||||
}
|
||||
} else {
|
||||
Ok((user_data.dm_channel, None))
|
||||
Ok(user_data.dm_channel)
|
||||
}
|
||||
} else {
|
||||
Err(ReminderError::InvalidTag)
|
||||
@ -301,13 +297,13 @@ impl<'a> MultiReminderBuilder<'a> {
|
||||
.commit_changes(&self.ctx.data().database)
|
||||
.await;
|
||||
|
||||
Ok((channel_data.id, channel_data.db_guild_id))
|
||||
Ok(channel_data.id)
|
||||
}
|
||||
|
||||
Err(e) => Err(ReminderError::DiscordError(e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Ok((channel_data.id, channel_data.db_guild_id))
|
||||
Ok(channel_data.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -321,8 +317,7 @@ impl<'a> MultiReminderBuilder<'a> {
|
||||
let builder = ReminderBuilder {
|
||||
pool: self.ctx.data().database.clone(),
|
||||
uid: generate_uid(),
|
||||
channel: c.0,
|
||||
guild: c.1,
|
||||
channel: c,
|
||||
thread_id,
|
||||
utc_time: self.utc_time,
|
||||
timezone: self.timezone.to_string(),
|
||||
|
@ -304,10 +304,7 @@ WHERE
|
||||
&self,
|
||||
db: impl Executor<'_, Database = Database>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
"UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?",
|
||||
self.uid
|
||||
)
|
||||
sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", self.uid)
|
||||
.execute(db)
|
||||
.await
|
||||
.map(|_| ())
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reminder_web"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||
edition = "2018"
|
||||
|
||||
@ -16,6 +16,6 @@ sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql",
|
||||
chrono = "0.4"
|
||||
chrono-tz = "0.8"
|
||||
lazy_static = "1.4.0"
|
||||
rand = "0.7"
|
||||
rand = "0.8"
|
||||
base64 = "0.13"
|
||||
csv = "1.1"
|
||||
csv = "1.2"
|
||||
|
40
web/src/catchers.rs
Normal file
40
web/src/catchers.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rocket::serde::json::json;
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::JsonValue;
|
||||
|
||||
#[catch(403)]
|
||||
pub(crate) async fn forbidden() -> Template {
|
||||
let map: HashMap<String, String> = HashMap::new();
|
||||
Template::render("errors/403", &map)
|
||||
}
|
||||
|
||||
#[catch(500)]
|
||||
pub(crate) async fn internal_server_error() -> Template {
|
||||
let map: HashMap<String, String> = HashMap::new();
|
||||
Template::render("errors/500", &map)
|
||||
}
|
||||
|
||||
#[catch(401)]
|
||||
pub(crate) async fn not_authorized() -> Template {
|
||||
let map: HashMap<String, String> = HashMap::new();
|
||||
Template::render("errors/401", &map)
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
pub(crate) async fn not_found() -> Template {
|
||||
let map: HashMap<String, String> = HashMap::new();
|
||||
Template::render("errors/404", &map)
|
||||
}
|
||||
|
||||
#[catch(413)]
|
||||
pub(crate) async fn payload_too_large() -> JsonValue {
|
||||
json!({"error": "Data too large.", "errors": ["Data too large."]})
|
||||
}
|
||||
|
||||
#[catch(422)]
|
||||
pub(crate) async fn unprocessable_entity() -> JsonValue {
|
||||
json!({"error": "Invalid request.", "errors": ["Invalid request."]})
|
||||
}
|
1
web/src/guards/mod.rs
Normal file
1
web/src/guards/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub(crate) mod transaction;
|
44
web/src/guards/transaction.rs
Normal file
44
web/src/guards/transaction.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use rocket::{
|
||||
http::Status,
|
||||
request::{FromRequest, Outcome},
|
||||
Request, State,
|
||||
};
|
||||
use sqlx::Pool;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
pub struct Transaction<'a>(sqlx::Transaction<'a, Database>);
|
||||
|
||||
impl Transaction<'_> {
|
||||
pub fn executor(&mut self) -> impl sqlx::Executor<'_, Database = Database> {
|
||||
&mut *(self.0)
|
||||
}
|
||||
|
||||
pub async fn commit(self) -> Result<(), sqlx::Error> {
|
||||
self.0.commit().await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TransactionError {
|
||||
Error(sqlx::Error),
|
||||
Missing,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for Transaction<'r> {
|
||||
type Error = TransactionError;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
match request.guard::<&State<Pool<Database>>>().await {
|
||||
Outcome::Success(pool) => match pool.begin().await {
|
||||
Ok(transaction) => Outcome::Success(Transaction(transaction)),
|
||||
Err(e) => {
|
||||
Outcome::Failure((Status::InternalServerError, TransactionError::Error(e)))
|
||||
}
|
||||
},
|
||||
Outcome::Failure(e) => Outcome::Failure((e.0, TransactionError::Missing)),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
142
web/src/lib.rs
142
web/src/lib.rs
@ -4,13 +4,16 @@ extern crate rocket;
|
||||
mod consts;
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod catchers;
|
||||
mod guards;
|
||||
mod routes;
|
||||
|
||||
use std::{collections::HashMap, env, path::Path};
|
||||
use std::{env, path::Path};
|
||||
|
||||
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
|
||||
use rocket::{
|
||||
fs::FileServer,
|
||||
http::CookieJar,
|
||||
serde::json::{json, Value as JsonValue},
|
||||
tokio::sync::broadcast::Sender,
|
||||
};
|
||||
@ -32,40 +35,6 @@ enum Error {
|
||||
Serenity(serenity::Error),
|
||||
}
|
||||
|
||||
#[catch(401)]
|
||||
async fn not_authorized() -> Template {
|
||||
let map: HashMap<String, String> = HashMap::new();
|
||||
Template::render("errors/401", &map)
|
||||
}
|
||||
|
||||
#[catch(403)]
|
||||
async fn forbidden() -> Template {
|
||||
let map: HashMap<String, String> = HashMap::new();
|
||||
Template::render("errors/403", &map)
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
async fn not_found() -> Template {
|
||||
let map: HashMap<String, String> = HashMap::new();
|
||||
Template::render("errors/404", &map)
|
||||
}
|
||||
|
||||
#[catch(413)]
|
||||
async fn payload_too_large() -> JsonValue {
|
||||
json!({"error": "Data too large.", "errors": ["Data too large."]})
|
||||
}
|
||||
|
||||
#[catch(422)]
|
||||
async fn unprocessable_entity() -> JsonValue {
|
||||
json!({"error": "Invalid request.", "errors": ["Invalid request."]})
|
||||
}
|
||||
|
||||
#[catch(500)]
|
||||
async fn internal_server_error() -> Template {
|
||||
let map: HashMap<String, String> = HashMap::new();
|
||||
Template::render("errors/500", &map)
|
||||
}
|
||||
|
||||
pub async fn initialize(
|
||||
kill_channel: Sender<()>,
|
||||
serenity_context: Context,
|
||||
@ -100,12 +69,12 @@ pub async fn initialize(
|
||||
.register(
|
||||
"/",
|
||||
catchers![
|
||||
not_authorized,
|
||||
forbidden,
|
||||
not_found,
|
||||
internal_server_error,
|
||||
unprocessable_entity,
|
||||
payload_too_large,
|
||||
catchers::not_authorized,
|
||||
catchers::forbidden,
|
||||
catchers::not_found,
|
||||
catchers::internal_server_error,
|
||||
catchers::unprocessable_entity,
|
||||
catchers::payload_too_large,
|
||||
],
|
||||
)
|
||||
.manage(oauth2_client)
|
||||
@ -150,22 +119,21 @@ pub async fn initialize(
|
||||
.mount(
|
||||
"/dashboard",
|
||||
routes![
|
||||
routes::dashboard::dashboard_1,
|
||||
routes::dashboard::dashboard_2,
|
||||
routes::dashboard::dashboard,
|
||||
routes::dashboard::dashboard_home,
|
||||
routes::dashboard::user::get_user_info,
|
||||
routes::dashboard::user::update_user_info,
|
||||
routes::dashboard::user::get_user_guilds,
|
||||
routes::dashboard::guild::get_guild_patreon,
|
||||
routes::dashboard::guild::get_guild_channels,
|
||||
routes::dashboard::guild::get_guild_roles,
|
||||
routes::dashboard::guild::get_reminder_templates,
|
||||
routes::dashboard::guild::create_reminder_template,
|
||||
routes::dashboard::guild::delete_reminder_template,
|
||||
routes::dashboard::guild::create_guild_reminder,
|
||||
routes::dashboard::guild::get_reminders,
|
||||
routes::dashboard::guild::edit_reminder,
|
||||
routes::dashboard::guild::delete_reminder,
|
||||
routes::dashboard::api::user::get_user_info,
|
||||
routes::dashboard::api::user::update_user_info,
|
||||
routes::dashboard::api::user::get_user_guilds,
|
||||
routes::dashboard::api::guild::get_guild_info,
|
||||
routes::dashboard::api::guild::get_guild_channels,
|
||||
routes::dashboard::api::guild::get_guild_roles,
|
||||
routes::dashboard::api::guild::get_reminder_templates,
|
||||
routes::dashboard::api::guild::create_reminder_template,
|
||||
routes::dashboard::api::guild::delete_reminder_template,
|
||||
routes::dashboard::api::guild::create_guild_reminder,
|
||||
routes::dashboard::api::guild::get_reminders,
|
||||
routes::dashboard::api::guild::edit_reminder,
|
||||
routes::dashboard::api::guild::delete_reminder,
|
||||
routes::dashboard::export::export_reminders,
|
||||
routes::dashboard::export::export_reminder_templates,
|
||||
routes::dashboard::export::export_todos,
|
||||
@ -223,3 +191,65 @@ pub async fn check_guild_subscription(
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_authorization(
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &Context,
|
||||
guild: u64,
|
||||
) -> Result<(), JsonValue> {
|
||||
let user_id = cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten();
|
||||
|
||||
if std::env::var("OFFLINE").map_or(true, |v| v != "1") {
|
||||
match user_id {
|
||||
Some(user_id) => {
|
||||
let admin_id = std::env::var("ADMIN_ID")
|
||||
.map_or(false, |u| u.parse::<u64>().map_or(false, |u| u == user_id));
|
||||
|
||||
if admin_id {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match GuildId(guild).to_guild_cached(ctx) {
|
||||
Some(guild) => {
|
||||
let member_res = guild.member(ctx, UserId(user_id)).await;
|
||||
|
||||
match member_res {
|
||||
Err(_) => {
|
||||
return Err(json!({"error": "User not in guild"}));
|
||||
}
|
||||
|
||||
Ok(member) => {
|
||||
let permissions_res = member.permissions(ctx);
|
||||
|
||||
match permissions_res {
|
||||
Err(_) => {
|
||||
return Err(json!({"error": "Couldn't fetch permissions"}));
|
||||
}
|
||||
|
||||
Ok(permissions) => {
|
||||
if !(permissions.manage_messages()
|
||||
|| permissions.manage_guild()
|
||||
|| permissions.administrator())
|
||||
{
|
||||
return Err(json!({"error": "Incorrect permissions"}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
return Err(json!({"error": "Bot not in guild"}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
return Err(json!({"error": "User not authorized"}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -54,56 +54,6 @@ macro_rules! check_url_opt {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! check_authorization {
|
||||
($cookies:expr, $ctx:expr, $guild:expr) => {
|
||||
use serenity::model::id::UserId;
|
||||
|
||||
let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten();
|
||||
|
||||
if std::env::var("OFFLINE").map_or(true, |v| v != "1") {
|
||||
match user_id {
|
||||
Some(user_id) => {
|
||||
match GuildId($guild).to_guild_cached($ctx) {
|
||||
Some(guild) => {
|
||||
let member_res = guild.member($ctx, UserId(user_id)).await;
|
||||
|
||||
match member_res {
|
||||
Err(_) => {
|
||||
return Err(json!({"error": "User not in guild"}));
|
||||
}
|
||||
|
||||
Ok(member) => {
|
||||
let permissions_res = member.permissions($ctx);
|
||||
|
||||
match permissions_res {
|
||||
Err(_) => {
|
||||
return Err(json!({"error": "Couldn't fetch permissions"}));
|
||||
}
|
||||
|
||||
Ok(permissions) => {
|
||||
if !(permissions.manage_messages() || permissions.manage_guild() || permissions.administrator()) {
|
||||
return Err(json!({"error": "Incorrect permissions"}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
return Err(json!({"error": "Bot not in guild"}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
return Err(json!({"error": "User not authorized"}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! update_field {
|
||||
($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => {
|
||||
if let Some(value) = &$reminder.$field {
|
||||
|
61
web/src/routes/dashboard/api/guild/channels.rs
Normal file
61
web/src/routes/dashboard/api/guild/channels.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use rocket::{http::CookieJar, serde::json::json, State};
|
||||
use serde::Serialize;
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::{
|
||||
channel::GuildChannel,
|
||||
id::{ChannelId, GuildId},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{check_authorization, routes::JsonResult};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ChannelInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
webhook_avatar: Option<String>,
|
||||
webhook_name: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/channels")]
|
||||
pub async fn get_guild_channels(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
) -> JsonResult {
|
||||
offline!(Ok(json!(vec![ChannelInfo {
|
||||
name: "general".to_string(),
|
||||
id: "1".to_string(),
|
||||
webhook_avatar: None,
|
||||
webhook_name: None,
|
||||
}])));
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
match GuildId(id).to_guild_cached(ctx.inner()) {
|
||||
Some(guild) => {
|
||||
let mut channels = guild
|
||||
.channels
|
||||
.iter()
|
||||
.filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c)))
|
||||
.filter(|(_, channel)| channel.is_text_based())
|
||||
.collect::<Vec<(ChannelId, GuildChannel)>>();
|
||||
|
||||
channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position));
|
||||
|
||||
let channel_info = channels
|
||||
.iter()
|
||||
.map(|(channel_id, channel)| ChannelInfo {
|
||||
name: channel.name.to_string(),
|
||||
id: channel_id.to_string(),
|
||||
webhook_avatar: None,
|
||||
webhook_name: None,
|
||||
})
|
||||
.collect::<Vec<ChannelInfo>>();
|
||||
|
||||
Ok(json!(channel_info))
|
||||
}
|
||||
|
||||
None => json_err!("Bot not in guild"),
|
||||
}
|
||||
}
|
42
web/src/routes/dashboard/api/guild/mod.rs
Normal file
42
web/src/routes/dashboard/api/guild/mod.rs
Normal file
@ -0,0 +1,42 @@
|
||||
mod channels;
|
||||
mod reminders;
|
||||
mod roles;
|
||||
mod templates;
|
||||
|
||||
use std::env;
|
||||
|
||||
pub use channels::*;
|
||||
pub use reminders::*;
|
||||
use rocket::{http::CookieJar, serde::json::json, State};
|
||||
pub use roles::*;
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::id::{GuildId, RoleId},
|
||||
};
|
||||
pub use templates::*;
|
||||
|
||||
use crate::{check_authorization, routes::JsonResult};
|
||||
|
||||
#[get("/api/guild/<id>")]
|
||||
pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
|
||||
offline!(Ok(json!({ "patreon": true, "name": "Guild" })));
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
match GuildId(id).to_guild_cached(ctx.inner()) {
|
||||
Some(guild) => {
|
||||
let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
|
||||
.member(&ctx.inner(), guild.owner_id)
|
||||
.await;
|
||||
|
||||
let patreon = member_res.map_or(false, |member| {
|
||||
member
|
||||
.roles
|
||||
.contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
|
||||
});
|
||||
|
||||
Ok(json!({ "patreon": patreon, "name": guild.name }))
|
||||
}
|
||||
|
||||
None => json_err!("Bot not in guild"),
|
||||
}
|
||||
}
|
373
web/src/routes/dashboard/api/guild/reminders.rs
Normal file
373
web/src/routes/dashboard/api/guild/reminders.rs
Normal file
@ -0,0 +1,373 @@
|
||||
use rocket::{
|
||||
http::CookieJar,
|
||||
serde::json::{json, Json},
|
||||
State,
|
||||
};
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::id::{ChannelId, GuildId, UserId},
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::{
|
||||
check_authorization, check_guild_subscription, check_subscription,
|
||||
consts::MIN_INTERVAL,
|
||||
guards::transaction::Transaction,
|
||||
routes::{
|
||||
dashboard::{
|
||||
create_database_channel, create_reminder, DeleteReminder, PatchReminder, Reminder,
|
||||
},
|
||||
JsonResult,
|
||||
},
|
||||
Database,
|
||||
};
|
||||
|
||||
#[post("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||
pub async fn create_guild_reminder(
|
||||
id: u64,
|
||||
reminder: Json<Reminder>,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
mut transaction: Transaction<'_>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let user_id =
|
||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||
|
||||
match create_reminder(
|
||||
ctx.inner(),
|
||||
&mut transaction,
|
||||
GuildId(id),
|
||||
UserId(user_id),
|
||||
reminder.into_inner(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => match transaction.commit().await {
|
||||
Ok(_) => Ok(r),
|
||||
Err(e) => {
|
||||
warn!("Couldn't commit transaction: {:?}", e);
|
||||
json_err!("Couldn't commit transaction.")
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/reminders")]
|
||||
pub async fn get_reminders(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let channels_res = GuildId(id).channels(&ctx.inner()).await;
|
||||
|
||||
match channels_res {
|
||||
Ok(channels) => {
|
||||
let channels = channels
|
||||
.keys()
|
||||
.into_iter()
|
||||
.map(|k| k.as_u64().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
|
||||
sqlx::query_as_unchecked!(
|
||||
Reminder,
|
||||
"SELECT
|
||||
reminders.attachment,
|
||||
reminders.attachment_name,
|
||||
reminders.avatar,
|
||||
channels.channel,
|
||||
reminders.content,
|
||||
reminders.embed_author,
|
||||
reminders.embed_author_url,
|
||||
reminders.embed_color,
|
||||
reminders.embed_description,
|
||||
reminders.embed_footer,
|
||||
reminders.embed_footer_url,
|
||||
reminders.embed_image_url,
|
||||
reminders.embed_thumbnail_url,
|
||||
reminders.embed_title,
|
||||
IFNULL(reminders.embed_fields, '[]') AS embed_fields,
|
||||
reminders.enabled,
|
||||
reminders.expires,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.restartable,
|
||||
reminders.tts,
|
||||
reminders.uid,
|
||||
reminders.username,
|
||||
reminders.utc_time
|
||||
FROM reminders
|
||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
||||
WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)",
|
||||
channels
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
.map(|r| Ok(json!(r)))
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("Failed to complete SQL query: {:?}", e);
|
||||
|
||||
json_err!("Could not load reminders")
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not fetch channels from {}: {:?}", id, e);
|
||||
|
||||
Ok(json!([]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[patch("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||
pub async fn edit_reminder(
|
||||
id: u64,
|
||||
reminder: Json<PatchReminder>,
|
||||
ctx: &State<Context>,
|
||||
mut transaction: Transaction<'_>,
|
||||
pool: &State<Pool<Database>>,
|
||||
cookies: &CookieJar<'_>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
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!(transaction.executor(), error, reminder.[
|
||||
content,
|
||||
embed_author,
|
||||
embed_description,
|
||||
embed_footer,
|
||||
embed_title,
|
||||
embed_fields,
|
||||
username
|
||||
]);
|
||||
} else {
|
||||
error.push("Message exceeds limits.".to_string());
|
||||
}
|
||||
|
||||
update_field!(transaction.executor(), error, reminder.[
|
||||
attachment,
|
||||
attachment_name,
|
||||
avatar,
|
||||
embed_author_url,
|
||||
embed_color,
|
||||
embed_footer_url,
|
||||
embed_image_url,
|
||||
embed_thumbnail_url,
|
||||
enabled,
|
||||
expires,
|
||||
name,
|
||||
restartable,
|
||||
tts,
|
||||
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(&ctx.inner(), id).await
|
||||
|| check_subscription(&ctx.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(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.days
|
||||
.unwrap_or(0),
|
||||
} * 86400 + 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(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.months
|
||||
.unwrap_or(0),
|
||||
} * 2592000 + 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(transaction.executor())
|
||||
.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!(transaction.executor(), error, reminder.[
|
||||
interval_days,
|
||||
interval_months,
|
||||
interval_seconds
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if reminder.channel > 0 {
|
||||
let channel = ChannelId(reminder.channel).to_channel_cached(&ctx.inner());
|
||||
match channel {
|
||||
Some(channel) => {
|
||||
let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id);
|
||||
|
||||
if !channel_matches_guild {
|
||||
warn!(
|
||||
"Error in `edit_reminder`: channel {:?} not found for guild {}",
|
||||
reminder.channel, id
|
||||
);
|
||||
|
||||
return Err(json!({"error": "Channel not found"}));
|
||||
}
|
||||
|
||||
let channel = create_database_channel(
|
||||
ctx.inner(),
|
||||
ChannelId(reminder.channel),
|
||||
&mut transaction,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = channel {
|
||||
warn!("`create_database_channel` returned an error code: {:?}", e);
|
||||
|
||||
return Err(
|
||||
json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}),
|
||||
);
|
||||
}
|
||||
|
||||
let channel = channel.unwrap();
|
||||
|
||||
match sqlx::query!(
|
||||
"UPDATE reminders SET channel_id = ? WHERE uid = ?",
|
||||
channel,
|
||||
reminder.uid
|
||||
)
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
warn!("Error setting channel: {:?}", e);
|
||||
|
||||
error.push("Couldn't set channel".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
warn!(
|
||||
"Error in `edit_reminder`: channel {:?} not found for guild {}",
|
||||
reminder.channel, id
|
||||
);
|
||||
|
||||
return Err(json!({"error": "Channel not found"}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = transaction.commit().await {
|
||||
warn!("Couldn't commit transaction: {:?}", e);
|
||||
return json_err!("Couldn't commit transaction");
|
||||
}
|
||||
|
||||
match sqlx::query_as_unchecked!(
|
||||
Reminder,
|
||||
"SELECT reminders.attachment,
|
||||
reminders.attachment_name,
|
||||
reminders.avatar,
|
||||
channels.channel,
|
||||
reminders.content,
|
||||
reminders.embed_author,
|
||||
reminders.embed_author_url,
|
||||
reminders.embed_color,
|
||||
reminders.embed_description,
|
||||
reminders.embed_footer,
|
||||
reminders.embed_footer_url,
|
||||
reminders.embed_image_url,
|
||||
reminders.embed_thumbnail_url,
|
||||
reminders.embed_title,
|
||||
reminders.embed_fields,
|
||||
reminders.enabled,
|
||||
reminders.expires,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.restartable,
|
||||
reminders.tts,
|
||||
reminders.uid,
|
||||
reminders.username,
|
||||
reminders.utc_time
|
||||
FROM reminders
|
||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
||||
WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error exiting `edit_reminder': {:?}", e);
|
||||
|
||||
Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||
pub async fn delete_reminder(
|
||||
cookies: &CookieJar<'_>,
|
||||
id: u64,
|
||||
reminder: Json<DeleteReminder>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid)
|
||||
.execute(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(json!({})),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error in `delete_reminder`: {:?}", e);
|
||||
|
||||
Err(json!({"error": "Could not delete reminder"}))
|
||||
}
|
||||
}
|
||||
}
|
35
web/src/routes/dashboard/api/guild/roles.rs
Normal file
35
web/src/routes/dashboard/api/guild/roles.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use rocket::{http::CookieJar, serde::json::json, State};
|
||||
use serde::Serialize;
|
||||
use serenity::client::Context;
|
||||
|
||||
use crate::{check_authorization, routes::JsonResult};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RoleInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/roles")]
|
||||
pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
|
||||
offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }])));
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let roles_res = ctx.cache.guild_roles(id);
|
||||
|
||||
match roles_res {
|
||||
Some(roles) => {
|
||||
let roles = roles
|
||||
.iter()
|
||||
.map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() })
|
||||
.collect::<Vec<RoleInfo>>();
|
||||
|
||||
Ok(json!(roles))
|
||||
}
|
||||
None => {
|
||||
warn!("Could not fetch roles from {}", id);
|
||||
|
||||
json_err!("Could not get roles")
|
||||
}
|
||||
}
|
||||
}
|
181
web/src/routes/dashboard/api/guild/templates.rs
Normal file
181
web/src/routes/dashboard/api/guild/templates.rs
Normal file
@ -0,0 +1,181 @@
|
||||
use rocket::{
|
||||
http::CookieJar,
|
||||
serde::json::{json, Json},
|
||||
State,
|
||||
};
|
||||
use serenity::client::Context;
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::{
|
||||
check_authorization,
|
||||
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,
|
||||
},
|
||||
routes::{
|
||||
dashboard::{template_name_default, DeleteReminderTemplate, ReminderTemplate},
|
||||
JsonResult,
|
||||
},
|
||||
};
|
||||
|
||||
#[get("/api/guild/<id>/templates")]
|
||||
pub async fn get_reminder_templates(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
match sqlx::query_as_unchecked!(
|
||||
ReminderTemplate,
|
||||
"SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||
id
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(templates) => Ok(json!(templates)),
|
||||
Err(e) => {
|
||||
warn!("Could not fetch templates from {}: {:?}", id, e);
|
||||
|
||||
json_err!("Could not get templates")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/guild/<id>/templates", data = "<reminder_template>")]
|
||||
pub async fn create_reminder_template(
|
||||
id: u64,
|
||||
reminder_template: Json<ReminderTemplate>,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
// validate lengths
|
||||
check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
|
||||
check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description);
|
||||
check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title);
|
||||
check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author);
|
||||
check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer);
|
||||
check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields);
|
||||
if let Some(fields) = &reminder_template.embed_fields {
|
||||
for field in &fields.0 {
|
||||
check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
|
||||
check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
|
||||
}
|
||||
}
|
||||
check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username);
|
||||
check_length_opt!(
|
||||
MAX_URL_LENGTH,
|
||||
reminder_template.embed_footer_url,
|
||||
reminder_template.embed_thumbnail_url,
|
||||
reminder_template.embed_author_url,
|
||||
reminder_template.embed_image_url,
|
||||
reminder_template.avatar
|
||||
);
|
||||
|
||||
// validate urls
|
||||
check_url_opt!(
|
||||
reminder_template.embed_footer_url,
|
||||
reminder_template.embed_thumbnail_url,
|
||||
reminder_template.embed_author_url,
|
||||
reminder_template.embed_image_url,
|
||||
reminder_template.avatar
|
||||
);
|
||||
|
||||
let name = if reminder_template.name.is_empty() {
|
||||
template_name_default()
|
||||
} else {
|
||||
reminder_template.name.clone()
|
||||
};
|
||||
|
||||
match sqlx::query!(
|
||||
"INSERT INTO reminder_template
|
||||
(guild_id,
|
||||
name,
|
||||
attachment,
|
||||
attachment_name,
|
||||
avatar,
|
||||
content,
|
||||
embed_author,
|
||||
embed_author_url,
|
||||
embed_color,
|
||||
embed_description,
|
||||
embed_footer,
|
||||
embed_footer_url,
|
||||
embed_image_url,
|
||||
embed_thumbnail_url,
|
||||
embed_title,
|
||||
embed_fields,
|
||||
interval_seconds,
|
||||
interval_days,
|
||||
interval_months,
|
||||
tts,
|
||||
username
|
||||
) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?)",
|
||||
id,
|
||||
name,
|
||||
reminder_template.attachment,
|
||||
reminder_template.attachment_name,
|
||||
reminder_template.avatar,
|
||||
reminder_template.content,
|
||||
reminder_template.embed_author,
|
||||
reminder_template.embed_author_url,
|
||||
reminder_template.embed_color,
|
||||
reminder_template.embed_description,
|
||||
reminder_template.embed_footer,
|
||||
reminder_template.embed_footer_url,
|
||||
reminder_template.embed_image_url,
|
||||
reminder_template.embed_thumbnail_url,
|
||||
reminder_template.embed_title,
|
||||
reminder_template.embed_fields,
|
||||
reminder_template.interval_seconds,
|
||||
reminder_template.interval_days,
|
||||
reminder_template.interval_months,
|
||||
reminder_template.tts,
|
||||
reminder_template.username,
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(json!({})),
|
||||
Err(e) => {
|
||||
warn!("Could not create template for {}: {:?}", id, e);
|
||||
|
||||
json_err!("Could not create template")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")]
|
||||
pub async fn delete_reminder_template(
|
||||
id: u64,
|
||||
delete_reminder_template: Json<DeleteReminderTemplate>,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
match sqlx::query!(
|
||||
"DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?",
|
||||
id, delete_reminder_template.id
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
Ok(json!({}))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not delete template from {}: {:?}", id, e);
|
||||
|
||||
json_err!("Could not delete template")
|
||||
}
|
||||
}
|
||||
}
|
2
web/src/routes/dashboard/api/mod.rs
Normal file
2
web/src/routes/dashboard/api/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod guild;
|
||||
pub mod user;
|
81
web/src/routes/dashboard/api/user/guilds.rs
Normal file
81
web/src/routes/dashboard/api/user/guilds.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use reqwest::Client;
|
||||
use rocket::{
|
||||
http::CookieJar,
|
||||
serde::json::{json, Value as JsonValue},
|
||||
State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::model::{id::GuildId, permissions::Permissions};
|
||||
|
||||
use crate::consts::DISCORD_API;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct GuildInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PartialGuild {
|
||||
pub id: GuildId,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub owner: bool,
|
||||
#[serde(rename = "permissions_new")]
|
||||
pub permissions: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/api/user/guilds")]
|
||||
pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue {
|
||||
offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }]));
|
||||
|
||||
if let Some(access_token) = cookies.get_private("access_token") {
|
||||
let request_res = reqwest_client
|
||||
.get(format!("{}/users/@me/guilds", DISCORD_API))
|
||||
.bearer_auth(access_token.value())
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match request_res {
|
||||
Ok(response) => {
|
||||
let guilds_res = response.json::<Vec<PartialGuild>>().await;
|
||||
|
||||
match guilds_res {
|
||||
Ok(guilds) => {
|
||||
let reduced_guilds = guilds
|
||||
.iter()
|
||||
.filter(|g| {
|
||||
g.owner
|
||||
|| g.permissions.as_ref().map_or(false, |p| {
|
||||
let permissions =
|
||||
Permissions::from_bits_truncate(p.parse().unwrap());
|
||||
|
||||
permissions.manage_messages()
|
||||
|| permissions.manage_guild()
|
||||
|| permissions.administrator()
|
||||
})
|
||||
})
|
||||
.map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() })
|
||||
.collect::<Vec<GuildInfo>>();
|
||||
|
||||
json!(reduced_guilds)
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error constructing user from request: {:?}", e);
|
||||
|
||||
json!({"error": "Could not get user details"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error getting user guilds: {:?}", e);
|
||||
|
||||
json!({"error": "Could not reach Discord"})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
json!({"error": "Not authorized"})
|
||||
}
|
||||
}
|
97
web/src/routes/dashboard/api/user/mod.rs
Normal file
97
web/src/routes/dashboard/api/user/mod.rs
Normal file
@ -0,0 +1,97 @@
|
||||
mod guilds;
|
||||
|
||||
use std::env;
|
||||
|
||||
use chrono_tz::Tz;
|
||||
pub use guilds::*;
|
||||
use rocket::{
|
||||
http::CookieJar,
|
||||
serde::json::{json, Json, Value as JsonValue},
|
||||
State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::id::{GuildId, RoleId},
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UserInfo {
|
||||
name: String,
|
||||
patreon: bool,
|
||||
timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateUser {
|
||||
timezone: String,
|
||||
}
|
||||
|
||||
#[get("/api/user")]
|
||||
pub async fn get_user_info(
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonValue {
|
||||
offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None }));
|
||||
|
||||
if let Some(user_id) =
|
||||
cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
|
||||
{
|
||||
let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
|
||||
.member(&ctx.inner(), user_id)
|
||||
.await;
|
||||
|
||||
let timezone = sqlx::query!(
|
||||
"SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?",
|
||||
user_id
|
||||
)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
.map_or(None, |q| Some(q.timezone));
|
||||
|
||||
let user_info = UserInfo {
|
||||
name: cookies
|
||||
.get_private("username")
|
||||
.map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()),
|
||||
patreon: member_res.map_or(false, |member| {
|
||||
member
|
||||
.roles
|
||||
.contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
|
||||
}),
|
||||
timezone,
|
||||
};
|
||||
|
||||
json!(user_info)
|
||||
} else {
|
||||
json!({"error": "Not authorized"})
|
||||
}
|
||||
}
|
||||
|
||||
#[patch("/api/user", data = "<user>")]
|
||||
pub async fn update_user_info(
|
||||
cookies: &CookieJar<'_>,
|
||||
user: Json<UpdateUser>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonValue {
|
||||
if let Some(user_id) =
|
||||
cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
|
||||
{
|
||||
if user.timezone.parse::<Tz>().is_ok() {
|
||||
let _ = sqlx::query!(
|
||||
"UPDATE users SET timezone = ? WHERE user = ?",
|
||||
user.timezone,
|
||||
user_id,
|
||||
)
|
||||
.execute(pool.inner())
|
||||
.await;
|
||||
|
||||
json!({})
|
||||
} else {
|
||||
json!({"error": "Timezone not recognized"})
|
||||
}
|
||||
} else {
|
||||
json!({"error": "Not authorized"})
|
||||
}
|
||||
}
|
20
web/src/routes/dashboard/api/user/models.rs
Normal file
20
web/src/routes/dashboard/api/user/models.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use std::env;
|
||||
|
||||
use chrono_tz::Tz;
|
||||
use reqwest::Client;
|
||||
use rocket::{
|
||||
http::CookieJar,
|
||||
serde::json::{json, Json, Value as JsonValue},
|
||||
State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::{
|
||||
id::{GuildId, RoleId},
|
||||
permissions::Permissions,
|
||||
},
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::{consts::DISCORD_API, routes::JsonResult};
|
29
web/src/routes/dashboard/api/user/reminders.rs
Normal file
29
web/src/routes/dashboard/api/user/reminders.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use std::env;
|
||||
|
||||
use chrono_tz::Tz;
|
||||
use reqwest::Client;
|
||||
use rocket::{
|
||||
http::CookieJar,
|
||||
serde::json::{json, Json, Value as JsonValue},
|
||||
State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::{
|
||||
id::{GuildId, RoleId},
|
||||
permissions::Permissions,
|
||||
},
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::{consts::DISCORD_API, routes::JsonResult};
|
||||
|
||||
#[get("/api/user/reminders")]
|
||||
pub async fn get_reminders(
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
Ok(json! {})
|
||||
}
|
@ -6,15 +6,20 @@ use rocket::{
|
||||
};
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::id::{ChannelId, GuildId},
|
||||
model::id::{ChannelId, GuildId, UserId},
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::routes::{
|
||||
use crate::{
|
||||
check_authorization,
|
||||
guards::transaction::Transaction,
|
||||
routes::{
|
||||
dashboard::{
|
||||
create_reminder, ImportBody, ReminderCreate, ReminderCsv, ReminderTemplateCsv, TodoCsv,
|
||||
create_reminder, generate_uid, ImportBody, Reminder, ReminderCsv, ReminderTemplateCsv,
|
||||
TodoCsv,
|
||||
},
|
||||
JsonResult,
|
||||
},
|
||||
};
|
||||
|
||||
#[get("/api/guild/<id>/export/reminders")]
|
||||
@ -24,7 +29,7 @@ pub async fn export_reminders(
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
|
||||
|
||||
@ -69,7 +74,7 @@ pub async fn export_reminders(
|
||||
reminders.utc_time
|
||||
FROM reminders
|
||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
||||
WHERE FIND_IN_SET(channels.channel, ?)",
|
||||
WHERE FIND_IN_SET(channels.channel, ?) AND status = 'pending'",
|
||||
channels
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
@ -117,14 +122,14 @@ pub async fn export_reminders(
|
||||
}
|
||||
|
||||
#[put("/api/guild/<id>/export/reminders", data = "<body>")]
|
||||
pub async fn import_reminders(
|
||||
pub(crate) async fn import_reminders(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
body: Json<ImportBody>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
mut transaction: Transaction<'_>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let user_id =
|
||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||
@ -132,6 +137,7 @@ pub async fn import_reminders(
|
||||
match base64::decode(&body.body) {
|
||||
Ok(body) => {
|
||||
let mut reader = csv::Reader::from_reader(body.as_slice());
|
||||
let mut count = 0;
|
||||
|
||||
for result in reader.deserialize::<ReminderCsv>() {
|
||||
match result {
|
||||
@ -140,7 +146,7 @@ pub async fn import_reminders(
|
||||
|
||||
match channel_id.parse::<u64>() {
|
||||
Ok(channel_id) => {
|
||||
let reminder = ReminderCreate {
|
||||
let reminder = Reminder {
|
||||
attachment: record.attachment,
|
||||
attachment_name: record.attachment_name,
|
||||
avatar: record.avatar,
|
||||
@ -167,18 +173,21 @@ pub async fn import_reminders(
|
||||
name: record.name,
|
||||
restartable: record.restartable,
|
||||
tts: record.tts,
|
||||
uid: generate_uid(),
|
||||
username: record.username,
|
||||
utc_time: record.utc_time,
|
||||
};
|
||||
|
||||
create_reminder(
|
||||
ctx.inner(),
|
||||
pool.inner(),
|
||||
&mut transaction,
|
||||
GuildId(id),
|
||||
UserId(user_id),
|
||||
reminder,
|
||||
)
|
||||
.await?;
|
||||
|
||||
count += 1;
|
||||
}
|
||||
|
||||
Err(_) => {
|
||||
@ -198,7 +207,16 @@ pub async fn import_reminders(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(json!({}))
|
||||
match transaction.commit().await {
|
||||
Ok(_) => Ok(json!({
|
||||
"message": format!("Imported {} reminders", count)
|
||||
})),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Failed to commit transaction: {:?}", e);
|
||||
json_err!("Couldn't commit transaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(_) => {
|
||||
@ -214,7 +232,7 @@ pub async fn export_todos(
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
|
||||
|
||||
@ -269,7 +287,7 @@ pub async fn import_todos(
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let channels_res = GuildId(id).channels(&ctx.inner()).await;
|
||||
|
||||
@ -364,7 +382,7 @@ pub async fn export_reminder_templates(
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
|
||||
|
||||
@ -386,6 +404,9 @@ pub async fn export_reminder_templates(
|
||||
embed_thumbnail_url,
|
||||
embed_title,
|
||||
embed_fields,
|
||||
interval_seconds,
|
||||
interval_days,
|
||||
interval_months,
|
||||
tts,
|
||||
username
|
||||
FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||
|
@ -1,621 +1 @@
|
||||
use std::env;
|
||||
|
||||
use rocket::{
|
||||
http::CookieJar,
|
||||
serde::json::{json, Json},
|
||||
State,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::{
|
||||
channel::GuildChannel,
|
||||
id::{ChannelId, GuildId, RoleId},
|
||||
},
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::{
|
||||
check_guild_subscription, check_subscription,
|
||||
consts::{
|
||||
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,
|
||||
DeleteReminderTemplate, PatchReminder, Reminder, ReminderCreate, ReminderTemplate,
|
||||
},
|
||||
JsonResult,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ChannelInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
webhook_avatar: Option<String>,
|
||||
webhook_name: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/patreon")]
|
||||
pub async fn get_guild_patreon(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
) -> JsonResult {
|
||||
offline!(Ok(json!({ "patreon": true })));
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
|
||||
match GuildId(id).to_guild_cached(ctx.inner()) {
|
||||
Some(guild) => {
|
||||
let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
|
||||
.member(&ctx.inner(), guild.owner_id)
|
||||
.await;
|
||||
|
||||
let patreon = member_res.map_or(false, |member| {
|
||||
member
|
||||
.roles
|
||||
.contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
|
||||
});
|
||||
|
||||
Ok(json!({ "patreon": patreon }))
|
||||
}
|
||||
|
||||
None => json_err!("Bot not in guild"),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/channels")]
|
||||
pub async fn get_guild_channels(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
) -> JsonResult {
|
||||
offline!(Ok(json!(vec![ChannelInfo {
|
||||
name: "general".to_string(),
|
||||
id: "1".to_string(),
|
||||
webhook_avatar: None,
|
||||
webhook_name: None,
|
||||
}])));
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
|
||||
match GuildId(id).to_guild_cached(ctx.inner()) {
|
||||
Some(guild) => {
|
||||
let mut channels = guild
|
||||
.channels
|
||||
.iter()
|
||||
.filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c)))
|
||||
.filter(|(_, channel)| channel.is_text_based())
|
||||
.collect::<Vec<(ChannelId, GuildChannel)>>();
|
||||
|
||||
channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position));
|
||||
|
||||
let channel_info = channels
|
||||
.iter()
|
||||
.map(|(channel_id, channel)| ChannelInfo {
|
||||
name: channel.name.to_string(),
|
||||
id: channel_id.to_string(),
|
||||
webhook_avatar: None,
|
||||
webhook_name: None,
|
||||
})
|
||||
.collect::<Vec<ChannelInfo>>();
|
||||
|
||||
Ok(json!(channel_info))
|
||||
}
|
||||
|
||||
None => json_err!("Bot not in guild"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RoleInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/roles")]
|
||||
pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
|
||||
offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }])));
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
|
||||
let roles_res = ctx.cache.guild_roles(id);
|
||||
|
||||
match roles_res {
|
||||
Some(roles) => {
|
||||
let roles = roles
|
||||
.iter()
|
||||
.map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() })
|
||||
.collect::<Vec<RoleInfo>>();
|
||||
|
||||
Ok(json!(roles))
|
||||
}
|
||||
None => {
|
||||
warn!("Could not fetch roles from {}", id);
|
||||
|
||||
json_err!("Could not get roles")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/templates")]
|
||||
pub async fn get_reminder_templates(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
|
||||
match sqlx::query_as_unchecked!(
|
||||
ReminderTemplate,
|
||||
"SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||
id
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(templates) => Ok(json!(templates)),
|
||||
Err(e) => {
|
||||
warn!("Could not fetch templates from {}: {:?}", id, e);
|
||||
|
||||
json_err!("Could not get templates")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/guild/<id>/templates", data = "<reminder_template>")]
|
||||
pub async fn create_reminder_template(
|
||||
id: u64,
|
||||
reminder_template: Json<ReminderTemplate>,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
|
||||
// validate lengths
|
||||
check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
|
||||
check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description);
|
||||
check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title);
|
||||
check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author);
|
||||
check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer);
|
||||
check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields);
|
||||
if let Some(fields) = &reminder_template.embed_fields {
|
||||
for field in &fields.0 {
|
||||
check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
|
||||
check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
|
||||
}
|
||||
}
|
||||
check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username);
|
||||
check_length_opt!(
|
||||
MAX_URL_LENGTH,
|
||||
reminder_template.embed_footer_url,
|
||||
reminder_template.embed_thumbnail_url,
|
||||
reminder_template.embed_author_url,
|
||||
reminder_template.embed_image_url,
|
||||
reminder_template.avatar
|
||||
);
|
||||
|
||||
// validate urls
|
||||
check_url_opt!(
|
||||
reminder_template.embed_footer_url,
|
||||
reminder_template.embed_thumbnail_url,
|
||||
reminder_template.embed_author_url,
|
||||
reminder_template.embed_image_url,
|
||||
reminder_template.avatar
|
||||
);
|
||||
|
||||
let name = if reminder_template.name.is_empty() {
|
||||
template_name_default()
|
||||
} else {
|
||||
reminder_template.name.clone()
|
||||
};
|
||||
|
||||
match sqlx::query!(
|
||||
"INSERT INTO reminder_template
|
||||
(guild_id,
|
||||
name,
|
||||
attachment,
|
||||
attachment_name,
|
||||
avatar,
|
||||
content,
|
||||
embed_author,
|
||||
embed_author_url,
|
||||
embed_color,
|
||||
embed_description,
|
||||
embed_footer,
|
||||
embed_footer_url,
|
||||
embed_image_url,
|
||||
embed_thumbnail_url,
|
||||
embed_title,
|
||||
embed_fields,
|
||||
tts,
|
||||
username
|
||||
) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
id, name,
|
||||
reminder_template.attachment,
|
||||
reminder_template.attachment_name,
|
||||
reminder_template.avatar,
|
||||
reminder_template.content,
|
||||
reminder_template.embed_author,
|
||||
reminder_template.embed_author_url,
|
||||
reminder_template.embed_color,
|
||||
reminder_template.embed_description,
|
||||
reminder_template.embed_footer,
|
||||
reminder_template.embed_footer_url,
|
||||
reminder_template.embed_image_url,
|
||||
reminder_template.embed_thumbnail_url,
|
||||
reminder_template.embed_title,
|
||||
reminder_template.embed_fields,
|
||||
reminder_template.tts,
|
||||
reminder_template.username,
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
Ok(json!({}))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not create template for {}: {:?}", id, e);
|
||||
|
||||
json_err!("Could not create template")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")]
|
||||
pub async fn delete_reminder_template(
|
||||
id: u64,
|
||||
delete_reminder_template: Json<DeleteReminderTemplate>,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, ctx.inner(), id);
|
||||
|
||||
match sqlx::query!(
|
||||
"DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?",
|
||||
id, delete_reminder_template.id
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
Ok(json!({}))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not delete template from {}: {:?}", id, e);
|
||||
|
||||
json_err!("Could not delete template")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||
pub async fn create_guild_reminder(
|
||||
id: u64,
|
||||
reminder: Json<ReminderCreate>,
|
||||
cookies: &CookieJar<'_>,
|
||||
serenity_context: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, serenity_context.inner(), id);
|
||||
|
||||
let user_id =
|
||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||
|
||||
create_reminder(
|
||||
serenity_context.inner(),
|
||||
pool.inner(),
|
||||
GuildId(id),
|
||||
UserId(user_id),
|
||||
reminder.into_inner(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/reminders?<status>")]
|
||||
pub async fn get_reminders(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
serenity_context: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
status: Option<String>,
|
||||
) -> JsonResult {
|
||||
check_authorization!(cookies, serenity_context.inner(), id);
|
||||
|
||||
let status = status.unwrap_or("pending".to_string());
|
||||
|
||||
sqlx::query_as_unchecked!(
|
||||
Reminder,
|
||||
"
|
||||
SELECT
|
||||
reminders.attachment,
|
||||
reminders.attachment_name,
|
||||
reminders.avatar,
|
||||
channels.channel,
|
||||
reminders.content,
|
||||
reminders.embed_author,
|
||||
reminders.embed_author_url,
|
||||
reminders.embed_color,
|
||||
reminders.embed_description,
|
||||
reminders.embed_footer,
|
||||
reminders.embed_footer_url,
|
||||
reminders.embed_image_url,
|
||||
reminders.embed_thumbnail_url,
|
||||
reminders.embed_title,
|
||||
IFNULL(reminders.embed_fields, '[]') AS embed_fields,
|
||||
reminders.enabled,
|
||||
reminders.expires,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.restartable,
|
||||
reminders.tts,
|
||||
reminders.uid,
|
||||
reminders.username,
|
||||
reminders.utc_time,
|
||||
reminders.status,
|
||||
reminders.status_change_time,
|
||||
reminders.status_message
|
||||
FROM reminders
|
||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
||||
WHERE FIND_IN_SET(`status`, ?) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||
status,
|
||||
id
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
.map(|r| Ok(json!(r)))
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("Failed to complete SQL query: {:?}", e);
|
||||
|
||||
json_err!("Could not load reminders")
|
||||
})
|
||||
}
|
||||
|
||||
#[patch("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||
pub async fn edit_reminder(
|
||||
id: u64,
|
||||
reminder: Json<PatchReminder>,
|
||||
serenity_context: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
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,
|
||||
embed_author_url,
|
||||
embed_color,
|
||||
embed_footer_url,
|
||||
embed_image_url,
|
||||
embed_thumbnail_url,
|
||||
enabled,
|
||||
expires,
|
||||
name,
|
||||
restartable,
|
||||
tts,
|
||||
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),
|
||||
} * 86400 + 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),
|
||||
} * 2592000 + 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 {
|
||||
Some(channel) => {
|
||||
let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id);
|
||||
|
||||
if !channel_matches_guild {
|
||||
warn!(
|
||||
"Error in `edit_reminder`: channel {:?} not found for guild {}",
|
||||
reminder.channel, id
|
||||
);
|
||||
|
||||
return Err(json!({"error": "Channel not found"}));
|
||||
}
|
||||
|
||||
let channel = create_database_channel(
|
||||
serenity_context.inner(),
|
||||
ChannelId(reminder.channel),
|
||||
pool.inner(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = channel {
|
||||
warn!("`create_database_channel` returned an error code: {:?}", e);
|
||||
|
||||
return Err(
|
||||
json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}),
|
||||
);
|
||||
}
|
||||
|
||||
let channel = channel.unwrap();
|
||||
|
||||
match sqlx::query!(
|
||||
"UPDATE reminders SET channel_id = ? WHERE uid = ?",
|
||||
channel,
|
||||
reminder.uid
|
||||
)
|
||||
.execute(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
warn!("Error setting channel: {:?}", e);
|
||||
|
||||
error.push("Couldn't set channel".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
warn!(
|
||||
"Error in `edit_reminder`: channel {:?} not found for guild {}",
|
||||
reminder.channel, id
|
||||
);
|
||||
|
||||
return Err(json!({"error": "Channel not found"}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match sqlx::query_as_unchecked!(
|
||||
Reminder,
|
||||
"
|
||||
SELECT reminders.attachment,
|
||||
reminders.attachment_name,
|
||||
reminders.avatar,
|
||||
channels.channel,
|
||||
reminders.content,
|
||||
reminders.embed_author,
|
||||
reminders.embed_author_url,
|
||||
reminders.embed_color,
|
||||
reminders.embed_description,
|
||||
reminders.embed_footer,
|
||||
reminders.embed_footer_url,
|
||||
reminders.embed_image_url,
|
||||
reminders.embed_thumbnail_url,
|
||||
reminders.embed_title,
|
||||
reminders.embed_fields,
|
||||
reminders.enabled,
|
||||
reminders.expires,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.restartable,
|
||||
reminders.tts,
|
||||
reminders.uid,
|
||||
reminders.username,
|
||||
reminders.utc_time,
|
||||
reminders.status,
|
||||
reminders.status_change_time,
|
||||
reminders.status_message
|
||||
FROM reminders
|
||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
||||
WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error exiting `edit_reminder': {:?}", e);
|
||||
|
||||
Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/api/guild/<_>/reminders", data = "<reminder>")]
|
||||
pub async fn delete_reminder(
|
||||
reminder: Json<DeleteReminder>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
match sqlx::query!(
|
||||
"UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.execute(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(json!({})),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error in `delete_reminder`: {:?}", e);
|
||||
|
||||
Err(json!({"error": "Could not delete reminder"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use serenity::{
|
||||
http::Http,
|
||||
model::id::{ChannelId, GuildId, UserId},
|
||||
};
|
||||
use sqlx::{types::Json, Executor};
|
||||
use sqlx::types::Json;
|
||||
|
||||
use crate::{
|
||||
check_guild_subscription, check_subscription,
|
||||
@ -20,13 +20,14 @@ use crate::{
|
||||
MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH,
|
||||
MAX_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL,
|
||||
},
|
||||
guards::transaction::Transaction,
|
||||
routes::JsonResult,
|
||||
Database, Error,
|
||||
Error,
|
||||
};
|
||||
|
||||
pub mod api;
|
||||
pub mod export;
|
||||
pub mod guild;
|
||||
pub mod user;
|
||||
|
||||
type Unset<T> = Option<T>;
|
||||
|
||||
@ -80,6 +81,9 @@ pub struct ReminderTemplate {
|
||||
embed_thumbnail_url: Option<String>,
|
||||
embed_title: String,
|
||||
embed_fields: Option<Json<Vec<EmbedField>>>,
|
||||
interval_seconds: Option<u32>,
|
||||
interval_days: Option<u32>,
|
||||
interval_months: Option<u32>,
|
||||
tts: bool,
|
||||
username: Option<String>,
|
||||
}
|
||||
@ -102,6 +106,9 @@ pub struct ReminderTemplateCsv {
|
||||
embed_thumbnail_url: Option<String>,
|
||||
embed_title: String,
|
||||
embed_fields: Option<String>,
|
||||
interval_seconds: Option<u32>,
|
||||
interval_days: Option<u32>,
|
||||
interval_months: Option<u32>,
|
||||
tts: bool,
|
||||
username: Option<String>,
|
||||
}
|
||||
@ -118,8 +125,8 @@ pub struct EmbedField {
|
||||
inline: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ReminderCreate {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Reminder {
|
||||
#[serde(with = "base64s")]
|
||||
attachment: Option<Vec<u8>>,
|
||||
attachment_name: Option<String>,
|
||||
@ -146,45 +153,10 @@ pub struct ReminderCreate {
|
||||
name: String,
|
||||
restartable: bool,
|
||||
tts: bool,
|
||||
username: Option<String>,
|
||||
utc_time: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Reminder {
|
||||
#[serde(with = "base64s")]
|
||||
attachment: Option<Vec<u8>>,
|
||||
attachment_name: Option<String>,
|
||||
avatar: Option<String>,
|
||||
#[serde(with = "string_opt")]
|
||||
channel: Option<u64>,
|
||||
content: String,
|
||||
embed_author: String,
|
||||
embed_author_url: Option<String>,
|
||||
embed_color: u32,
|
||||
embed_description: String,
|
||||
embed_footer: String,
|
||||
embed_footer_url: Option<String>,
|
||||
embed_image_url: Option<String>,
|
||||
embed_thumbnail_url: Option<String>,
|
||||
embed_title: String,
|
||||
embed_fields: Option<Json<Vec<EmbedField>>>,
|
||||
enabled: bool,
|
||||
expires: Option<NaiveDateTime>,
|
||||
interval_seconds: Option<u32>,
|
||||
interval_days: Option<u32>,
|
||||
interval_months: Option<u32>,
|
||||
#[serde(default = "name_default")]
|
||||
name: String,
|
||||
restartable: bool,
|
||||
tts: bool,
|
||||
#[serde(default)]
|
||||
uid: String,
|
||||
username: Option<String>,
|
||||
utc_time: NaiveDateTime,
|
||||
status: String,
|
||||
status_message: Option<String>,
|
||||
status_change_time: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -323,7 +295,15 @@ pub fn generate_uid() -> String {
|
||||
mod string {
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
T: Display,
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.collect_str(value)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
@ -335,34 +315,6 @@ mod string {
|
||||
}
|
||||
}
|
||||
|
||||
mod string_opt {
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
T: Display,
|
||||
S: Serializer,
|
||||
{
|
||||
match value {
|
||||
Some(value) => serializer.collect_str(value),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
T: FromStr,
|
||||
T::Err: Display,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Option::deserialize(deserializer)?
|
||||
.map(|d: String| d.parse().map_err(de::Error::custom))
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
mod base64s {
|
||||
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||
|
||||
@ -402,21 +354,21 @@ pub struct TodoCsv {
|
||||
channel_id: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create_reminder(
|
||||
pub(crate) async fn create_reminder(
|
||||
ctx: &Context,
|
||||
pool: impl sqlx::Executor<'_, Database = Database> + Copy,
|
||||
transaction: &mut Transaction<'_>,
|
||||
guild_id: GuildId,
|
||||
user_id: UserId,
|
||||
reminder: ReminderCreate,
|
||||
reminder: Reminder,
|
||||
) -> JsonResult {
|
||||
// check guild in db
|
||||
match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0)
|
||||
.fetch_one(pool)
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
{
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.0)
|
||||
.execute(pool)
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
@ -435,14 +387,14 @@ pub async fn create_reminder(
|
||||
|
||||
if !channel_matches_guild || !channel_exists {
|
||||
warn!(
|
||||
"Error in `create_reminder`: channel {:?} not found for guild {} (channel exists: {})",
|
||||
"Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})",
|
||||
reminder.channel, guild_id, channel_exists
|
||||
);
|
||||
|
||||
return Err(json!({"error": "Channel not found"}));
|
||||
}
|
||||
|
||||
let channel = create_database_channel(&ctx, ChannelId(reminder.channel), pool).await;
|
||||
let channel = create_database_channel(&ctx, ChannelId(reminder.channel), transaction).await;
|
||||
|
||||
if let Err(e) = channel {
|
||||
warn!("`create_database_channel` returned an error code: {:?}", e);
|
||||
@ -516,8 +468,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
|
||||
@ -529,13 +479,11 @@ pub async fn create_reminder(
|
||||
|
||||
// write to db
|
||||
match sqlx::query!(
|
||||
"
|
||||
INSERT INTO reminders (
|
||||
"INSERT INTO reminders (
|
||||
uid,
|
||||
attachment,
|
||||
attachment_name,
|
||||
channel_id,
|
||||
guild_id,
|
||||
avatar,
|
||||
content,
|
||||
embed_author,
|
||||
@ -558,14 +506,11 @@ pub async fn create_reminder(
|
||||
tts,
|
||||
username,
|
||||
`utc_time`
|
||||
) VALUES (?, ?, ?, ?,
|
||||
(SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?)",
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
new_uid,
|
||||
attachment_data,
|
||||
reminder.attachment,
|
||||
reminder.attachment_name,
|
||||
channel,
|
||||
guild_id.0,
|
||||
reminder.avatar,
|
||||
reminder.content,
|
||||
reminder.embed_author,
|
||||
@ -589,7 +534,7 @@ pub async fn create_reminder(
|
||||
username,
|
||||
reminder.utc_time,
|
||||
)
|
||||
.execute(pool)
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
{
|
||||
Ok(_) => sqlx::query_as_unchecked!(
|
||||
@ -620,16 +565,13 @@ pub async fn create_reminder(
|
||||
reminders.tts,
|
||||
reminders.uid,
|
||||
reminders.username,
|
||||
reminders.utc_time,
|
||||
reminders.status,
|
||||
reminders.status_change_time,
|
||||
reminders.status_message
|
||||
reminders.utc_time
|
||||
FROM reminders
|
||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
||||
WHERE uid = ?",
|
||||
new_uid
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
.map(|r| Ok(json!(r)))
|
||||
.unwrap_or_else(|e| {
|
||||
@ -649,11 +591,11 @@ pub async fn create_reminder(
|
||||
async fn create_database_channel(
|
||||
ctx: impl AsRef<Http>,
|
||||
channel: ChannelId,
|
||||
pool: impl Executor<'_, Database = Database> + Copy,
|
||||
transaction: &mut Transaction<'_>,
|
||||
) -> Result<u32, crate::Error> {
|
||||
let row =
|
||||
sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0)
|
||||
.fetch_one(pool)
|
||||
.fetch_one(transaction.executor())
|
||||
.await;
|
||||
|
||||
match row {
|
||||
@ -670,7 +612,7 @@ async fn create_database_channel(
|
||||
webhook.token,
|
||||
channel.0
|
||||
)
|
||||
.execute(pool)
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| Error::SQLx(e))?;
|
||||
}
|
||||
@ -696,7 +638,7 @@ async fn create_database_channel(
|
||||
webhook.token,
|
||||
channel.0
|
||||
)
|
||||
.execute(pool)
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| Error::SQLx(e))?;
|
||||
|
||||
@ -707,7 +649,7 @@ async fn create_database_channel(
|
||||
}?;
|
||||
|
||||
let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.0)
|
||||
.fetch_one(pool)
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| Error::SQLx(e))?;
|
||||
|
||||
@ -717,27 +659,19 @@ async fn create_database_channel(
|
||||
#[get("/")]
|
||||
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
|
||||
if cookies.get_private("userid").is_some() {
|
||||
let map: HashMap<&str, String> = HashMap::new();
|
||||
let mut map = HashMap::new();
|
||||
map.insert("version", env!("CARGO_PKG_VERSION"));
|
||||
Ok(Template::render("dashboard", &map))
|
||||
} else {
|
||||
Err(Redirect::to("/login/discord"))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/<_>")]
|
||||
pub async fn dashboard_1(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
|
||||
#[get("/<_..>")]
|
||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
|
||||
if cookies.get_private("userid").is_some() {
|
||||
let map: HashMap<&str, String> = HashMap::new();
|
||||
Ok(Template::render("dashboard", &map))
|
||||
} else {
|
||||
Err(Redirect::to("/login/discord"))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/<_>/<_>")]
|
||||
pub async fn dashboard_2(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
|
||||
if cookies.get_private("userid").is_some() {
|
||||
let map: HashMap<&str, String> = HashMap::new();
|
||||
let mut map = HashMap::new();
|
||||
map.insert("version", env!("CARGO_PKG_VERSION"));
|
||||
Ok(Template::render("dashboard", &map))
|
||||
} else {
|
||||
Err(Redirect::to("/login/discord"))
|
||||
|
@ -1,172 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
use chrono_tz::Tz;
|
||||
use reqwest::Client;
|
||||
use rocket::{
|
||||
http::CookieJar,
|
||||
serde::json::{json, Json, Value as JsonValue},
|
||||
State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::{
|
||||
id::{GuildId, RoleId},
|
||||
permissions::Permissions,
|
||||
},
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::consts::DISCORD_API;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UserInfo {
|
||||
name: String,
|
||||
patreon: bool,
|
||||
timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateUser {
|
||||
timezone: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct GuildInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PartialGuild {
|
||||
pub id: GuildId,
|
||||
pub icon: Option<String>,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub owner: bool,
|
||||
#[serde(rename = "permissions_new")]
|
||||
pub permissions: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/api/user")]
|
||||
pub async fn get_user_info(
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonValue {
|
||||
offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None }));
|
||||
|
||||
if let Some(user_id) =
|
||||
cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
|
||||
{
|
||||
let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
|
||||
.member(&ctx.inner(), user_id)
|
||||
.await;
|
||||
|
||||
let timezone = sqlx::query!(
|
||||
"SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?",
|
||||
user_id
|
||||
)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
.map_or(None, |q| Some(q.timezone));
|
||||
|
||||
let user_info = UserInfo {
|
||||
name: cookies
|
||||
.get_private("username")
|
||||
.map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()),
|
||||
patreon: member_res.map_or(false, |member| {
|
||||
member
|
||||
.roles
|
||||
.contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
|
||||
}),
|
||||
timezone,
|
||||
};
|
||||
|
||||
json!(user_info)
|
||||
} else {
|
||||
json!({"error": "Not authorized"})
|
||||
}
|
||||
}
|
||||
|
||||
#[patch("/api/user", data = "<user>")]
|
||||
pub async fn update_user_info(
|
||||
cookies: &CookieJar<'_>,
|
||||
user: Json<UpdateUser>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonValue {
|
||||
if let Some(user_id) =
|
||||
cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
|
||||
{
|
||||
if user.timezone.parse::<Tz>().is_ok() {
|
||||
let _ = sqlx::query!(
|
||||
"UPDATE users SET timezone = ? WHERE user = ?",
|
||||
user.timezone,
|
||||
user_id,
|
||||
)
|
||||
.execute(pool.inner())
|
||||
.await;
|
||||
|
||||
json!({})
|
||||
} else {
|
||||
json!({"error": "Timezone not recognized"})
|
||||
}
|
||||
} else {
|
||||
json!({"error": "Not authorized"})
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/user/guilds")]
|
||||
pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue {
|
||||
offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }]));
|
||||
|
||||
if let Some(access_token) = cookies.get_private("access_token") {
|
||||
let request_res = reqwest_client
|
||||
.get(format!("{}/users/@me/guilds", DISCORD_API))
|
||||
.bearer_auth(access_token.value())
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match request_res {
|
||||
Ok(response) => {
|
||||
let guilds_res = response.json::<Vec<PartialGuild>>().await;
|
||||
|
||||
match guilds_res {
|
||||
Ok(guilds) => {
|
||||
let reduced_guilds = guilds
|
||||
.iter()
|
||||
.filter(|g| {
|
||||
g.owner
|
||||
|| g.permissions.as_ref().map_or(false, |p| {
|
||||
let permissions =
|
||||
Permissions::from_bits_truncate(p.parse().unwrap());
|
||||
|
||||
permissions.manage_messages()
|
||||
|| permissions.manage_guild()
|
||||
|| permissions.administrator()
|
||||
})
|
||||
})
|
||||
.map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() })
|
||||
.collect::<Vec<GuildInfo>>();
|
||||
|
||||
json!(reduced_guilds)
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error constructing user from request: {:?}", e);
|
||||
|
||||
json!({"error": "Could not get user details"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error getting user guilds: {:?}", e);
|
||||
|
||||
json!({"error": "Could not reach Discord"})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
json!({"error": "Not authorized"})
|
||||
}
|
||||
}
|
@ -15,6 +15,10 @@ div.reminderContent.is-collapsed .column.settings {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed .reminder-settings {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.reminderContent.is-collapsed .button-row {
|
||||
display: none;
|
||||
}
|
||||
@ -304,11 +308,6 @@ div.dashboard-sidebar {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
div.dashboard-sidebar:not(.mobile-sidebar) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ul.guildList {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
@ -318,6 +317,9 @@ ul.guildList {
|
||||
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 226px;
|
||||
}
|
||||
|
||||
div.dashboard-sidebar svg {
|
||||
|
@ -18,7 +18,6 @@ const $downloader = document.querySelector("a#downloader");
|
||||
const $uploader = document.querySelector("input#uploader");
|
||||
|
||||
let channels = [];
|
||||
let reminderErrors = [];
|
||||
let guildNames = {};
|
||||
let roles = [];
|
||||
let templates = {};
|
||||
@ -34,11 +33,16 @@ let globalPatreon = false;
|
||||
let guildPatreon = false;
|
||||
|
||||
function guildId() {
|
||||
return document.querySelector("li > a.is-active").parentElement.dataset["guild"];
|
||||
return window.location.pathname.match(/dashboard\/(\d+)/)[1];
|
||||
}
|
||||
|
||||
function guildName() {
|
||||
return guildNames[guildId()];
|
||||
function pane() {
|
||||
const match = window.location.pathname.match(/dashboard\/\d+\/(.+)/);
|
||||
if (match === null) {
|
||||
return null;
|
||||
} else {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
function colorToInt(r, g, b) {
|
||||
@ -57,7 +61,7 @@ function switch_pane(selector) {
|
||||
el.classList.add("is-hidden");
|
||||
});
|
||||
|
||||
document.querySelector(`*[data-name=${selector}]`).classList.remove("is-hidden");
|
||||
document.getElementById(selector).classList.remove("is-hidden");
|
||||
}
|
||||
|
||||
function update_select(sel) {
|
||||
@ -101,7 +105,7 @@ function reset_guild_pane() {
|
||||
}
|
||||
|
||||
async function fetch_patreon(guild_id) {
|
||||
fetch(`/dashboard/api/guild/${guild_id}/patreon`)
|
||||
fetch(`/dashboard/api/guild/${guild_id}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
@ -228,11 +232,10 @@ async function fetch_reminders(guild_id) {
|
||||
}
|
||||
|
||||
async function serialize_reminder(node, mode) {
|
||||
let interval, utc_time, expiration_time;
|
||||
let utc_time, expiration_time;
|
||||
let interval = get_interval(node);
|
||||
|
||||
if (mode !== "template") {
|
||||
interval = get_interval(node);
|
||||
|
||||
utc_time = luxon.DateTime.fromISO(
|
||||
node.querySelector('input[name="time"]').value
|
||||
).setZone("UTC");
|
||||
@ -361,9 +364,9 @@ async function serialize_reminder(node, mode) {
|
||||
embed_title: embed_title,
|
||||
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,
|
||||
interval_seconds: interval.seconds,
|
||||
interval_days: interval.days,
|
||||
interval_months: interval.months,
|
||||
name: node.querySelector('input[name="name"]').value,
|
||||
tts: node.querySelector('input[name="tts"]').checked,
|
||||
username: node.querySelector('input[name="username"]').value,
|
||||
@ -425,9 +428,9 @@ function deserialize_reminder(reminder, frame, mode) {
|
||||
.insertBefore(embed_field, lastChild);
|
||||
}
|
||||
|
||||
if (mode !== "template") {
|
||||
if (reminder["interval_seconds"]) update_interval(frame);
|
||||
|
||||
if (mode !== "template") {
|
||||
let $enableBtn = frame.querySelector(".disable-enable");
|
||||
$enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable";
|
||||
|
||||
@ -454,27 +457,23 @@ document.addEventListener("guildSwitched", async (e) => {
|
||||
.querySelectorAll(".patreon-only")
|
||||
.forEach((el) => el.classList.add("is-locked"));
|
||||
|
||||
let $li = document.querySelectorAll(`li[data-guild="${e.detail.guild_id}"]`);
|
||||
let $anchor = document.querySelector(
|
||||
`.switch-pane[data-guild="${e.detail.guild_id}"]`
|
||||
);
|
||||
|
||||
if ($li.length === 0) {
|
||||
switch_pane("user-error");
|
||||
return;
|
||||
let hasError = false;
|
||||
|
||||
if (pane() === null) {
|
||||
window.history.replaceState({}, "", `/dashboard/${guildId()}/reminders`);
|
||||
}
|
||||
|
||||
switch_pane(pane());
|
||||
|
||||
if ($anchor !== null) {
|
||||
$anchor.classList.add("is-active");
|
||||
}
|
||||
|
||||
switch_pane(e.detail.pane);
|
||||
reset_guild_pane();
|
||||
document
|
||||
.querySelectorAll(`li[data-guild="${e.detail.guild_id}"] > a`)
|
||||
.forEach((el) => {
|
||||
el.classList.add("is-active");
|
||||
});
|
||||
document
|
||||
.querySelectorAll(
|
||||
`li[data-guild="${e.detail.guild_id}"] *[data-pane="${e.detail.pane}"]`
|
||||
)
|
||||
.forEach((el) => {
|
||||
el.classList.add("is-active");
|
||||
});
|
||||
|
||||
if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) {
|
||||
document
|
||||
@ -482,26 +481,15 @@ document.addEventListener("guildSwitched", async (e) => {
|
||||
.forEach((el) => el.classList.remove("is-locked"));
|
||||
}
|
||||
|
||||
const event = new CustomEvent("paneLoad", {
|
||||
detail: {
|
||||
guild_id: e.detail.guild_id,
|
||||
pane: e.detail.pane,
|
||||
},
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
|
||||
document.addEventListener("paneLoad", async (ev) => {
|
||||
const hasError = await fetch_channels(ev.detail.guild_id);
|
||||
hasError = await fetch_channels(e.detail.guild_id);
|
||||
if (!hasError) {
|
||||
fetch_roles(ev.detail.guild_id);
|
||||
fetch_templates(ev.detail.guild_id);
|
||||
fetch_reminders(ev.detail.guild_id);
|
||||
fetch_roles(e.detail.guild_id);
|
||||
fetch_templates(e.detail.guild_id);
|
||||
fetch_reminders(e.detail.guild_id);
|
||||
|
||||
document.querySelectorAll("p.pageTitle").forEach((el) => {
|
||||
el.textContent = `${guildName()} Reminders`;
|
||||
el.textContent = `${e.detail.guild_name} Reminders`;
|
||||
});
|
||||
|
||||
document.querySelectorAll("select.channel-selector").forEach((el) => {
|
||||
el.addEventListener("change", (e) => {
|
||||
update_select(e.target);
|
||||
@ -626,6 +614,16 @@ function show_error(error) {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function show_success(error) {
|
||||
document.getElementById("success").querySelector("span.success-message").textContent =
|
||||
error;
|
||||
document.getElementById("success").classList.add("is-active");
|
||||
|
||||
window.setTimeout(() => {
|
||||
document.getElementById("success").classList.remove("is-active");
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
$colorPickerInput.value = colorPicker.color.hexString;
|
||||
|
||||
$colorPickerInput.addEventListener("input", () => {
|
||||
@ -706,56 +704,40 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
"%guildname%",
|
||||
guild.name
|
||||
);
|
||||
$anchor.dataset["guild"] = guild.id;
|
||||
$anchor.dataset["name"] = guild.name;
|
||||
$anchor.href = `/dashboard/${guild.id}/reminders`;
|
||||
|
||||
const $li = $anchor.parentElement;
|
||||
$li.dataset["guild"] = guild.id;
|
||||
|
||||
$li.querySelectorAll("a").forEach((el) => {
|
||||
el.addEventListener("click", (e) => {
|
||||
const pane = el.dataset["pane"];
|
||||
const slug = el.dataset["slug"];
|
||||
|
||||
if (pane !== undefined && slug !== undefined) {
|
||||
$anchor.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
switch_pane(pane);
|
||||
|
||||
window.history.pushState(
|
||||
{},
|
||||
"",
|
||||
`/dashboard/${guild.id}/${slug}`
|
||||
`/dashboard/${guild.id}/reminders`
|
||||
);
|
||||
const event = new CustomEvent("guildSwitched", {
|
||||
detail: {
|
||||
guild_name: guild.name,
|
||||
guild_id: guild.id,
|
||||
pane,
|
||||
},
|
||||
});
|
||||
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
element.append($clone);
|
||||
});
|
||||
}
|
||||
|
||||
const matches = window.location.href.match(
|
||||
/dashboard\/(\d+)(\/)?([a-zA-Z\-]+)?/
|
||||
);
|
||||
const matches = window.location.href.match(/dashboard\/(\d+)/);
|
||||
if (matches) {
|
||||
let id = matches[1];
|
||||
let kind = matches[3];
|
||||
let name = guildNames[id];
|
||||
|
||||
const event = new CustomEvent("guildSwitched", {
|
||||
detail: {
|
||||
guild_name: name,
|
||||
guild_id: id,
|
||||
pane: kind,
|
||||
},
|
||||
});
|
||||
|
||||
@ -796,11 +778,25 @@ $uploader.addEventListener("change", (ev) => {
|
||||
fileReader.onload = (e) => resolve(fileReader.result);
|
||||
fileReader.readAsDataURL($uploader.files[0]);
|
||||
}).then((dataUrl) => {
|
||||
$importBtn.setAttribute("disabled", true);
|
||||
|
||||
fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ body: dataUrl.split(",")[1] }),
|
||||
}).then(() => {
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
$importBtn.removeAttribute("disabled");
|
||||
|
||||
if (data.error) {
|
||||
show_error(data.error);
|
||||
} else {
|
||||
show_success(data.message);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
delete $uploader.files[0];
|
||||
fetch_reminders(guild);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,45 +0,0 @@
|
||||
function loadErrors() {
|
||||
return fetch(
|
||||
`/dashboard/api/guild/${guildId()}/reminders?status=deleted,sent,failed`
|
||||
).then((response) => response.json());
|
||||
}
|
||||
|
||||
document.addEventListener("paneLoad", (ev) => {
|
||||
if (ev.detail.pane !== "errors") {
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelectorAll(".reminderError").forEach((el) => el.remove());
|
||||
|
||||
const template = document.getElementById("reminderError");
|
||||
const container = document.getElementById("reminderLog");
|
||||
|
||||
loadErrors()
|
||||
.then((res) => {
|
||||
res = res
|
||||
.filter((r) => r.status_change_time !== null)
|
||||
.sort((a, b) => a.status_change_time < b.status_change_time);
|
||||
|
||||
for (const reminder of res) {
|
||||
const newRow = template.content.cloneNode(true);
|
||||
|
||||
newRow.querySelector(".reminderError").dataset["case"] = reminder.status;
|
||||
|
||||
const statusTime = new luxon.DateTime.fromISO(
|
||||
reminder.status_change_time,
|
||||
{ zone: "UTC" }
|
||||
);
|
||||
newRow.querySelector(".reminderName").textContent = reminder.name;
|
||||
newRow.querySelector(".reminderMessage").textContent =
|
||||
reminder.status_message;
|
||||
newRow.querySelector(".reminderTime").textContent = statusTime
|
||||
.toLocal()
|
||||
.toLocaleString(luxon.DateTime.DATETIME_MED);
|
||||
|
||||
container.appendChild(newRow);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
$loader.classList.add("is-hidden");
|
||||
});
|
||||
});
|
@ -27,7 +27,7 @@
|
||||
<link rel="stylesheet" href="/static/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="/static/css/fa.css">
|
||||
<link rel="stylesheet" href="/static/css/font.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v{{ version }}">
|
||||
<link rel="stylesheet" href="/static/css/dtsel.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tributejs/5.1.3/tribute.css" integrity="sha512-GnwBnXd+ZGO9CdP343MUr0jCcJXCr++JVtQRnllexRW2IDq4Zvrh/McTQjooAKnSUbXZ7wamp7AQSweTnfMVoA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
@ -76,6 +76,10 @@
|
||||
<span class="icon"><i class="far fa-exclamation-circle"></i></span> <span class="error-message"></span>
|
||||
</div>
|
||||
|
||||
<div class="notification is-success flash-message" id="success">
|
||||
<span class="icon"><i class="far fa-check"></i></span> <span class="success-message"></span>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="addImageModal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
@ -185,14 +189,6 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="field">
|
||||
<label>
|
||||
<input type="radio" class="default-width" name="exportSelect" value="todos">
|
||||
Todo Lists
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="has-text-centered">
|
||||
<div style="color: red">
|
||||
@ -257,11 +253,9 @@
|
||||
</p>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
{#
|
||||
<a class="show-modal" data-modal="dataManagerModal">
|
||||
<span class="icon"><i class="fas fa-exchange"></i></span> Import/Export
|
||||
</a>
|
||||
#}
|
||||
<a class="show-modal" data-modal="chooseTimezoneModal">
|
||||
<span class="icon"><i class="fas fa-map-marked"></i></span> Timezone
|
||||
</a>
|
||||
@ -303,11 +297,9 @@
|
||||
</p>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
{#
|
||||
<a class="show-modal" data-modal="dataManagerModal">
|
||||
<span class="icon"><i class="fas fa-exchange"></i></span> Import/Export
|
||||
</a>
|
||||
#}
|
||||
<a class="show-modal" data-modal="chooseTimezoneModal">
|
||||
<span class="icon"><i class="fas fa-map-marked"></i></span> Timezone
|
||||
</a>
|
||||
@ -333,16 +325,16 @@
|
||||
<p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p>
|
||||
</div>
|
||||
</section>
|
||||
<section data-name="reminders" class="is-hidden">
|
||||
<section id="reminders" class="is-hidden">
|
||||
{% include "reminder_dashboard/reminder_dashboard" %}
|
||||
</section>
|
||||
<section data-name="errors" class="is-hidden">
|
||||
<section id="reminder-errors" class="is-hidden">
|
||||
{% include "reminder_dashboard/reminder_errors" %}
|
||||
</section>
|
||||
<section data-name="guild-error" class="is-hidden">
|
||||
<section id="guild-error" class="is-hidden">
|
||||
{% include "reminder_dashboard/guild_error" %}
|
||||
</section>
|
||||
<section data-name="user-error" class="is-hidden">
|
||||
<section id="user-error" class="is-hidden">
|
||||
{% include "reminder_dashboard/user_error" %}
|
||||
</section>
|
||||
</div>
|
||||
@ -376,36 +368,22 @@
|
||||
|
||||
<template id="guildListEntry">
|
||||
<li>
|
||||
<a class="switch-pane" data-pane="reminders" data-slug="reminders">
|
||||
<a class="switch-pane" data-pane="guild">
|
||||
<span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span>
|
||||
</a>
|
||||
<ul class="guild-submenu">
|
||||
<li>
|
||||
<a class="switch-pane" data-pane="reminders" data-slug="reminders">
|
||||
<span class="icon"><i class="fas fa-calendar-alt"></i></span> Reminders
|
||||
</a>
|
||||
<a class="switch-pane" data-pane="errors" data-slug="errors">
|
||||
<span class="icon"><i class="fas fa-file-alt"></i></span> Logs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<template id="guildReminder">
|
||||
{% include "reminder_dashboard/templates/guild_reminder" %}
|
||||
</template>
|
||||
|
||||
<template id="reminderError">
|
||||
{% include "reminder_dashboard/templates/reminder_error" %}
|
||||
{% include "reminder_dashboard/guild_reminder" %}
|
||||
</template>
|
||||
|
||||
<script src="/static/js/iro.js"></script>
|
||||
<script src="/static/js/dtsel.js"></script>
|
||||
|
||||
<script src="/static/js/interval.js"></script>
|
||||
<script src="/static/js/timezone.js" defer></script>
|
||||
<script src="/static/js/main.js" defer></script>
|
||||
<script src="/static/js/interval.js?v{{ version }}"></script>
|
||||
<script src="/static/js/timezone.js?v{{ version }}" defer></script>
|
||||
<script src="/static/js/main.js?v{{ version }}" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<strong>Create Reminder</strong>
|
||||
<div id="reminderCreator">
|
||||
{% set creating = true %}
|
||||
{% include "reminder_dashboard/templates/guild_reminder" %}
|
||||
{% include "reminder_dashboard/guild_reminder" %}
|
||||
{% set creating = false %}
|
||||
</div>
|
||||
<br>
|
||||
@ -46,10 +46,6 @@
|
||||
<div id="guildReminders">
|
||||
|
||||
</div>
|
||||
|
||||
<div id="guildErrors">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/sort.js"></script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div id="reminderLog">
|
||||
<div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/reminder_errors.js"></script>
|
||||
<!--<script src="/static/js/reminder_errors.js"></script>-->
|
||||
|
@ -1,20 +0,0 @@
|
||||
<div class="reminderError" data-case="success">
|
||||
<div class="errorHead">
|
||||
<div class="errorIcon">
|
||||
<span class="icon">
|
||||
<i class="fas fa-trash"></i>
|
||||
<i class="fas fa-check"></i>
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="reminderName">
|
||||
Reminder
|
||||
</div>
|
||||
<div class="reminderMessage">
|
||||
|
||||
</div>
|
||||
<div class="reminderTime">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Reference in New Issue
Block a user