Compare commits
35 Commits
next
...
jude/react
Author | SHA1 | Date | |
---|---|---|---|
1a03c2471b | |||
a476f43f28 | |||
17192b0f89 | |||
0419863afa | |||
827a982a40 | |||
6e435bfc2e | |||
8ba0f02b98 | |||
d36438c6ce | |||
e0c60e2ce3 | |||
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 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@
|
|||||||
/venv
|
/venv
|
||||||
.cargo
|
.cargo
|
||||||
/.idea
|
/.idea
|
||||||
|
web/static/index.html
|
||||||
|
web/static/assets
|
||||||
|
1910
Cargo.lock
generated
1910
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@ -1,18 +1,18 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder-rs"
|
name = "reminder-rs"
|
||||||
version = "1.6.38"
|
version = "1.6.50"
|
||||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0 only"
|
license = "AGPL-3.0 only"
|
||||||
description = "Reminder Bot for Discord, now in Rust"
|
description = "Reminder Bot for Discord, now in Rust"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
poise = "0.5.5"
|
poise = "0.5"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
tokio = { version = "1", features = ["process", "full"] }
|
tokio = { version = "1", features = ["process", "full"] }
|
||||||
reqwest = "0.11"
|
reqwest = "0.11"
|
||||||
lazy-regex = "2.3.0"
|
lazy-regex = "3.0.2"
|
||||||
regex = "1.6"
|
regex = "1.9"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
@ -25,7 +25,7 @@ serde_repr = "0.1"
|
|||||||
rmp-serde = "1.1"
|
rmp-serde = "1.1"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
levenshtein = "1.0"
|
levenshtein = "1.0"
|
||||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]}
|
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]}
|
||||||
base64 = "0.21.0"
|
base64 = "0.21.0"
|
||||||
|
|
||||||
[dependencies.postman]
|
[dependencies.postman]
|
||||||
@ -43,6 +43,7 @@ assets = [
|
|||||||
["conf/default.env", "etc/reminder-rs/config.env", "600"],
|
["conf/default.env", "etc/reminder-rs/config.env", "600"],
|
||||||
["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
|
["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
|
||||||
["web/static/**/*", "lib/reminder-rs/static", "644"],
|
["web/static/**/*", "lib/reminder-rs/static", "644"],
|
||||||
|
["reminder-dashboard/dist/static/**/*", "lib/reminder-rs/static", "644"],
|
||||||
["web/templates/**/*", "lib/reminder-rs/templates", "644"],
|
["web/templates/**/*", "lib/reminder-rs/templates", "644"],
|
||||||
["healthcheck", "lib/reminder-rs/healthcheck", "755"],
|
["healthcheck", "lib/reminder-rs/healthcheck", "755"],
|
||||||
["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"],
|
["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"],
|
||||||
|
@ -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;
|
@ -5,12 +5,12 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1", features = ["process", "full"] }
|
tokio = { version = "1", features = ["process", "full"] }
|
||||||
regex = "1.4"
|
regex = "1.9"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
chrono-tz = { version = "0.5", features = ["serde"] }
|
chrono-tz = { version = "0.8", features = ["serde"] }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
num-integer = "0.1"
|
num-integer = "0.1"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
|
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
|
||||||
serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
|
serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
|
||||||
|
@ -27,7 +27,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
"SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
"SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||||
guild_id.0
|
guild_id.0
|
||||||
)
|
)
|
||||||
.fetch_all(&mut transaction)
|
.fetch_all(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut added_aliases = 0;
|
let mut added_aliases = 0;
|
||||||
@ -42,7 +42,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
cmd_macro.description,
|
cmd_macro.description,
|
||||||
cmd_macro.commands
|
cmd_macro.commands
|
||||||
)
|
)
|
||||||
.execute(&mut transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
added_aliases += 1;
|
added_aliases += 1;
|
||||||
|
@ -114,6 +114,8 @@ pub async fn offset(
|
|||||||
#[description = "Number of minutes to offset by"] minutes: Option<isize>,
|
#[description = "Number of minutes to offset by"] minutes: Option<isize>,
|
||||||
#[description = "Number of seconds to offset by"] seconds: Option<isize>,
|
#[description = "Number of seconds to offset by"] seconds: Option<isize>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer().await?;
|
||||||
|
|
||||||
let combined_time = hours.map_or(0, |h| h * HOUR as isize)
|
let combined_time = hours.map_or(0, |h| h * HOUR as isize)
|
||||||
+ minutes.map_or(0, |m| m * MINUTE as isize)
|
+ minutes.map_or(0, |m| m * MINUTE as isize)
|
||||||
+ seconds.map_or(0, |s| s);
|
+ seconds.map_or(0, |s| s);
|
||||||
@ -619,7 +621,7 @@ pub async fn multiline(
|
|||||||
)]
|
)]
|
||||||
pub async fn remind(
|
pub async fn remind(
|
||||||
ctx: ApplicationContext<'_>,
|
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"]
|
#[autocomplete = "time_hint_autocomplete"]
|
||||||
time: String,
|
time: String,
|
||||||
#[description = "The message content to send"] content: String,
|
#[description = "The message content to send"] content: String,
|
||||||
@ -773,7 +775,7 @@ async fn create_reminder(
|
|||||||
b.emoji(ReactionType::Unicode("📝".to_string()))
|
b.emoji(ReactionType::Unicode("📝".to_string()))
|
||||||
.label("Edit")
|
.label("Edit")
|
||||||
.style(ButtonStyle::Link)
|
.style(ButtonStyle::Link)
|
||||||
.url("https://reminder-bot.com/dashboard")
|
.url("https://beta.reminder-bot.com/dashboard")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -150,7 +150,7 @@ impl<'a> Parser<'a> {
|
|||||||
"hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0),
|
"hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0),
|
||||||
"days" | "day" | "d" => (0, n, 0, 0),
|
"days" | "day" | "d" => (0, n, 0, 0),
|
||||||
"weeks" | "week" | "w" => (0, n.mul(7)?, 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),
|
"years" | "year" | "y" => (n.mul(12)?, 0, 0, 0),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Error::UnknownUnit {
|
return Err(Error::UnknownUnit {
|
||||||
@ -255,7 +255,7 @@ impl<'a> Parser<'a> {
|
|||||||
/// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000)));
|
/// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000)));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn parse_duration(s: &str) -> Result<Interval, Error> {
|
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)]
|
#[cfg(test)]
|
||||||
@ -324,4 +324,13 @@ mod tests {
|
|||||||
assert_eq!(interval.day, 0);
|
assert_eq!(interval.day, 0);
|
||||||
assert_eq!(interval.month, 120);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder_web"
|
name = "reminder_web"
|
||||||
version = "0.1.0"
|
version = "0.1.4"
|
||||||
authors = ["jellywx <judesouthworth@pm.me>"]
|
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] }
|
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] }
|
||||||
rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] }
|
rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] }
|
||||||
serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
|
serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
|
||||||
oauth2 = "4"
|
oauth2 = "4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
reqwest = "0.11"
|
reqwest = "0.11"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] }
|
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
chrono-tz = "0.5"
|
chrono-tz = "0.8"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
rand = "0.7"
|
rand = "0.8"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
csv = "1.1"
|
csv = "1.2"
|
||||||
|
prometheus = "0.13.3"
|
||||||
|
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;
|
42
web/src/guards/transaction.rs
Normal file
42
web/src/guards/transaction.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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::Error((Status::InternalServerError, TransactionError::Error(e))),
|
||||||
|
},
|
||||||
|
Outcome::Error(e) => Outcome::Error((e.0, TransactionError::Missing)),
|
||||||
|
Outcome::Forward(f) => Outcome::Forward(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
155
web/src/lib.rs
155
web/src/lib.rs
@ -4,13 +4,17 @@ extern crate rocket;
|
|||||||
mod consts;
|
mod consts;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
mod macros;
|
||||||
|
mod catchers;
|
||||||
|
mod guards;
|
||||||
|
mod metrics;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
use std::{collections::HashMap, env, path::Path};
|
use std::{env, path::Path};
|
||||||
|
|
||||||
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
|
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
fs::FileServer,
|
fs::FileServer,
|
||||||
|
http::CookieJar,
|
||||||
serde::json::{json, Value as JsonValue},
|
serde::json::{json, Value as JsonValue},
|
||||||
tokio::sync::broadcast::Sender,
|
tokio::sync::broadcast::Sender,
|
||||||
};
|
};
|
||||||
@ -22,7 +26,10 @@ use serenity::{
|
|||||||
};
|
};
|
||||||
use sqlx::{MySql, Pool};
|
use sqlx::{MySql, Pool};
|
||||||
|
|
||||||
use crate::consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES};
|
use crate::{
|
||||||
|
consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES},
|
||||||
|
metrics::{init_metrics, MetricProducer},
|
||||||
|
};
|
||||||
|
|
||||||
type Database = MySql;
|
type Database = MySql;
|
||||||
|
|
||||||
@ -32,40 +39,6 @@ enum Error {
|
|||||||
Serenity(serenity::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(
|
pub async fn initialize(
|
||||||
kill_channel: Sender<()>,
|
kill_channel: Sender<()>,
|
||||||
serenity_context: Context,
|
serenity_context: Context,
|
||||||
@ -95,17 +68,20 @@ pub async fn initialize(
|
|||||||
let static_path =
|
let static_path =
|
||||||
if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" };
|
if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" };
|
||||||
|
|
||||||
|
init_metrics();
|
||||||
|
|
||||||
rocket::build()
|
rocket::build()
|
||||||
|
.attach(MetricProducer)
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
.register(
|
.register(
|
||||||
"/",
|
"/",
|
||||||
catchers![
|
catchers![
|
||||||
not_authorized,
|
catchers::not_authorized,
|
||||||
forbidden,
|
catchers::forbidden,
|
||||||
not_found,
|
catchers::not_found,
|
||||||
internal_server_error,
|
catchers::internal_server_error,
|
||||||
unprocessable_entity,
|
catchers::unprocessable_entity,
|
||||||
payload_too_large,
|
catchers::payload_too_large,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.manage(oauth2_client)
|
.manage(oauth2_client)
|
||||||
@ -116,12 +92,13 @@ pub async fn initialize(
|
|||||||
.mount(
|
.mount(
|
||||||
"/",
|
"/",
|
||||||
routes![
|
routes![
|
||||||
routes::index,
|
|
||||||
routes::cookies,
|
routes::cookies,
|
||||||
|
routes::index,
|
||||||
|
routes::metrics::metrics,
|
||||||
routes::privacy,
|
routes::privacy,
|
||||||
routes::terms,
|
|
||||||
routes::return_to_same_site,
|
|
||||||
routes::report::report_error,
|
routes::report::report_error,
|
||||||
|
routes::return_to_same_site,
|
||||||
|
routes::terms,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
@ -152,19 +129,19 @@ pub async fn initialize(
|
|||||||
routes![
|
routes![
|
||||||
routes::dashboard::dashboard,
|
routes::dashboard::dashboard,
|
||||||
routes::dashboard::dashboard_home,
|
routes::dashboard::dashboard_home,
|
||||||
routes::dashboard::user::get_user_info,
|
routes::dashboard::api::user::get_user_info,
|
||||||
routes::dashboard::user::update_user_info,
|
routes::dashboard::api::user::update_user_info,
|
||||||
routes::dashboard::user::get_user_guilds,
|
routes::dashboard::api::user::get_user_guilds,
|
||||||
routes::dashboard::guild::get_guild_patreon,
|
routes::dashboard::api::guild::get_guild_info,
|
||||||
routes::dashboard::guild::get_guild_channels,
|
routes::dashboard::api::guild::get_guild_channels,
|
||||||
routes::dashboard::guild::get_guild_roles,
|
routes::dashboard::api::guild::get_guild_roles,
|
||||||
routes::dashboard::guild::get_reminder_templates,
|
routes::dashboard::api::guild::get_reminder_templates,
|
||||||
routes::dashboard::guild::create_reminder_template,
|
routes::dashboard::api::guild::create_reminder_template,
|
||||||
routes::dashboard::guild::delete_reminder_template,
|
routes::dashboard::api::guild::delete_reminder_template,
|
||||||
routes::dashboard::guild::create_guild_reminder,
|
routes::dashboard::api::guild::create_guild_reminder,
|
||||||
routes::dashboard::guild::get_reminders,
|
routes::dashboard::api::guild::get_reminders,
|
||||||
routes::dashboard::guild::edit_reminder,
|
routes::dashboard::api::guild::edit_reminder,
|
||||||
routes::dashboard::guild::delete_reminder,
|
routes::dashboard::api::guild::delete_reminder,
|
||||||
routes::dashboard::export::export_reminders,
|
routes::dashboard::export::export_reminders,
|
||||||
routes::dashboard::export::export_reminder_templates,
|
routes::dashboard::export::export_reminder_templates,
|
||||||
routes::dashboard::export::export_todos,
|
routes::dashboard::export::export_todos,
|
||||||
@ -222,3 +199,65 @@ pub async fn check_guild_subscription(
|
|||||||
false
|
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 {
|
macro_rules! update_field {
|
||||||
($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => {
|
($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => {
|
||||||
if let Some(value) = &$reminder.$field {
|
if let Some(value) = &$reminder.$field {
|
||||||
|
43
web/src/metrics.rs
Normal file
43
web/src/metrics.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
use prometheus::{IntCounterVec, Opts, Registry};
|
||||||
|
use rocket::{
|
||||||
|
fairing::{Fairing, Info, Kind},
|
||||||
|
Data, Request, Response,
|
||||||
|
};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref REGISTRY: Registry = Registry::new();
|
||||||
|
static ref REQUEST_COUNTER: IntCounterVec =
|
||||||
|
IntCounterVec::new(Opts::new("requests", "Requests"), &["method", "route"]).unwrap();
|
||||||
|
static ref RESPONSE_COUNTER: IntCounterVec =
|
||||||
|
IntCounterVec::new(Opts::new("responses", "Responses"), &["status", "route"]).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_metrics() {
|
||||||
|
REGISTRY.register(Box::new(REQUEST_COUNTER.clone())).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MetricProducer;
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl Fairing for MetricProducer {
|
||||||
|
fn info(&self) -> Info {
|
||||||
|
Info { name: "Metrics fairing", kind: Kind::Request }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) {
|
||||||
|
if let Some(route) = req.route() {
|
||||||
|
REQUEST_COUNTER
|
||||||
|
.with_label_values(&[req.method().as_str(), &route.uri.to_string()])
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_response<'r>(&self, req: &'r Request<'_>, resp: &mut Response<'r>) {
|
||||||
|
if let Some(route) = req.route() {
|
||||||
|
RESPONSE_COUNTER
|
||||||
|
.with_label_values(&[&resp.status().code.to_string(), &route.uri.to_string()])
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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"),
|
||||||
|
}
|
||||||
|
}
|
@ -1,321 +1,59 @@
|
|||||||
use std::env;
|
|
||||||
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::CookieJar,
|
http::CookieJar,
|
||||||
serde::json::{json, Json},
|
serde::json::{json, Json},
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
client::Context,
|
client::Context,
|
||||||
model::{
|
model::id::{ChannelId, GuildId, UserId},
|
||||||
channel::GuildChannel,
|
|
||||||
id::{ChannelId, GuildId, RoleId},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use sqlx::{MySql, Pool};
|
use sqlx::{MySql, Pool};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
check_guild_subscription, check_subscription,
|
check_authorization, check_guild_subscription, check_subscription,
|
||||||
consts::{
|
consts::MIN_INTERVAL,
|
||||||
MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
|
guards::transaction::Transaction,
|
||||||
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::{
|
routes::{
|
||||||
dashboard::{
|
dashboard::{
|
||||||
create_database_channel, create_reminder, template_name_default, DeleteReminder,
|
create_database_channel, create_reminder, DeleteReminder, PatchReminder, Reminder,
|
||||||
DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate,
|
|
||||||
},
|
},
|
||||||
JsonResult,
|
JsonResult,
|
||||||
},
|
},
|
||||||
|
Database,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[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>")]
|
#[post("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||||
pub async fn create_guild_reminder(
|
pub async fn create_guild_reminder(
|
||||||
id: u64,
|
id: u64,
|
||||||
reminder: Json<Reminder>,
|
reminder: Json<Reminder>,
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
serenity_context: &State<Context>,
|
ctx: &State<Context>,
|
||||||
pool: &State<Pool<MySql>>,
|
mut transaction: Transaction<'_>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
check_authorization!(cookies, serenity_context.inner(), id);
|
check_authorization(cookies, ctx.inner(), id).await?;
|
||||||
|
|
||||||
let user_id =
|
let user_id =
|
||||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||||
|
|
||||||
create_reminder(
|
match create_reminder(
|
||||||
serenity_context.inner(),
|
ctx.inner(),
|
||||||
pool.inner(),
|
&mut transaction,
|
||||||
GuildId(id),
|
GuildId(id),
|
||||||
UserId(user_id),
|
UserId(user_id),
|
||||||
reminder.into_inner(),
|
reminder.into_inner(),
|
||||||
)
|
)
|
||||||
.await
|
.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")]
|
#[get("/api/guild/<id>/reminders")]
|
||||||
@ -323,10 +61,9 @@ pub async fn get_reminders(
|
|||||||
id: u64,
|
id: u64,
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
ctx: &State<Context>,
|
ctx: &State<Context>,
|
||||||
serenity_context: &State<Context>,
|
|
||||||
pool: &State<Pool<MySql>>,
|
pool: &State<Pool<MySql>>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
check_authorization!(cookies, serenity_context.inner(), id);
|
check_authorization(cookies, ctx.inner(), id).await?;
|
||||||
|
|
||||||
let channels_res = GuildId(id).channels(&ctx.inner()).await;
|
let channels_res = GuildId(id).channels(&ctx.inner()).await;
|
||||||
|
|
||||||
@ -394,11 +131,12 @@ pub async fn get_reminders(
|
|||||||
pub async fn edit_reminder(
|
pub async fn edit_reminder(
|
||||||
id: u64,
|
id: u64,
|
||||||
reminder: Json<PatchReminder>,
|
reminder: Json<PatchReminder>,
|
||||||
serenity_context: &State<Context>,
|
ctx: &State<Context>,
|
||||||
pool: &State<Pool<MySql>>,
|
mut transaction: Transaction<'_>,
|
||||||
|
pool: &State<Pool<Database>>,
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
check_authorization!(cookies, serenity_context.inner(), id);
|
check_authorization(cookies, ctx.inner(), id).await?;
|
||||||
|
|
||||||
let mut error = vec![];
|
let mut error = vec![];
|
||||||
|
|
||||||
@ -406,7 +144,7 @@ pub async fn edit_reminder(
|
|||||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||||
|
|
||||||
if reminder.message_ok() {
|
if reminder.message_ok() {
|
||||||
update_field!(pool.inner(), error, reminder.[
|
update_field!(transaction.executor(), error, reminder.[
|
||||||
content,
|
content,
|
||||||
embed_author,
|
embed_author,
|
||||||
embed_description,
|
embed_description,
|
||||||
@ -419,7 +157,7 @@ pub async fn edit_reminder(
|
|||||||
error.push("Message exceeds limits.".to_string());
|
error.push("Message exceeds limits.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
update_field!(pool.inner(), error, reminder.[
|
update_field!(transaction.executor(), error, reminder.[
|
||||||
attachment,
|
attachment,
|
||||||
attachment_name,
|
attachment_name,
|
||||||
avatar,
|
avatar,
|
||||||
@ -440,8 +178,8 @@ pub async fn edit_reminder(
|
|||||||
|| reminder.interval_months.flatten().is_some()
|
|| reminder.interval_months.flatten().is_some()
|
||||||
|| reminder.interval_seconds.flatten().is_some()
|
|| reminder.interval_seconds.flatten().is_some()
|
||||||
{
|
{
|
||||||
if check_guild_subscription(&serenity_context.inner(), id).await
|
if check_guild_subscription(&ctx.inner(), id).await
|
||||||
|| check_subscription(&serenity_context.inner(), user_id).await
|
|| check_subscription(&ctx.inner(), user_id).await
|
||||||
{
|
{
|
||||||
let new_interval_length = match reminder.interval_days {
|
let new_interval_length = match reminder.interval_days {
|
||||||
Some(interval) => interval.unwrap_or(0),
|
Some(interval) => interval.unwrap_or(0),
|
||||||
@ -449,7 +187,7 @@ pub async fn edit_reminder(
|
|||||||
"SELECT interval_days AS days FROM reminders WHERE uid = ?",
|
"SELECT interval_days AS days FROM reminders WHERE uid = ?",
|
||||||
reminder.uid
|
reminder.uid
|
||||||
)
|
)
|
||||||
.fetch_one(pool.inner())
|
.fetch_one(transaction.executor())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
warn!("Error updating reminder interval: {:?}", e);
|
warn!("Error updating reminder interval: {:?}", e);
|
||||||
@ -463,7 +201,7 @@ pub async fn edit_reminder(
|
|||||||
"SELECT interval_months AS months FROM reminders WHERE uid = ?",
|
"SELECT interval_months AS months FROM reminders WHERE uid = ?",
|
||||||
reminder.uid
|
reminder.uid
|
||||||
)
|
)
|
||||||
.fetch_one(pool.inner())
|
.fetch_one(transaction.executor())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
warn!("Error updating reminder interval: {:?}", e);
|
warn!("Error updating reminder interval: {:?}", e);
|
||||||
@ -477,7 +215,7 @@ pub async fn edit_reminder(
|
|||||||
"SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?",
|
"SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?",
|
||||||
reminder.uid
|
reminder.uid
|
||||||
)
|
)
|
||||||
.fetch_one(pool.inner())
|
.fetch_one(transaction.executor())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
warn!("Error updating reminder interval: {:?}", e);
|
warn!("Error updating reminder interval: {:?}", e);
|
||||||
@ -490,17 +228,32 @@ pub async fn edit_reminder(
|
|||||||
if new_interval_length < *MIN_INTERVAL {
|
if new_interval_length < *MIN_INTERVAL {
|
||||||
error.push(String::from("New interval is too short."));
|
error.push(String::from("New interval is too short."));
|
||||||
} else {
|
} else {
|
||||||
update_field!(pool.inner(), error, reminder.[
|
update_field!(transaction.executor(), error, reminder.[
|
||||||
interval_days,
|
interval_days,
|
||||||
interval_months,
|
interval_months,
|
||||||
interval_seconds
|
interval_seconds
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE reminders
|
||||||
|
SET interval_seconds = NULL, interval_days = NULL, interval_months = NULL
|
||||||
|
WHERE uid = ?
|
||||||
|
",
|
||||||
|
reminder.uid
|
||||||
|
)
|
||||||
|
.execute(transaction.executor())
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("Error updating reminder interval: {:?}", e);
|
||||||
|
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if reminder.channel > 0 {
|
if reminder.channel > 0 {
|
||||||
let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner());
|
let channel = ChannelId(reminder.channel).to_channel_cached(&ctx.inner());
|
||||||
match channel {
|
match channel {
|
||||||
Some(channel) => {
|
Some(channel) => {
|
||||||
let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id);
|
let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id);
|
||||||
@ -515,9 +268,9 @@ pub async fn edit_reminder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let channel = create_database_channel(
|
let channel = create_database_channel(
|
||||||
serenity_context.inner(),
|
ctx.inner(),
|
||||||
ChannelId(reminder.channel),
|
ChannelId(reminder.channel),
|
||||||
pool.inner(),
|
&mut transaction,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@ -536,7 +289,7 @@ pub async fn edit_reminder(
|
|||||||
channel,
|
channel,
|
||||||
reminder.uid
|
reminder.uid
|
||||||
)
|
)
|
||||||
.execute(pool.inner())
|
.execute(transaction.executor())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
@ -559,6 +312,11 @@ pub async fn edit_reminder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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!(
|
match sqlx::query_as_unchecked!(
|
||||||
Reminder,
|
Reminder,
|
||||||
"SELECT reminders.attachment,
|
"SELECT reminders.attachment,
|
||||||
@ -605,11 +363,16 @@ pub async fn edit_reminder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/api/guild/<_>/reminders", data = "<reminder>")]
|
#[delete("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||||
pub async fn delete_reminder(
|
pub async fn delete_reminder(
|
||||||
|
cookies: &CookieJar<'_>,
|
||||||
|
id: u64,
|
||||||
reminder: Json<DeleteReminder>,
|
reminder: Json<DeleteReminder>,
|
||||||
|
ctx: &State<Context>,
|
||||||
pool: &State<Pool<MySql>>,
|
pool: &State<Pool<MySql>>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
|
check_authorization(cookies, ctx.inner(), id).await?;
|
||||||
|
|
||||||
match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid)
|
match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid)
|
||||||
.execute(pool.inner())
|
.execute(pool.inner())
|
||||||
.await
|
.await
|
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,16 +6,20 @@ use rocket::{
|
|||||||
};
|
};
|
||||||
use serenity::{
|
use serenity::{
|
||||||
client::Context,
|
client::Context,
|
||||||
model::id::{ChannelId, GuildId},
|
model::id::{ChannelId, GuildId, UserId},
|
||||||
};
|
};
|
||||||
use sqlx::{MySql, Pool};
|
use sqlx::{MySql, Pool};
|
||||||
|
|
||||||
use crate::routes::{
|
use crate::{
|
||||||
|
check_authorization,
|
||||||
|
guards::transaction::Transaction,
|
||||||
|
routes::{
|
||||||
dashboard::{
|
dashboard::{
|
||||||
create_reminder, generate_uid, ImportBody, Reminder, ReminderCsv, ReminderTemplateCsv,
|
create_reminder, generate_uid, ImportBody, Reminder, ReminderCsv, ReminderTemplateCsv,
|
||||||
TodoCsv,
|
TodoCsv,
|
||||||
},
|
},
|
||||||
JsonResult,
|
JsonResult,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[get("/api/guild/<id>/export/reminders")]
|
#[get("/api/guild/<id>/export/reminders")]
|
||||||
@ -25,7 +29,7 @@ pub async fn export_reminders(
|
|||||||
ctx: &State<Context>,
|
ctx: &State<Context>,
|
||||||
pool: &State<Pool<MySql>>,
|
pool: &State<Pool<MySql>>,
|
||||||
) -> JsonResult {
|
) -> 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![]);
|
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
|
||||||
|
|
||||||
@ -70,7 +74,7 @@ pub async fn export_reminders(
|
|||||||
reminders.utc_time
|
reminders.utc_time
|
||||||
FROM reminders
|
FROM reminders
|
||||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
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
|
channels
|
||||||
)
|
)
|
||||||
.fetch_all(pool.inner())
|
.fetch_all(pool.inner())
|
||||||
@ -118,14 +122,14 @@ pub async fn export_reminders(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[put("/api/guild/<id>/export/reminders", data = "<body>")]
|
#[put("/api/guild/<id>/export/reminders", data = "<body>")]
|
||||||
pub async fn import_reminders(
|
pub(crate) async fn import_reminders(
|
||||||
id: u64,
|
id: u64,
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
body: Json<ImportBody>,
|
body: Json<ImportBody>,
|
||||||
ctx: &State<Context>,
|
ctx: &State<Context>,
|
||||||
pool: &State<Pool<MySql>>,
|
mut transaction: Transaction<'_>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
check_authorization!(cookies, ctx.inner(), id);
|
check_authorization(cookies, ctx.inner(), id).await?;
|
||||||
|
|
||||||
let user_id =
|
let user_id =
|
||||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||||
@ -133,6 +137,7 @@ pub async fn import_reminders(
|
|||||||
match base64::decode(&body.body) {
|
match base64::decode(&body.body) {
|
||||||
Ok(body) => {
|
Ok(body) => {
|
||||||
let mut reader = csv::Reader::from_reader(body.as_slice());
|
let mut reader = csv::Reader::from_reader(body.as_slice());
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
for result in reader.deserialize::<ReminderCsv>() {
|
for result in reader.deserialize::<ReminderCsv>() {
|
||||||
match result {
|
match result {
|
||||||
@ -175,12 +180,14 @@ pub async fn import_reminders(
|
|||||||
|
|
||||||
create_reminder(
|
create_reminder(
|
||||||
ctx.inner(),
|
ctx.inner(),
|
||||||
pool.inner(),
|
&mut transaction,
|
||||||
GuildId(id),
|
GuildId(id),
|
||||||
UserId(user_id),
|
UserId(user_id),
|
||||||
reminder,
|
reminder,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@ -200,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(_) => {
|
Err(_) => {
|
||||||
@ -216,7 +232,7 @@ pub async fn export_todos(
|
|||||||
ctx: &State<Context>,
|
ctx: &State<Context>,
|
||||||
pool: &State<Pool<MySql>>,
|
pool: &State<Pool<MySql>>,
|
||||||
) -> JsonResult {
|
) -> 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![]);
|
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
|
||||||
|
|
||||||
@ -271,7 +287,7 @@ pub async fn import_todos(
|
|||||||
ctx: &State<Context>,
|
ctx: &State<Context>,
|
||||||
pool: &State<Pool<MySql>>,
|
pool: &State<Pool<MySql>>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
check_authorization!(cookies, ctx.inner(), id);
|
check_authorization(cookies, ctx.inner(), id).await?;
|
||||||
|
|
||||||
let channels_res = GuildId(id).channels(&ctx.inner()).await;
|
let channels_res = GuildId(id).channels(&ctx.inner()).await;
|
||||||
|
|
||||||
@ -366,7 +382,7 @@ pub async fn export_reminder_templates(
|
|||||||
ctx: &State<Context>,
|
ctx: &State<Context>,
|
||||||
pool: &State<Pool<MySql>>,
|
pool: &State<Pool<MySql>>,
|
||||||
) -> JsonResult {
|
) -> 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![]);
|
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
|
||||||
|
|
||||||
@ -388,6 +404,9 @@ pub async fn export_reminder_templates(
|
|||||||
embed_thumbnail_url,
|
embed_thumbnail_url,
|
||||||
embed_title,
|
embed_title,
|
||||||
embed_fields,
|
embed_fields,
|
||||||
|
interval_seconds,
|
||||||
|
interval_days,
|
||||||
|
interval_months,
|
||||||
tts,
|
tts,
|
||||||
username
|
username
|
||||||
FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
use std::collections::HashMap;
|
use std::path::Path;
|
||||||
|
|
||||||
use chrono::{naive::NaiveDateTime, Utc};
|
use chrono::{naive::NaiveDateTime, Utc};
|
||||||
use rand::{rngs::OsRng, seq::IteratorRandom};
|
use rand::{rngs::OsRng, seq::IteratorRandom};
|
||||||
use rocket::{http::CookieJar, response::Redirect, serde::json::json};
|
use rocket::{
|
||||||
use rocket_dyn_templates::Template;
|
fs::{relative, NamedFile},
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
http::CookieJar,
|
||||||
|
response::Redirect,
|
||||||
|
serde::json::json,
|
||||||
|
};
|
||||||
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use serenity::{
|
use serenity::{
|
||||||
client::Context,
|
client::Context,
|
||||||
http::Http,
|
http::Http,
|
||||||
model::id::{ChannelId, GuildId, UserId},
|
model::id::{ChannelId, GuildId, UserId},
|
||||||
};
|
};
|
||||||
use sqlx::{types::Json, Executor};
|
use sqlx::types::Json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
check_guild_subscription, check_subscription,
|
check_guild_subscription, check_subscription,
|
||||||
@ -20,13 +24,13 @@ use crate::{
|
|||||||
MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH,
|
MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH,
|
||||||
MAX_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL,
|
MAX_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL,
|
||||||
},
|
},
|
||||||
|
guards::transaction::Transaction,
|
||||||
routes::JsonResult,
|
routes::JsonResult,
|
||||||
Database, Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod api;
|
||||||
pub mod export;
|
pub mod export;
|
||||||
pub mod guild;
|
|
||||||
pub mod user;
|
|
||||||
|
|
||||||
type Unset<T> = Option<T>;
|
type Unset<T> = Option<T>;
|
||||||
|
|
||||||
@ -50,12 +54,27 @@ fn interval_default() -> Unset<Option<u32>> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
|
#[derive(sqlx::Type)]
|
||||||
where
|
#[sqlx(transparent)]
|
||||||
|
struct Attachment(Vec<u8>);
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Attachment {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Attachment, D::Error>
|
||||||
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
T: Deserialize<'de>,
|
{
|
||||||
{
|
let string = String::deserialize(deserializer)?;
|
||||||
Ok(Some(Option::deserialize(deserializer)?))
|
Ok(Attachment(base64::decode(string).map_err(de::Error::custom)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Attachment {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.collect_str(&base64::encode(&self.0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@ -66,7 +85,7 @@ pub struct ReminderTemplate {
|
|||||||
guild_id: u32,
|
guild_id: u32,
|
||||||
#[serde(default = "template_name_default")]
|
#[serde(default = "template_name_default")]
|
||||||
name: String,
|
name: String,
|
||||||
attachment: Option<Vec<u8>>,
|
attachment: Option<Attachment>,
|
||||||
attachment_name: Option<String>,
|
attachment_name: Option<String>,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
content: String,
|
content: String,
|
||||||
@ -80,6 +99,9 @@ pub struct ReminderTemplate {
|
|||||||
embed_thumbnail_url: Option<String>,
|
embed_thumbnail_url: Option<String>,
|
||||||
embed_title: String,
|
embed_title: String,
|
||||||
embed_fields: Option<Json<Vec<EmbedField>>>,
|
embed_fields: Option<Json<Vec<EmbedField>>>,
|
||||||
|
interval_seconds: Option<u32>,
|
||||||
|
interval_days: Option<u32>,
|
||||||
|
interval_months: Option<u32>,
|
||||||
tts: bool,
|
tts: bool,
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
}
|
}
|
||||||
@ -88,7 +110,7 @@ pub struct ReminderTemplate {
|
|||||||
pub struct ReminderTemplateCsv {
|
pub struct ReminderTemplateCsv {
|
||||||
#[serde(default = "template_name_default")]
|
#[serde(default = "template_name_default")]
|
||||||
name: String,
|
name: String,
|
||||||
attachment: Option<Vec<u8>>,
|
attachment: Option<Attachment>,
|
||||||
attachment_name: Option<String>,
|
attachment_name: Option<String>,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
content: String,
|
content: String,
|
||||||
@ -102,6 +124,9 @@ pub struct ReminderTemplateCsv {
|
|||||||
embed_thumbnail_url: Option<String>,
|
embed_thumbnail_url: Option<String>,
|
||||||
embed_title: String,
|
embed_title: String,
|
||||||
embed_fields: Option<String>,
|
embed_fields: Option<String>,
|
||||||
|
interval_seconds: Option<u32>,
|
||||||
|
interval_days: Option<u32>,
|
||||||
|
interval_months: Option<u32>,
|
||||||
tts: bool,
|
tts: bool,
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
}
|
}
|
||||||
@ -120,8 +145,7 @@ pub struct EmbedField {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Reminder {
|
pub struct Reminder {
|
||||||
#[serde(with = "base64s")]
|
attachment: Option<Attachment>,
|
||||||
attachment: Option<Vec<u8>>,
|
|
||||||
attachment_name: Option<String>,
|
attachment_name: Option<String>,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
#[serde(with = "string")]
|
#[serde(with = "string")]
|
||||||
@ -154,8 +178,7 @@ pub struct Reminder {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ReminderCsv {
|
pub struct ReminderCsv {
|
||||||
#[serde(with = "base64s")]
|
attachment: Option<Attachment>,
|
||||||
attachment: Option<Vec<u8>>,
|
|
||||||
attachment_name: Option<String>,
|
attachment_name: Option<String>,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
channel: String,
|
channel: String,
|
||||||
@ -188,7 +211,7 @@ pub struct PatchReminder {
|
|||||||
uid: String,
|
uid: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||||
attachment: Unset<Option<String>>,
|
attachment: Unset<Option<Attachment>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||||
attachment_name: Unset<Option<String>>,
|
attachment_name: Unset<Option<String>>,
|
||||||
@ -284,6 +307,14 @@ pub fn generate_uid() -> String {
|
|||||||
.join("")
|
.join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
T: Deserialize<'de>,
|
||||||
|
{
|
||||||
|
Ok(Some(Option::deserialize(deserializer)?))
|
||||||
|
}
|
||||||
|
|
||||||
// https://github.com/serde-rs/json/issues/329#issuecomment-305608405
|
// https://github.com/serde-rs/json/issues/329#issuecomment-305608405
|
||||||
mod string {
|
mod string {
|
||||||
use std::{fmt::Display, str::FromStr};
|
use std::{fmt::Display, str::FromStr};
|
||||||
@ -308,29 +339,6 @@ mod string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod base64s {
|
|
||||||
use serde::{de, Deserialize, Deserializer, Serializer};
|
|
||||||
|
|
||||||
pub fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
if let Some(opt) = value {
|
|
||||||
serializer.collect_str(&base64::encode(opt))
|
|
||||||
} else {
|
|
||||||
serializer.serialize_none()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let string = Option::<String>::deserialize(deserializer)?;
|
|
||||||
Some(string.map(|b| base64::decode(b).map_err(de::Error::custom))).flatten().transpose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct DeleteReminder {
|
pub struct DeleteReminder {
|
||||||
uid: String,
|
uid: String,
|
||||||
@ -347,21 +355,21 @@ pub struct TodoCsv {
|
|||||||
channel_id: Option<String>,
|
channel_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_reminder(
|
pub(crate) async fn create_reminder(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
pool: impl sqlx::Executor<'_, Database = Database> + Copy,
|
transaction: &mut Transaction<'_>,
|
||||||
guild_id: GuildId,
|
guild_id: GuildId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
reminder: Reminder,
|
reminder: Reminder,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
// check guild in db
|
// check guild in db
|
||||||
match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0)
|
match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0)
|
||||||
.fetch_one(pool)
|
.fetch_one(transaction.executor())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Err(sqlx::Error::RowNotFound) => {
|
Err(sqlx::Error::RowNotFound) => {
|
||||||
if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.0)
|
if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.0)
|
||||||
.execute(pool)
|
.execute(transaction.executor())
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
@ -387,7 +395,7 @@ pub async fn create_reminder(
|
|||||||
return Err(json!({"error": "Channel not found"}));
|
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 {
|
if let Err(e) = channel {
|
||||||
warn!("`create_database_channel` returned an error code: {:?}", e);
|
warn!("`create_database_channel` returned an error code: {:?}", e);
|
||||||
@ -461,8 +469,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 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) {
|
let username = if reminder.username.as_ref().map(|s| s.is_empty()).unwrap_or(true) {
|
||||||
None
|
None
|
||||||
@ -503,7 +509,7 @@ pub async fn create_reminder(
|
|||||||
`utc_time`
|
`utc_time`
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
new_uid,
|
new_uid,
|
||||||
attachment_data,
|
reminder.attachment,
|
||||||
reminder.attachment_name,
|
reminder.attachment_name,
|
||||||
channel,
|
channel,
|
||||||
reminder.avatar,
|
reminder.avatar,
|
||||||
@ -529,7 +535,7 @@ pub async fn create_reminder(
|
|||||||
username,
|
username,
|
||||||
reminder.utc_time,
|
reminder.utc_time,
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(transaction.executor())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => sqlx::query_as_unchecked!(
|
Ok(_) => sqlx::query_as_unchecked!(
|
||||||
@ -566,7 +572,7 @@ pub async fn create_reminder(
|
|||||||
WHERE uid = ?",
|
WHERE uid = ?",
|
||||||
new_uid
|
new_uid
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(transaction.executor())
|
||||||
.await
|
.await
|
||||||
.map(|r| Ok(json!(r)))
|
.map(|r| Ok(json!(r)))
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
@ -586,11 +592,11 @@ pub async fn create_reminder(
|
|||||||
async fn create_database_channel(
|
async fn create_database_channel(
|
||||||
ctx: impl AsRef<Http>,
|
ctx: impl AsRef<Http>,
|
||||||
channel: ChannelId,
|
channel: ChannelId,
|
||||||
pool: impl Executor<'_, Database = Database> + Copy,
|
transaction: &mut Transaction<'_>,
|
||||||
) -> Result<u32, crate::Error> {
|
) -> Result<u32, crate::Error> {
|
||||||
let row =
|
let row =
|
||||||
sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0)
|
sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0)
|
||||||
.fetch_one(pool)
|
.fetch_one(transaction.executor())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
@ -607,7 +613,7 @@ async fn create_database_channel(
|
|||||||
webhook.token,
|
webhook.token,
|
||||||
channel.0
|
channel.0
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(transaction.executor())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::SQLx(e))?;
|
.map_err(|e| Error::SQLx(e))?;
|
||||||
}
|
}
|
||||||
@ -633,7 +639,7 @@ async fn create_database_channel(
|
|||||||
webhook.token,
|
webhook.token,
|
||||||
channel.0
|
channel.0
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(transaction.executor())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::SQLx(e))?;
|
.map_err(|e| Error::SQLx(e))?;
|
||||||
|
|
||||||
@ -644,7 +650,7 @@ async fn create_database_channel(
|
|||||||
}?;
|
}?;
|
||||||
|
|
||||||
let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.0)
|
let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.0)
|
||||||
.fetch_one(pool)
|
.fetch_one(transaction.executor())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::SQLx(e))?;
|
.map_err(|e| Error::SQLx(e))?;
|
||||||
|
|
||||||
@ -652,20 +658,26 @@ async fn create_database_channel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
|
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<NamedFile, Redirect> {
|
||||||
if cookies.get_private("userid").is_some() {
|
if cookies.get_private("userid").is_some() {
|
||||||
let map: HashMap<&str, String> = HashMap::new();
|
NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| {
|
||||||
Ok(Template::render("dashboard", &map))
|
warn!("Couldn't render dashboard: {:?}", e);
|
||||||
|
|
||||||
|
Redirect::to("/login/discord")
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Redirect::to("/login/discord"))
|
Err(Redirect::to("/login/discord"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<_>")]
|
#[get("/<_..>")]
|
||||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
|
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<NamedFile, Redirect> {
|
||||||
if cookies.get_private("userid").is_some() {
|
if cookies.get_private("userid").is_some() {
|
||||||
let map: HashMap<&str, String> = HashMap::new();
|
NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| {
|
||||||
Ok(Template::render("dashboard", &map))
|
warn!("Couldn't render dashboard: {:?}", e);
|
||||||
|
|
||||||
|
Redirect::to("/login/discord")
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Redirect::to("/login/discord"))
|
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"})
|
|
||||||
}
|
|
||||||
}
|
|
@ -31,22 +31,20 @@ pub async fn discord_login(
|
|||||||
|
|
||||||
// store the pkce secret to verify the authorization later
|
// store the pkce secret to verify the authorization later
|
||||||
cookies.add_private(
|
cookies.add_private(
|
||||||
Cookie::build("verify", pkce_verifier.secret().to_string())
|
Cookie::build(("verify", pkce_verifier.secret().to_string()))
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.path("/login")
|
.path("/login")
|
||||||
.same_site(SameSite::Lax)
|
.same_site(SameSite::Lax)
|
||||||
.expires(Expiration::Session)
|
.expires(Expiration::Session),
|
||||||
.finish(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// store the csrf token to verify no interference
|
// store the csrf token to verify no interference
|
||||||
cookies.add_private(
|
cookies.add_private(
|
||||||
Cookie::build("csrf", csrf_token.secret().to_string())
|
Cookie::build(("csrf", csrf_token.secret().to_string()))
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.path("/login")
|
.path("/login")
|
||||||
.same_site(SameSite::Lax)
|
.same_site(SameSite::Lax)
|
||||||
.expires(Expiration::Session)
|
.expires(Expiration::Session),
|
||||||
.finish(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Redirect::to(auth_url.to_string())
|
Redirect::to(auth_url.to_string())
|
||||||
@ -54,9 +52,9 @@ pub async fn discord_login(
|
|||||||
|
|
||||||
#[get("/discord/logout")]
|
#[get("/discord/logout")]
|
||||||
pub async fn discord_logout(cookies: &CookieJar<'_>) -> Redirect {
|
pub async fn discord_logout(cookies: &CookieJar<'_>) -> Redirect {
|
||||||
cookies.remove_private(Cookie::named("username"));
|
cookies.remove_private(Cookie::from("username"));
|
||||||
cookies.remove_private(Cookie::named("userid"));
|
cookies.remove_private(Cookie::from("userid"));
|
||||||
cookies.remove_private(Cookie::named("access_token"));
|
cookies.remove_private(Cookie::from("access_token"));
|
||||||
|
|
||||||
Redirect::to(uri!(routes::index))
|
Redirect::to(uri!(routes::index))
|
||||||
}
|
}
|
||||||
@ -80,17 +78,16 @@ pub async fn discord_callback(
|
|||||||
.request_async(async_http_client)
|
.request_async(async_http_client)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
cookies.remove_private(Cookie::named("verify"));
|
cookies.remove_private(Cookie::from("verify"));
|
||||||
cookies.remove_private(Cookie::named("csrf"));
|
cookies.remove_private(Cookie::from("csrf"));
|
||||||
|
|
||||||
match token_result {
|
match token_result {
|
||||||
Ok(token) => {
|
Ok(token) => {
|
||||||
cookies.add_private(
|
cookies.add_private(
|
||||||
Cookie::build("access_token", token.access_token().secret().to_string())
|
Cookie::build(("access_token", token.access_token().secret().to_string()))
|
||||||
.secure(true)
|
.secure(true)
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.path("/dashboard")
|
.path("/dashboard"),
|
||||||
.finish(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let request_res = reqwest_client
|
let request_res = reqwest_client
|
||||||
|
18
web/src/routes/metrics.rs
Normal file
18
web/src/routes/metrics.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
use prometheus;
|
||||||
|
|
||||||
|
use crate::metrics::REGISTRY;
|
||||||
|
|
||||||
|
#[get("/metrics")]
|
||||||
|
pub async fn metrics() -> String {
|
||||||
|
let encoder = prometheus::TextEncoder::new();
|
||||||
|
let res_custom = encoder.encode_to_string(®ISTRY.gather());
|
||||||
|
|
||||||
|
match res_custom {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error encoding metrics: {:?}", e);
|
||||||
|
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
pub mod metrics;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -15,6 +15,22 @@ div.reminderContent.is-collapsed .column.settings {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.reminderContent.is-collapsed .reminder-settings {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderContent.is-collapsed .button-row {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderContent.is-collapsed .button-row-edit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderContent.is-collapsed .reminder-topbar {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
div.reminderContent.is-collapsed .invert-collapses {
|
div.reminderContent.is-collapsed .invert-collapses {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
@ -129,6 +145,12 @@ div.split-controls {
|
|||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reminder-settings > .column {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
div.reminderContent {
|
div.reminderContent {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@ -196,17 +218,16 @@ div.inset-content {
|
|||||||
margin-right: 10%;
|
margin-right: 10%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.flash-message {
|
div.flash-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.flash-message {
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
margin: 16px !important;
|
margin: 16px !important;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
bottom: 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.flash-message.is-active {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@ -286,17 +307,24 @@ div.dashboard-sidebar {
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.dashboard-sidebar:not(.mobile-sidebar) {
|
ul.guildList {
|
||||||
display: flex;
|
flex-grow: 1;
|
||||||
flex-direction: column;
|
flex-shrink: 1;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
|
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 226px;
|
width: 226px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.dashboard-sidebar svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
div.mobile-sidebar {
|
div.mobile-sidebar {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@ -444,8 +472,7 @@ input.default-width {
|
|||||||
.customizable.is-400x300 img {
|
.customizable.is-400x300 img {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100px;
|
height: 100px;
|
||||||
max-height: 400px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.customizable.is-32x32 img {
|
.customizable.is-32x32 img {
|
||||||
@ -589,6 +616,14 @@ input.default-width {
|
|||||||
border-bottom: 1px solid #fff;
|
border-bottom: 1px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-selector {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
li.highlight {
|
li.highlight {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
@ -597,6 +632,10 @@ li.highlight {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-row-edit > button {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.button-row .button-row-reminder {
|
.button-row .button-row-reminder {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
@ -612,7 +651,22 @@ li.highlight {
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1408px) {
|
@media only screen and (max-width: 1023px) {
|
||||||
|
p.title.pageTitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-frame {
|
||||||
|
margin-top: 4rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizable.thumbnail img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
.button-row {
|
.button-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -630,37 +684,13 @@ li.highlight {
|
|||||||
.button-row button {
|
.button-row button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
.reminder-settings {
|
||||||
.button-row-edit {
|
margin-bottom: 0 !important;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row-edit > button {
|
.tts-row {
|
||||||
width: 100%;
|
padding-bottom: 0;
|
||||||
margin: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.title.pageTitle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-frame {
|
|
||||||
margin-top: 4rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
|
||||||
.customizable.thumbnail img {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customizable.is-24x24 img {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -679,6 +709,86 @@ li.highlight {
|
|||||||
|
|
||||||
/* END */
|
/* END */
|
||||||
|
|
||||||
|
div.reminderError {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorHead {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorIcon {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorIcon .fas {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="deleted"] .errorIcon {
|
||||||
|
background-color: #e7e5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="failed"] .errorIcon {
|
||||||
|
background-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="sent"] .errorIcon {
|
||||||
|
background-color: #d9f99d;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="sent"] .errorIcon .fas.fa-check {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorHead .reminderName {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgb(54, 54, 54);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorHead .reminderTime {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 1;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgb(54, 54, 54);
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-color: #e5e5e5;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .reminderMessage {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgb(54, 54, 54);
|
||||||
|
flex-grow: 1;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* other stuff */
|
/* other stuff */
|
||||||
|
|
||||||
.half-rem {
|
.half-rem {
|
||||||
@ -716,6 +826,18 @@ a.switch-pane {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guild-submenu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guild-submenu li {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.switch-pane.is-active ~ .guild-submenu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.feedback {
|
.feedback {
|
||||||
background-color: #5865F2;
|
background-color: #5865F2;
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,16 @@ let globalPatreon = false;
|
|||||||
let guildPatreon = false;
|
let guildPatreon = false;
|
||||||
|
|
||||||
function guildId() {
|
function guildId() {
|
||||||
return document.querySelector(".guildList a.is-active").dataset["guild"];
|
return window.location.pathname.match(/dashboard\/(\d+)/)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function pane() {
|
||||||
|
const match = window.location.pathname.match(/dashboard\/\d+\/(.+)/);
|
||||||
|
if (match === null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function colorToInt(r, g, b) {
|
function colorToInt(r, g, b) {
|
||||||
@ -96,7 +105,7 @@ function reset_guild_pane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetch_patreon(guild_id) {
|
async function fetch_patreon(guild_id) {
|
||||||
fetch(`/dashboard/api/guild/${guild_id}/patreon`)
|
fetch(`/dashboard/api/guild/${guild_id}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
@ -223,11 +232,10 @@ async function fetch_reminders(guild_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function serialize_reminder(node, mode) {
|
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") {
|
if (mode !== "template") {
|
||||||
interval = get_interval(node);
|
|
||||||
|
|
||||||
utc_time = luxon.DateTime.fromISO(
|
utc_time = luxon.DateTime.fromISO(
|
||||||
node.querySelector('input[name="time"]').value
|
node.querySelector('input[name="time"]').value
|
||||||
).setZone("UTC");
|
).setZone("UTC");
|
||||||
@ -356,9 +364,9 @@ async function serialize_reminder(node, mode) {
|
|||||||
embed_title: embed_title,
|
embed_title: embed_title,
|
||||||
embed_fields: fields,
|
embed_fields: fields,
|
||||||
expires: expiration_time,
|
expires: expiration_time,
|
||||||
interval_seconds: mode !== "template" ? interval.seconds : null,
|
interval_seconds: interval.seconds,
|
||||||
interval_days: mode !== "template" ? interval.days : null,
|
interval_days: interval.days,
|
||||||
interval_months: mode !== "template" ? interval.months : null,
|
interval_months: interval.months,
|
||||||
name: node.querySelector('input[name="name"]').value,
|
name: node.querySelector('input[name="name"]').value,
|
||||||
tts: node.querySelector('input[name="tts"]').checked,
|
tts: node.querySelector('input[name="tts"]').checked,
|
||||||
username: node.querySelector('input[name="username"]').value,
|
username: node.querySelector('input[name="username"]').value,
|
||||||
@ -420,9 +428,9 @@ function deserialize_reminder(reminder, frame, mode) {
|
|||||||
.insertBefore(embed_field, lastChild);
|
.insertBefore(embed_field, lastChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode !== "template") {
|
|
||||||
if (reminder["interval_seconds"]) update_interval(frame);
|
if (reminder["interval_seconds"]) update_interval(frame);
|
||||||
|
|
||||||
|
if (mode !== "template") {
|
||||||
let $enableBtn = frame.querySelector(".disable-enable");
|
let $enableBtn = frame.querySelector(".disable-enable");
|
||||||
$enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable";
|
$enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable";
|
||||||
|
|
||||||
@ -455,15 +463,17 @@ document.addEventListener("guildSwitched", async (e) => {
|
|||||||
|
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
|
||||||
if ($anchor === null) {
|
if (pane() === null) {
|
||||||
switch_pane("user-error");
|
window.history.replaceState({}, "", `/dashboard/${guildId()}/reminders`);
|
||||||
hasError = true;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch_pane($anchor.dataset["pane"]);
|
switch_pane(pane());
|
||||||
reset_guild_pane();
|
|
||||||
|
if ($anchor !== null) {
|
||||||
$anchor.classList.add("is-active");
|
$anchor.classList.add("is-active");
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_guild_pane();
|
||||||
|
|
||||||
if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) {
|
if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) {
|
||||||
document
|
document
|
||||||
@ -604,6 +614,16 @@ function show_error(error) {
|
|||||||
}, 5000);
|
}, 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.value = colorPicker.color.hexString;
|
||||||
|
|
||||||
$colorPickerInput.addEventListener("input", () => {
|
$colorPickerInput.addEventListener("input", () => {
|
||||||
@ -686,11 +706,15 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
);
|
);
|
||||||
$anchor.dataset["guild"] = guild.id;
|
$anchor.dataset["guild"] = guild.id;
|
||||||
$anchor.dataset["name"] = guild.name;
|
$anchor.dataset["name"] = guild.name;
|
||||||
$anchor.href = `/dashboard/${guild.id}?name=${guild.name}`;
|
$anchor.href = `/dashboard/${guild.id}/reminders`;
|
||||||
|
|
||||||
$anchor.addEventListener("click", async (e) => {
|
$anchor.addEventListener("click", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.history.pushState({}, "", `/dashboard/${guild.id}`);
|
window.history.pushState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
`/dashboard/${guild.id}/reminders`
|
||||||
|
);
|
||||||
const event = new CustomEvent("guildSwitched", {
|
const event = new CustomEvent("guildSwitched", {
|
||||||
detail: {
|
detail: {
|
||||||
guild_name: guild.name,
|
guild_name: guild.name,
|
||||||
@ -754,11 +778,25 @@ $uploader.addEventListener("change", (ev) => {
|
|||||||
fileReader.onload = (e) => resolve(fileReader.result);
|
fileReader.onload = (e) => resolve(fileReader.result);
|
||||||
fileReader.readAsDataURL($uploader.files[0]);
|
fileReader.readAsDataURL($uploader.files[0]);
|
||||||
}).then((dataUrl) => {
|
}).then((dataUrl) => {
|
||||||
|
$importBtn.setAttribute("disabled", true);
|
||||||
|
|
||||||
fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, {
|
fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({ body: dataUrl.split(",")[1] }),
|
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];
|
delete $uploader.files[0];
|
||||||
|
fetch_reminders(guild);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
let _reminderErrors = [];
|
|
||||||
|
|
||||||
const reminderErrors = () => {
|
|
||||||
return _reminderErrors;
|
|
||||||
}
|
|
||||||
|
|
||||||
const guildId = () => {
|
|
||||||
let selected: HTMLElement = document.querySelector(".guildList a.is-active");
|
|
||||||
return selected.dataset["guild"];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function loadErrors() {
|
|
||||||
fetch(`/dashboard/api/guild/${guildId()}/errors`).then(response => response.json())
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
|
|
||||||
})
|
|
@ -27,7 +27,7 @@
|
|||||||
<link rel="stylesheet" href="/static/css/bulma.min.css">
|
<link rel="stylesheet" href="/static/css/bulma.min.css">
|
||||||
<link rel="stylesheet" href="/static/css/fa.css">
|
<link rel="stylesheet" href="/static/css/fa.css">
|
||||||
<link rel="stylesheet" href="/static/css/font.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="/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" />
|
<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" />
|
||||||
|
|
||||||
@ -40,7 +40,7 @@
|
|||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item" href="/">
|
<a class="navbar-item" href="/">
|
||||||
<figure class="image">
|
<figure class="image">
|
||||||
<img src="/static/img/logo_nobg.webp" alt="Reminder Bot Logo">
|
<img width="28px" height="28px" src="/static/img/logo_nobg.webp" alt="Reminder Bot Logo">
|
||||||
</figure>
|
</figure>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@ -76,6 +76,10 @@
|
|||||||
<span class="icon"><i class="far fa-exclamation-circle"></i></span> <span class="error-message"></span>
|
<span class="icon"><i class="far fa-exclamation-circle"></i></span> <span class="error-message"></span>
|
||||||
</div>
|
</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" id="addImageModal">
|
||||||
<div class="modal-background"></div>
|
<div class="modal-background"></div>
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
@ -185,14 +189,6 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<br>
|
||||||
<div class="has-text-centered">
|
<div class="has-text-centered">
|
||||||
<div style="color: red">
|
<div style="color: red">
|
||||||
@ -234,6 +230,7 @@
|
|||||||
<a href="/">
|
<a href="/">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<img src="/static/img/logo_nobg.webp" alt="Reminder bot logo"
|
<img src="/static/img/logo_nobg.webp" alt="Reminder bot logo"
|
||||||
|
width="52px" height="52px"
|
||||||
class="dashboard-brand">
|
class="dashboard-brand">
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@ -256,11 +253,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
{#
|
|
||||||
<a class="show-modal" data-modal="dataManagerModal">
|
<a class="show-modal" data-modal="dataManagerModal">
|
||||||
<span class="icon"><i class="fas fa-exchange"></i></span> Import/Export
|
<span class="icon"><i class="fas fa-exchange"></i></span> Import/Export
|
||||||
</a>
|
</a>
|
||||||
#}
|
|
||||||
<a class="show-modal" data-modal="chooseTimezoneModal">
|
<a class="show-modal" data-modal="chooseTimezoneModal">
|
||||||
<span class="icon"><i class="fas fa-map-marked"></i></span> Timezone
|
<span class="icon"><i class="fas fa-map-marked"></i></span> Timezone
|
||||||
</a>
|
</a>
|
||||||
@ -302,11 +297,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
{#
|
|
||||||
<a class="show-modal" data-modal="dataManagerModal">
|
<a class="show-modal" data-modal="dataManagerModal">
|
||||||
<span class="icon"><i class="fas fa-exchange"></i></span> Import/Export
|
<span class="icon"><i class="fas fa-exchange"></i></span> Import/Export
|
||||||
</a>
|
</a>
|
||||||
#}
|
|
||||||
<a class="show-modal" data-modal="chooseTimezoneModal">
|
<a class="show-modal" data-modal="chooseTimezoneModal">
|
||||||
<span class="icon"><i class="fas fa-map-marked"></i></span> Timezone
|
<span class="icon"><i class="fas fa-map-marked"></i></span> Timezone
|
||||||
</a>
|
</a>
|
||||||
@ -332,7 +325,7 @@
|
|||||||
<p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p>
|
<p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section id="guild" class="is-hidden">
|
<section id="reminders" class="is-hidden">
|
||||||
{% include "reminder_dashboard/reminder_dashboard" %}
|
{% include "reminder_dashboard/reminder_dashboard" %}
|
||||||
</section>
|
</section>
|
||||||
<section id="reminder-errors" class="is-hidden">
|
<section id="reminder-errors" class="is-hidden">
|
||||||
@ -388,9 +381,9 @@
|
|||||||
<script src="/static/js/iro.js"></script>
|
<script src="/static/js/iro.js"></script>
|
||||||
<script src="/static/js/dtsel.js"></script>
|
<script src="/static/js/dtsel.js"></script>
|
||||||
|
|
||||||
<script src="/static/js/interval.js"></script>
|
<script src="/static/js/interval.js?v{{ version }}"></script>
|
||||||
<script src="/static/js/timezone.js" defer></script>
|
<script src="/static/js/timezone.js?v{{ version }}" defer></script>
|
||||||
<script src="/static/js/main.js" defer></script>
|
<script src="/static/js/main.js?v{{ version }}" defer></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -133,8 +133,6 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div class="column settings">
|
<div class="column settings">
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<div class="field channel-field">
|
<div class="field channel-field">
|
||||||
<div class="collapses">
|
<div class="collapses">
|
||||||
<label class="label" for="channelOption">Channel*</label>
|
<label class="label" for="channelOption">Channel*</label>
|
||||||
@ -149,8 +147,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label class="label collapses">
|
<label class="label collapses">
|
||||||
@ -159,8 +156,6 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="collapses split-controls">
|
<div class="collapses split-controls">
|
||||||
<div>
|
<div>
|
||||||
@ -236,7 +231,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% if creating %}
|
{% if creating %}
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<div class="button-row-reminder">
|
<div class="button-row-reminder">
|
||||||
@ -269,7 +266,4 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/reminder_errors.js"></script>
|
<!--<script src="/static/js/reminder_errors.js"></script>-->
|
||||||
|
Reference in New Issue
Block a user