Compare commits
	
		
			26 Commits
		
	
	
		
			8587bed703
			...
			jude/fix-o
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | adb9c728f4 | ||
|  | 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 | 
							
								
								
									
										1501
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1501
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										10
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -1,18 +1,18 @@ | ||||
| [package] | ||||
| name = "reminder-rs" | ||||
| version = "1.6.38" | ||||
| version = "1.6.48" | ||||
| authors = ["Jude Southworth <judesouthworth@pm.me>"] | ||||
| edition = "2021" | ||||
| license = "AGPL-3.0 only" | ||||
| description = "Reminder Bot for Discord, now in Rust" | ||||
|  | ||||
| [dependencies] | ||||
| poise = "0.5.5" | ||||
| poise = "0.5" | ||||
| dotenv = "0.15" | ||||
| tokio = { version = "1", features = ["process", "full"] } | ||||
| reqwest = "0.11" | ||||
| lazy-regex = "2.3.0" | ||||
| regex = "1.6" | ||||
| lazy-regex = "3.0.2" | ||||
| regex = "1.9" | ||||
| log = "0.4" | ||||
| env_logger = "0.10" | ||||
| chrono = "0.4" | ||||
| @@ -25,7 +25,7 @@ serde_repr = "0.1" | ||||
| rmp-serde = "1.1" | ||||
| rand = "0.8" | ||||
| levenshtein = "1.0" | ||||
| sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]} | ||||
| sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]} | ||||
| base64 = "0.21.0" | ||||
|  | ||||
| [dependencies.postman] | ||||
|   | ||||
| @@ -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] | ||||
| tokio = { version = "1", features = ["process", "full"] } | ||||
| regex = "1.4" | ||||
| regex = "1.9" | ||||
| log = "0.4" | ||||
| chrono = "0.4" | ||||
| chrono-tz = { version = "0.5", features = ["serde"] } | ||||
| chrono-tz = { version = "0.8", features = ["serde"] } | ||||
| lazy_static = "1.4" | ||||
| num-integer = "0.1" | ||||
| serde = "1.0" | ||||
| sqlx = { version = "0.6", 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"] } | ||||
| sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]} | ||||
| 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 = ?)", | ||||
|         guild_id.0 | ||||
|     ) | ||||
|     .fetch_all(&mut transaction) | ||||
|     .fetch_all(&mut *transaction) | ||||
|     .await?; | ||||
|  | ||||
|     let mut added_aliases = 0; | ||||
| @@ -42,7 +42,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> { | ||||
|                     cmd_macro.description, | ||||
|                     cmd_macro.commands | ||||
|                 ) | ||||
|                 .execute(&mut transaction) | ||||
|                 .execute(&mut *transaction) | ||||
|                 .await?; | ||||
|  | ||||
|                 added_aliases += 1; | ||||
|   | ||||
| @@ -114,6 +114,8 @@ pub async fn offset( | ||||
|     #[description = "Number of minutes to offset by"] minutes: Option<isize>, | ||||
|     #[description = "Number of seconds to offset by"] seconds: Option<isize>, | ||||
| ) -> Result<(), Error> { | ||||
|     ctx.defer().await?; | ||||
|  | ||||
|     let combined_time = hours.map_or(0, |h| h * HOUR as isize) | ||||
|         + minutes.map_or(0, |m| m * MINUTE as isize) | ||||
|         + seconds.map_or(0, |s| s); | ||||
| @@ -619,7 +621,7 @@ pub async fn multiline( | ||||
| )] | ||||
| pub async fn remind( | ||||
|     ctx: ApplicationContext<'_>, | ||||
|     #[description = "A description of the time to set the reminder for"] | ||||
|     #[description = "The time (and optionally date) to set the reminder for"] | ||||
|     #[autocomplete = "time_hint_autocomplete"] | ||||
|     time: String, | ||||
|     #[description = "The message content to send"] content: String, | ||||
| @@ -773,7 +775,7 @@ async fn create_reminder( | ||||
|                                     b.emoji(ReactionType::Unicode("📝".to_string())) | ||||
|                                         .label("Edit") | ||||
|                                         .style(ButtonStyle::Link) | ||||
|                                         .url("https://reminder-bot.com/dashboard") | ||||
|                                         .url("https://beta.reminder-bot.com/dashboard") | ||||
|                                 }) | ||||
|                             }) | ||||
|                         }) | ||||
|   | ||||
| @@ -150,7 +150,7 @@ impl<'a> Parser<'a> { | ||||
|             "hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0), | ||||
|             "days" | "day" | "d" => (0, n, 0, 0), | ||||
|             "weeks" | "week" | "w" => (0, n.mul(7)?, 0, 0), | ||||
|             "months" | "month" | "M" => (n, 0, 0, 0), | ||||
|             "months" | "month" => (n, 0, 0, 0), | ||||
|             "years" | "year" | "y" => (n.mul(12)?, 0, 0, 0), | ||||
|             _ => { | ||||
|                 return Err(Error::UnknownUnit { | ||||
| @@ -255,7 +255,7 @@ impl<'a> Parser<'a> { | ||||
| /// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000))); | ||||
| /// ``` | ||||
| pub fn parse_duration(s: &str) -> Result<Interval, Error> { | ||||
|     Parser { iter: s.chars(), src: s, current: (0, 0, 0, 0) }.parse() | ||||
|     Parser { iter: s.to_lowercase().chars(), src: &s.to_lowercase(), current: (0, 0, 0, 0) }.parse() | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| @@ -324,4 +324,13 @@ mod tests { | ||||
|         assert_eq!(interval.day, 0); | ||||
|         assert_eq!(interval.month, 120); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn parse_case() { | ||||
|         let interval = parse_duration("200 Seconds").unwrap(); | ||||
|  | ||||
|         assert_eq!(interval.sec, 200); | ||||
|         assert_eq!(interval.day, 0); | ||||
|         assert_eq!(interval.month, 0); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,21 +1,21 @@ | ||||
| [package] | ||||
| name = "reminder_web" | ||||
| version = "0.1.0" | ||||
| version = "0.1.2" | ||||
| authors = ["jellywx <judesouthworth@pm.me>"] | ||||
| edition = "2018" | ||||
|  | ||||
| [dependencies] | ||||
| 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"] } | ||||
| 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" | ||||
| log = "0.4" | ||||
| reqwest = "0.11" | ||||
| 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-tz = "0.5" | ||||
| chrono-tz = "0.8" | ||||
| lazy_static = "1.4.0" | ||||
| rand = "0.7" | ||||
| rand = "0.8" | ||||
| base64 = "0.13" | ||||
| csv = "1.1" | ||||
| csv = "1.2" | ||||
|   | ||||
							
								
								
									
										40
									
								
								web/src/catchers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								web/src/catchers.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use rocket::serde::json::json; | ||||
| use rocket_dyn_templates::Template; | ||||
|  | ||||
| use crate::JsonValue; | ||||
|  | ||||
| #[catch(403)] | ||||
| pub(crate) async fn forbidden() -> Template { | ||||
|     let map: HashMap<String, String> = HashMap::new(); | ||||
|     Template::render("errors/403", &map) | ||||
| } | ||||
|  | ||||
| #[catch(500)] | ||||
| pub(crate) async fn internal_server_error() -> Template { | ||||
|     let map: HashMap<String, String> = HashMap::new(); | ||||
|     Template::render("errors/500", &map) | ||||
| } | ||||
|  | ||||
| #[catch(401)] | ||||
| pub(crate) async fn not_authorized() -> Template { | ||||
|     let map: HashMap<String, String> = HashMap::new(); | ||||
|     Template::render("errors/401", &map) | ||||
| } | ||||
|  | ||||
| #[catch(404)] | ||||
| pub(crate) async fn not_found() -> Template { | ||||
|     let map: HashMap<String, String> = HashMap::new(); | ||||
|     Template::render("errors/404", &map) | ||||
| } | ||||
|  | ||||
| #[catch(413)] | ||||
| pub(crate) async fn payload_too_large() -> JsonValue { | ||||
|     json!({"error": "Data too large.", "errors": ["Data too large."]}) | ||||
| } | ||||
|  | ||||
| #[catch(422)] | ||||
| pub(crate) async fn unprocessable_entity() -> JsonValue { | ||||
|     json!({"error": "Invalid request.", "errors": ["Invalid request."]}) | ||||
| } | ||||
							
								
								
									
										1
									
								
								web/src/guards/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/src/guards/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| pub(crate) mod transaction; | ||||
							
								
								
									
										44
									
								
								web/src/guards/transaction.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								web/src/guards/transaction.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| use rocket::{ | ||||
|     http::Status, | ||||
|     request::{FromRequest, Outcome}, | ||||
|     Request, State, | ||||
| }; | ||||
| use sqlx::Pool; | ||||
|  | ||||
| use crate::Database; | ||||
|  | ||||
| pub struct Transaction<'a>(sqlx::Transaction<'a, Database>); | ||||
|  | ||||
| impl Transaction<'_> { | ||||
|     pub fn executor(&mut self) -> impl sqlx::Executor<'_, Database = Database> { | ||||
|         &mut *(self.0) | ||||
|     } | ||||
|  | ||||
|     pub async fn commit(self) -> Result<(), sqlx::Error> { | ||||
|         self.0.commit().await | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub enum TransactionError { | ||||
|     Error(sqlx::Error), | ||||
|     Missing, | ||||
| } | ||||
|  | ||||
| #[rocket::async_trait] | ||||
| impl<'r> FromRequest<'r> for Transaction<'r> { | ||||
|     type Error = TransactionError; | ||||
|  | ||||
|     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { | ||||
|         match request.guard::<&State<Pool<Database>>>().await { | ||||
|             Outcome::Success(pool) => match pool.begin().await { | ||||
|                 Ok(transaction) => Outcome::Success(Transaction(transaction)), | ||||
|                 Err(e) => { | ||||
|                     Outcome::Failure((Status::InternalServerError, TransactionError::Error(e))) | ||||
|                 } | ||||
|             }, | ||||
|             Outcome::Failure(e) => Outcome::Failure((e.0, TransactionError::Missing)), | ||||
|             Outcome::Forward(f) => Outcome::Forward(f), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										139
									
								
								web/src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										139
									
								
								web/src/lib.rs
									
									
									
									
									
								
							| @@ -4,13 +4,16 @@ extern crate rocket; | ||||
| mod consts; | ||||
| #[macro_use] | ||||
| mod macros; | ||||
| mod catchers; | ||||
| mod guards; | ||||
| mod routes; | ||||
|  | ||||
| use std::{collections::HashMap, env, path::Path}; | ||||
| use std::{env, path::Path}; | ||||
|  | ||||
| use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; | ||||
| use rocket::{ | ||||
|     fs::FileServer, | ||||
|     http::CookieJar, | ||||
|     serde::json::{json, Value as JsonValue}, | ||||
|     tokio::sync::broadcast::Sender, | ||||
| }; | ||||
| @@ -32,40 +35,6 @@ enum Error { | ||||
|     Serenity(serenity::Error), | ||||
| } | ||||
|  | ||||
| #[catch(401)] | ||||
| async fn not_authorized() -> Template { | ||||
|     let map: HashMap<String, String> = HashMap::new(); | ||||
|     Template::render("errors/401", &map) | ||||
| } | ||||
|  | ||||
| #[catch(403)] | ||||
| async fn forbidden() -> Template { | ||||
|     let map: HashMap<String, String> = HashMap::new(); | ||||
|     Template::render("errors/403", &map) | ||||
| } | ||||
|  | ||||
| #[catch(404)] | ||||
| async fn not_found() -> Template { | ||||
|     let map: HashMap<String, String> = HashMap::new(); | ||||
|     Template::render("errors/404", &map) | ||||
| } | ||||
|  | ||||
| #[catch(413)] | ||||
| async fn payload_too_large() -> JsonValue { | ||||
|     json!({"error": "Data too large.", "errors": ["Data too large."]}) | ||||
| } | ||||
|  | ||||
| #[catch(422)] | ||||
| async fn unprocessable_entity() -> JsonValue { | ||||
|     json!({"error": "Invalid request.", "errors": ["Invalid request."]}) | ||||
| } | ||||
|  | ||||
| #[catch(500)] | ||||
| async fn internal_server_error() -> Template { | ||||
|     let map: HashMap<String, String> = HashMap::new(); | ||||
|     Template::render("errors/500", &map) | ||||
| } | ||||
|  | ||||
| pub async fn initialize( | ||||
|     kill_channel: Sender<()>, | ||||
|     serenity_context: Context, | ||||
| @@ -100,12 +69,12 @@ pub async fn initialize( | ||||
|         .register( | ||||
|             "/", | ||||
|             catchers![ | ||||
|                 not_authorized, | ||||
|                 forbidden, | ||||
|                 not_found, | ||||
|                 internal_server_error, | ||||
|                 unprocessable_entity, | ||||
|                 payload_too_large, | ||||
|                 catchers::not_authorized, | ||||
|                 catchers::forbidden, | ||||
|                 catchers::not_found, | ||||
|                 catchers::internal_server_error, | ||||
|                 catchers::unprocessable_entity, | ||||
|                 catchers::payload_too_large, | ||||
|             ], | ||||
|         ) | ||||
|         .manage(oauth2_client) | ||||
| @@ -152,19 +121,19 @@ pub async fn initialize( | ||||
|             routes![ | ||||
|                 routes::dashboard::dashboard, | ||||
|                 routes::dashboard::dashboard_home, | ||||
|                 routes::dashboard::user::get_user_info, | ||||
|                 routes::dashboard::user::update_user_info, | ||||
|                 routes::dashboard::user::get_user_guilds, | ||||
|                 routes::dashboard::guild::get_guild_patreon, | ||||
|                 routes::dashboard::guild::get_guild_channels, | ||||
|                 routes::dashboard::guild::get_guild_roles, | ||||
|                 routes::dashboard::guild::get_reminder_templates, | ||||
|                 routes::dashboard::guild::create_reminder_template, | ||||
|                 routes::dashboard::guild::delete_reminder_template, | ||||
|                 routes::dashboard::guild::create_guild_reminder, | ||||
|                 routes::dashboard::guild::get_reminders, | ||||
|                 routes::dashboard::guild::edit_reminder, | ||||
|                 routes::dashboard::guild::delete_reminder, | ||||
|                 routes::dashboard::api::user::get_user_info, | ||||
|                 routes::dashboard::api::user::update_user_info, | ||||
|                 routes::dashboard::api::user::get_user_guilds, | ||||
|                 routes::dashboard::api::guild::get_guild_info, | ||||
|                 routes::dashboard::api::guild::get_guild_channels, | ||||
|                 routes::dashboard::api::guild::get_guild_roles, | ||||
|                 routes::dashboard::api::guild::get_reminder_templates, | ||||
|                 routes::dashboard::api::guild::create_reminder_template, | ||||
|                 routes::dashboard::api::guild::delete_reminder_template, | ||||
|                 routes::dashboard::api::guild::create_guild_reminder, | ||||
|                 routes::dashboard::api::guild::get_reminders, | ||||
|                 routes::dashboard::api::guild::edit_reminder, | ||||
|                 routes::dashboard::api::guild::delete_reminder, | ||||
|                 routes::dashboard::export::export_reminders, | ||||
|                 routes::dashboard::export::export_reminder_templates, | ||||
|                 routes::dashboard::export::export_todos, | ||||
| @@ -222,3 +191,65 @@ pub async fn check_guild_subscription( | ||||
|         false | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn check_authorization( | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &Context, | ||||
|     guild: u64, | ||||
| ) -> Result<(), JsonValue> { | ||||
|     let user_id = cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten(); | ||||
|  | ||||
|     if std::env::var("OFFLINE").map_or(true, |v| v != "1") { | ||||
|         match user_id { | ||||
|             Some(user_id) => { | ||||
|                 let admin_id = std::env::var("ADMIN_ID") | ||||
|                     .map_or(false, |u| u.parse::<u64>().map_or(false, |u| u == user_id)); | ||||
|  | ||||
|                 if admin_id { | ||||
|                     return Ok(()); | ||||
|                 } | ||||
|  | ||||
|                 match GuildId(guild).to_guild_cached(ctx) { | ||||
|                     Some(guild) => { | ||||
|                         let member_res = guild.member(ctx, UserId(user_id)).await; | ||||
|  | ||||
|                         match member_res { | ||||
|                             Err(_) => { | ||||
|                                 return Err(json!({"error": "User not in guild"})); | ||||
|                             } | ||||
|  | ||||
|                             Ok(member) => { | ||||
|                                 let permissions_res = member.permissions(ctx); | ||||
|  | ||||
|                                 match permissions_res { | ||||
|                                     Err(_) => { | ||||
|                                         return Err(json!({"error": "Couldn't fetch permissions"})); | ||||
|                                     } | ||||
|  | ||||
|                                     Ok(permissions) => { | ||||
|                                         if !(permissions.manage_messages() | ||||
|                                             || permissions.manage_guild() | ||||
|                                             || permissions.administrator()) | ||||
|                                         { | ||||
|                                             return Err(json!({"error": "Incorrect permissions"})); | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     None => { | ||||
|                         return Err(json!({"error": "Bot not in guild"})); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             None => { | ||||
|                 return Err(json!({"error": "User not authorized"})); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|   | ||||
| @@ -54,56 +54,6 @@ macro_rules! check_url_opt { | ||||
|     }; | ||||
| } | ||||
|  | ||||
| macro_rules! check_authorization { | ||||
|     ($cookies:expr, $ctx:expr, $guild:expr) => { | ||||
|         use serenity::model::id::UserId; | ||||
|  | ||||
|         let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten(); | ||||
|  | ||||
|         if std::env::var("OFFLINE").map_or(true, |v| v != "1") { | ||||
|             match user_id { | ||||
|                 Some(user_id) => { | ||||
|                     match GuildId($guild).to_guild_cached($ctx) { | ||||
|                         Some(guild) => { | ||||
|                             let member_res = guild.member($ctx, UserId(user_id)).await; | ||||
|  | ||||
|                             match member_res { | ||||
|                                 Err(_) => { | ||||
|                                     return Err(json!({"error": "User not in guild"})); | ||||
|                                 } | ||||
|  | ||||
|                                 Ok(member) => { | ||||
|                                     let permissions_res = member.permissions($ctx); | ||||
|  | ||||
|                                     match permissions_res { | ||||
|                                         Err(_) => { | ||||
|                                             return Err(json!({"error": "Couldn't fetch permissions"})); | ||||
|                                         } | ||||
|  | ||||
|                                         Ok(permissions) => { | ||||
|                                             if !(permissions.manage_messages() || permissions.manage_guild() || permissions.administrator()) { | ||||
|                                                 return Err(json!({"error": "Incorrect permissions"})); | ||||
|                                             } | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         None => { | ||||
|                             return Err(json!({"error": "Bot not in guild"})); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 None => { | ||||
|                     return Err(json!({"error": "User not authorized"})); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| macro_rules! update_field { | ||||
|     ($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => { | ||||
|         if let Some(value) = &$reminder.$field { | ||||
|   | ||||
							
								
								
									
										61
									
								
								web/src/routes/dashboard/api/guild/channels.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								web/src/routes/dashboard/api/guild/channels.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| use rocket::{http::CookieJar, serde::json::json, State}; | ||||
| use serde::Serialize; | ||||
| use serenity::{ | ||||
|     client::Context, | ||||
|     model::{ | ||||
|         channel::GuildChannel, | ||||
|         id::{ChannelId, GuildId}, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| use crate::{check_authorization, routes::JsonResult}; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| struct ChannelInfo { | ||||
|     id: String, | ||||
|     name: String, | ||||
|     webhook_avatar: Option<String>, | ||||
|     webhook_name: Option<String>, | ||||
| } | ||||
|  | ||||
| #[get("/api/guild/<id>/channels")] | ||||
| pub async fn get_guild_channels( | ||||
|     id: u64, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
| ) -> JsonResult { | ||||
|     offline!(Ok(json!(vec![ChannelInfo { | ||||
|         name: "general".to_string(), | ||||
|         id: "1".to_string(), | ||||
|         webhook_avatar: None, | ||||
|         webhook_name: None, | ||||
|     }]))); | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     match GuildId(id).to_guild_cached(ctx.inner()) { | ||||
|         Some(guild) => { | ||||
|             let mut channels = guild | ||||
|                 .channels | ||||
|                 .iter() | ||||
|                 .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c))) | ||||
|                 .filter(|(_, channel)| channel.is_text_based()) | ||||
|                 .collect::<Vec<(ChannelId, GuildChannel)>>(); | ||||
|  | ||||
|             channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position)); | ||||
|  | ||||
|             let channel_info = channels | ||||
|                 .iter() | ||||
|                 .map(|(channel_id, channel)| ChannelInfo { | ||||
|                     name: channel.name.to_string(), | ||||
|                     id: channel_id.to_string(), | ||||
|                     webhook_avatar: None, | ||||
|                     webhook_name: None, | ||||
|                 }) | ||||
|                 .collect::<Vec<ChannelInfo>>(); | ||||
|  | ||||
|             Ok(json!(channel_info)) | ||||
|         } | ||||
|  | ||||
|         None => json_err!("Bot not in guild"), | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								web/src/routes/dashboard/api/guild/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								web/src/routes/dashboard/api/guild/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| mod channels; | ||||
| mod reminders; | ||||
| mod roles; | ||||
| mod templates; | ||||
|  | ||||
| use std::env; | ||||
|  | ||||
| pub use channels::*; | ||||
| pub use reminders::*; | ||||
| use rocket::{http::CookieJar, serde::json::json, State}; | ||||
| pub use roles::*; | ||||
| use serenity::{ | ||||
|     client::Context, | ||||
|     model::id::{GuildId, RoleId}, | ||||
| }; | ||||
| pub use templates::*; | ||||
|  | ||||
| use crate::{check_authorization, routes::JsonResult}; | ||||
|  | ||||
| #[get("/api/guild/<id>")] | ||||
| pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { | ||||
|     offline!(Ok(json!({ "patreon": true, "name": "Guild" }))); | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     match GuildId(id).to_guild_cached(ctx.inner()) { | ||||
|         Some(guild) => { | ||||
|             let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) | ||||
|                 .member(&ctx.inner(), guild.owner_id) | ||||
|                 .await; | ||||
|  | ||||
|             let patreon = member_res.map_or(false, |member| { | ||||
|                 member | ||||
|                     .roles | ||||
|                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) | ||||
|             }); | ||||
|  | ||||
|             Ok(json!({ "patreon": patreon, "name": guild.name })) | ||||
|         } | ||||
|  | ||||
|         None => json_err!("Bot not in guild"), | ||||
|     } | ||||
| } | ||||
							
								
								
									
										373
									
								
								web/src/routes/dashboard/api/guild/reminders.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										373
									
								
								web/src/routes/dashboard/api/guild/reminders.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,373 @@ | ||||
| use rocket::{ | ||||
|     http::CookieJar, | ||||
|     serde::json::{json, Json}, | ||||
|     State, | ||||
| }; | ||||
| use serenity::{ | ||||
|     client::Context, | ||||
|     model::id::{ChannelId, GuildId, UserId}, | ||||
| }; | ||||
| use sqlx::{MySql, Pool}; | ||||
|  | ||||
| use crate::{ | ||||
|     check_authorization, check_guild_subscription, check_subscription, | ||||
|     consts::MIN_INTERVAL, | ||||
|     guards::transaction::Transaction, | ||||
|     routes::{ | ||||
|         dashboard::{ | ||||
|             create_database_channel, create_reminder, DeleteReminder, PatchReminder, Reminder, | ||||
|         }, | ||||
|         JsonResult, | ||||
|     }, | ||||
|     Database, | ||||
| }; | ||||
|  | ||||
| #[post("/api/guild/<id>/reminders", data = "<reminder>")] | ||||
| pub async fn create_guild_reminder( | ||||
|     id: u64, | ||||
|     reminder: Json<Reminder>, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     mut transaction: Transaction<'_>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     let user_id = | ||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||
|  | ||||
|     match create_reminder( | ||||
|         ctx.inner(), | ||||
|         &mut transaction, | ||||
|         GuildId(id), | ||||
|         UserId(user_id), | ||||
|         reminder.into_inner(), | ||||
|     ) | ||||
|     .await | ||||
|     { | ||||
|         Ok(r) => match transaction.commit().await { | ||||
|             Ok(_) => Ok(r), | ||||
|             Err(e) => { | ||||
|                 warn!("Couldn't commit transaction: {:?}", e); | ||||
|                 json_err!("Couldn't commit transaction.") | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         Err(e) => Err(e), | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/api/guild/<id>/reminders")] | ||||
| pub async fn get_reminders( | ||||
|     id: u64, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     let channels_res = GuildId(id).channels(&ctx.inner()).await; | ||||
|  | ||||
|     match channels_res { | ||||
|         Ok(channels) => { | ||||
|             let channels = channels | ||||
|                 .keys() | ||||
|                 .into_iter() | ||||
|                 .map(|k| k.as_u64().to_string()) | ||||
|                 .collect::<Vec<String>>() | ||||
|                 .join(","); | ||||
|  | ||||
|             sqlx::query_as_unchecked!( | ||||
|                 Reminder, | ||||
|                 "SELECT | ||||
|                  reminders.attachment, | ||||
|                  reminders.attachment_name, | ||||
|                  reminders.avatar, | ||||
|                  channels.channel, | ||||
|                  reminders.content, | ||||
|                  reminders.embed_author, | ||||
|                  reminders.embed_author_url, | ||||
|                  reminders.embed_color, | ||||
|                  reminders.embed_description, | ||||
|                  reminders.embed_footer, | ||||
|                  reminders.embed_footer_url, | ||||
|                  reminders.embed_image_url, | ||||
|                  reminders.embed_thumbnail_url, | ||||
|                  reminders.embed_title, | ||||
|                  IFNULL(reminders.embed_fields, '[]') AS embed_fields, | ||||
|                  reminders.enabled, | ||||
|                  reminders.expires, | ||||
|                  reminders.interval_seconds, | ||||
|                  reminders.interval_days, | ||||
|                  reminders.interval_months, | ||||
|                  reminders.name, | ||||
|                  reminders.restartable, | ||||
|                  reminders.tts, | ||||
|                  reminders.uid, | ||||
|                  reminders.username, | ||||
|                  reminders.utc_time | ||||
|                 FROM reminders | ||||
|                 LEFT JOIN channels ON channels.id = reminders.channel_id | ||||
|                 WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)", | ||||
|                 channels | ||||
|             ) | ||||
|             .fetch_all(pool.inner()) | ||||
|             .await | ||||
|             .map(|r| Ok(json!(r))) | ||||
|             .unwrap_or_else(|e| { | ||||
|                 warn!("Failed to complete SQL query: {:?}", e); | ||||
|  | ||||
|                 json_err!("Could not load reminders") | ||||
|             }) | ||||
|         } | ||||
|         Err(e) => { | ||||
|             warn!("Could not fetch channels from {}: {:?}", id, e); | ||||
|  | ||||
|             Ok(json!([])) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[patch("/api/guild/<id>/reminders", data = "<reminder>")] | ||||
| pub async fn edit_reminder( | ||||
|     id: u64, | ||||
|     reminder: Json<PatchReminder>, | ||||
|     ctx: &State<Context>, | ||||
|     mut transaction: Transaction<'_>, | ||||
|     pool: &State<Pool<Database>>, | ||||
|     cookies: &CookieJar<'_>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     let mut error = vec![]; | ||||
|  | ||||
|     let user_id = | ||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||
|  | ||||
|     if reminder.message_ok() { | ||||
|         update_field!(transaction.executor(), error, reminder.[ | ||||
|             content, | ||||
|             embed_author, | ||||
|             embed_description, | ||||
|             embed_footer, | ||||
|             embed_title, | ||||
|             embed_fields, | ||||
|             username | ||||
|         ]); | ||||
|     } else { | ||||
|         error.push("Message exceeds limits.".to_string()); | ||||
|     } | ||||
|  | ||||
|     update_field!(transaction.executor(), error, reminder.[ | ||||
|         attachment, | ||||
|         attachment_name, | ||||
|         avatar, | ||||
|         embed_author_url, | ||||
|         embed_color, | ||||
|         embed_footer_url, | ||||
|         embed_image_url, | ||||
|         embed_thumbnail_url, | ||||
|         enabled, | ||||
|         expires, | ||||
|         name, | ||||
|         restartable, | ||||
|         tts, | ||||
|         utc_time | ||||
|     ]); | ||||
|  | ||||
|     if reminder.interval_days.flatten().is_some() | ||||
|         || reminder.interval_months.flatten().is_some() | ||||
|         || reminder.interval_seconds.flatten().is_some() | ||||
|     { | ||||
|         if check_guild_subscription(&ctx.inner(), id).await | ||||
|             || check_subscription(&ctx.inner(), user_id).await | ||||
|         { | ||||
|             let new_interval_length = match reminder.interval_days { | ||||
|                 Some(interval) => interval.unwrap_or(0), | ||||
|                 None => sqlx::query!( | ||||
|                     "SELECT interval_days AS days FROM reminders WHERE uid = ?", | ||||
|                     reminder.uid | ||||
|                 ) | ||||
|                 .fetch_one(transaction.executor()) | ||||
|                 .await | ||||
|                 .map_err(|e| { | ||||
|                     warn!("Error updating reminder interval: {:?}", e); | ||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) | ||||
|                 })? | ||||
|                 .days | ||||
|                 .unwrap_or(0), | ||||
|             } * 86400 + match reminder.interval_months { | ||||
|                 Some(interval) => interval.unwrap_or(0), | ||||
|                 None => sqlx::query!( | ||||
|                     "SELECT interval_months AS months FROM reminders WHERE uid = ?", | ||||
|                     reminder.uid | ||||
|                 ) | ||||
|                 .fetch_one(transaction.executor()) | ||||
|                 .await | ||||
|                 .map_err(|e| { | ||||
|                     warn!("Error updating reminder interval: {:?}", e); | ||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) | ||||
|                 })? | ||||
|                 .months | ||||
|                 .unwrap_or(0), | ||||
|             } * 2592000 + match reminder.interval_seconds { | ||||
|                 Some(interval) => interval.unwrap_or(0), | ||||
|                 None => sqlx::query!( | ||||
|                     "SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?", | ||||
|                     reminder.uid | ||||
|                 ) | ||||
|                 .fetch_one(transaction.executor()) | ||||
|                 .await | ||||
|                 .map_err(|e| { | ||||
|                     warn!("Error updating reminder interval: {:?}", e); | ||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) | ||||
|                 })? | ||||
|                 .seconds | ||||
|                 .unwrap_or(0), | ||||
|             }; | ||||
|  | ||||
|             if new_interval_length < *MIN_INTERVAL { | ||||
|                 error.push(String::from("New interval is too short.")); | ||||
|             } else { | ||||
|                 update_field!(transaction.executor(), error, reminder.[ | ||||
|                     interval_days, | ||||
|                     interval_months, | ||||
|                     interval_seconds | ||||
|                 ]); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if reminder.channel > 0 { | ||||
|         let channel = ChannelId(reminder.channel).to_channel_cached(&ctx.inner()); | ||||
|         match channel { | ||||
|             Some(channel) => { | ||||
|                 let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id); | ||||
|  | ||||
|                 if !channel_matches_guild { | ||||
|                     warn!( | ||||
|                         "Error in `edit_reminder`: channel {:?} not found for guild {}", | ||||
|                         reminder.channel, id | ||||
|                     ); | ||||
|  | ||||
|                     return Err(json!({"error": "Channel not found"})); | ||||
|                 } | ||||
|  | ||||
|                 let channel = create_database_channel( | ||||
|                     ctx.inner(), | ||||
|                     ChannelId(reminder.channel), | ||||
|                     &mut transaction, | ||||
|                 ) | ||||
|                 .await; | ||||
|  | ||||
|                 if let Err(e) = channel { | ||||
|                     warn!("`create_database_channel` returned an error code: {:?}", e); | ||||
|  | ||||
|                     return Err( | ||||
|                         json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}), | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 let channel = channel.unwrap(); | ||||
|  | ||||
|                 match sqlx::query!( | ||||
|                     "UPDATE reminders SET channel_id = ? WHERE uid = ?", | ||||
|                     channel, | ||||
|                     reminder.uid | ||||
|                 ) | ||||
|                 .execute(transaction.executor()) | ||||
|                 .await | ||||
|                 { | ||||
|                     Ok(_) => {} | ||||
|                     Err(e) => { | ||||
|                         warn!("Error setting channel: {:?}", e); | ||||
|  | ||||
|                         error.push("Couldn't set channel".to_string()) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             None => { | ||||
|                 warn!( | ||||
|                     "Error in `edit_reminder`: channel {:?} not found for guild {}", | ||||
|                     reminder.channel, id | ||||
|                 ); | ||||
|  | ||||
|                 return Err(json!({"error": "Channel not found"})); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if let Err(e) = transaction.commit().await { | ||||
|         warn!("Couldn't commit transaction: {:?}", e); | ||||
|         return json_err!("Couldn't commit transaction"); | ||||
|     } | ||||
|  | ||||
|     match sqlx::query_as_unchecked!( | ||||
|         Reminder, | ||||
|         "SELECT reminders.attachment, | ||||
|          reminders.attachment_name, | ||||
|          reminders.avatar, | ||||
|          channels.channel, | ||||
|          reminders.content, | ||||
|          reminders.embed_author, | ||||
|          reminders.embed_author_url, | ||||
|          reminders.embed_color, | ||||
|          reminders.embed_description, | ||||
|          reminders.embed_footer, | ||||
|          reminders.embed_footer_url, | ||||
|          reminders.embed_image_url, | ||||
|          reminders.embed_thumbnail_url, | ||||
|          reminders.embed_title, | ||||
|          reminders.embed_fields, | ||||
|          reminders.enabled, | ||||
|          reminders.expires, | ||||
|          reminders.interval_seconds, | ||||
|          reminders.interval_days, | ||||
|          reminders.interval_months, | ||||
|          reminders.name, | ||||
|          reminders.restartable, | ||||
|          reminders.tts, | ||||
|          reminders.uid, | ||||
|          reminders.username, | ||||
|          reminders.utc_time | ||||
|         FROM reminders | ||||
|         LEFT JOIN channels ON channels.id = reminders.channel_id | ||||
|         WHERE uid = ?", | ||||
|         reminder.uid | ||||
|     ) | ||||
|     .fetch_one(pool.inner()) | ||||
|     .await | ||||
|     { | ||||
|         Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})), | ||||
|  | ||||
|         Err(e) => { | ||||
|             warn!("Error exiting `edit_reminder': {:?}", e); | ||||
|  | ||||
|             Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]})) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[delete("/api/guild/<id>/reminders", data = "<reminder>")] | ||||
| pub async fn delete_reminder( | ||||
|     cookies: &CookieJar<'_>, | ||||
|     id: u64, | ||||
|     reminder: Json<DeleteReminder>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) | ||||
|         .execute(pool.inner()) | ||||
|         .await | ||||
|     { | ||||
|         Ok(_) => Ok(json!({})), | ||||
|  | ||||
|         Err(e) => { | ||||
|             warn!("Error in `delete_reminder`: {:?}", e); | ||||
|  | ||||
|             Err(json!({"error": "Could not delete reminder"})) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										35
									
								
								web/src/routes/dashboard/api/guild/roles.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/src/routes/dashboard/api/guild/roles.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| use rocket::{http::CookieJar, serde::json::json, State}; | ||||
| use serde::Serialize; | ||||
| use serenity::client::Context; | ||||
|  | ||||
| use crate::{check_authorization, routes::JsonResult}; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| struct RoleInfo { | ||||
|     id: String, | ||||
|     name: String, | ||||
| } | ||||
|  | ||||
| #[get("/api/guild/<id>/roles")] | ||||
| pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { | ||||
|     offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }]))); | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     let roles_res = ctx.cache.guild_roles(id); | ||||
|  | ||||
|     match roles_res { | ||||
|         Some(roles) => { | ||||
|             let roles = roles | ||||
|                 .iter() | ||||
|                 .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() }) | ||||
|                 .collect::<Vec<RoleInfo>>(); | ||||
|  | ||||
|             Ok(json!(roles)) | ||||
|         } | ||||
|         None => { | ||||
|             warn!("Could not fetch roles from {}", id); | ||||
|  | ||||
|             json_err!("Could not get roles") | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										181
									
								
								web/src/routes/dashboard/api/guild/templates.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								web/src/routes/dashboard/api/guild/templates.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| use rocket::{ | ||||
|     http::CookieJar, | ||||
|     serde::json::{json, Json}, | ||||
|     State, | ||||
| }; | ||||
| use serenity::client::Context; | ||||
| use sqlx::{MySql, Pool}; | ||||
|  | ||||
| use crate::{ | ||||
|     check_authorization, | ||||
|     consts::{ | ||||
|         MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, | ||||
|         MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH, | ||||
|         MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, | ||||
|     }, | ||||
|     routes::{ | ||||
|         dashboard::{template_name_default, DeleteReminderTemplate, ReminderTemplate}, | ||||
|         JsonResult, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| #[get("/api/guild/<id>/templates")] | ||||
| pub async fn get_reminder_templates( | ||||
|     id: u64, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     match sqlx::query_as_unchecked!( | ||||
|         ReminderTemplate, | ||||
|         "SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||
|         id | ||||
|     ) | ||||
|     .fetch_all(pool.inner()) | ||||
|     .await | ||||
|     { | ||||
|         Ok(templates) => Ok(json!(templates)), | ||||
|         Err(e) => { | ||||
|             warn!("Could not fetch templates from {}: {:?}", id, e); | ||||
|  | ||||
|             json_err!("Could not get templates") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[post("/api/guild/<id>/templates", data = "<reminder_template>")] | ||||
| pub async fn create_reminder_template( | ||||
|     id: u64, | ||||
|     reminder_template: Json<ReminderTemplate>, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     // validate lengths | ||||
|     check_length!(MAX_CONTENT_LENGTH, reminder_template.content); | ||||
|     check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description); | ||||
|     check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title); | ||||
|     check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author); | ||||
|     check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer); | ||||
|     check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields); | ||||
|     if let Some(fields) = &reminder_template.embed_fields { | ||||
|         for field in &fields.0 { | ||||
|             check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value); | ||||
|             check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title); | ||||
|         } | ||||
|     } | ||||
|     check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username); | ||||
|     check_length_opt!( | ||||
|         MAX_URL_LENGTH, | ||||
|         reminder_template.embed_footer_url, | ||||
|         reminder_template.embed_thumbnail_url, | ||||
|         reminder_template.embed_author_url, | ||||
|         reminder_template.embed_image_url, | ||||
|         reminder_template.avatar | ||||
|     ); | ||||
|  | ||||
|     // validate urls | ||||
|     check_url_opt!( | ||||
|         reminder_template.embed_footer_url, | ||||
|         reminder_template.embed_thumbnail_url, | ||||
|         reminder_template.embed_author_url, | ||||
|         reminder_template.embed_image_url, | ||||
|         reminder_template.avatar | ||||
|     ); | ||||
|  | ||||
|     let name = if reminder_template.name.is_empty() { | ||||
|         template_name_default() | ||||
|     } else { | ||||
|         reminder_template.name.clone() | ||||
|     }; | ||||
|  | ||||
|     match sqlx::query!( | ||||
|         "INSERT INTO reminder_template | ||||
|         (guild_id, | ||||
|          name, | ||||
|          attachment, | ||||
|          attachment_name, | ||||
|          avatar, | ||||
|          content, | ||||
|          embed_author, | ||||
|          embed_author_url, | ||||
|          embed_color, | ||||
|          embed_description, | ||||
|          embed_footer, | ||||
|          embed_footer_url, | ||||
|          embed_image_url, | ||||
|          embed_thumbnail_url, | ||||
|          embed_title, | ||||
|          embed_fields, | ||||
|          interval_seconds, | ||||
|          interval_days, | ||||
|          interval_months, | ||||
|          tts, | ||||
|          username | ||||
|         ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, | ||||
|          ?, ?, ?, ?, ?, ?, ?)", | ||||
|         id, | ||||
|         name, | ||||
|         reminder_template.attachment, | ||||
|         reminder_template.attachment_name, | ||||
|         reminder_template.avatar, | ||||
|         reminder_template.content, | ||||
|         reminder_template.embed_author, | ||||
|         reminder_template.embed_author_url, | ||||
|         reminder_template.embed_color, | ||||
|         reminder_template.embed_description, | ||||
|         reminder_template.embed_footer, | ||||
|         reminder_template.embed_footer_url, | ||||
|         reminder_template.embed_image_url, | ||||
|         reminder_template.embed_thumbnail_url, | ||||
|         reminder_template.embed_title, | ||||
|         reminder_template.embed_fields, | ||||
|         reminder_template.interval_seconds, | ||||
|         reminder_template.interval_days, | ||||
|         reminder_template.interval_months, | ||||
|         reminder_template.tts, | ||||
|         reminder_template.username, | ||||
|     ) | ||||
|     .fetch_all(pool.inner()) | ||||
|     .await | ||||
|     { | ||||
|         Ok(_) => Ok(json!({})), | ||||
|         Err(e) => { | ||||
|             warn!("Could not create template for {}: {:?}", id, e); | ||||
|  | ||||
|             json_err!("Could not create template") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")] | ||||
| pub async fn delete_reminder_template( | ||||
|     id: u64, | ||||
|     delete_reminder_template: Json<DeleteReminderTemplate>, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     match sqlx::query!( | ||||
|         "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?", | ||||
|         id, delete_reminder_template.id | ||||
|     ) | ||||
|     .fetch_all(pool.inner()) | ||||
|     .await | ||||
|     { | ||||
|         Ok(_) => { | ||||
|             Ok(json!({})) | ||||
|         } | ||||
|         Err(e) => { | ||||
|             warn!("Could not delete template from {}: {:?}", id, e); | ||||
|  | ||||
|             json_err!("Could not delete template") | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2
									
								
								web/src/routes/dashboard/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web/src/routes/dashboard/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| pub mod guild; | ||||
| pub mod user; | ||||
							
								
								
									
										81
									
								
								web/src/routes/dashboard/api/user/guilds.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								web/src/routes/dashboard/api/user/guilds.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| use reqwest::Client; | ||||
| use rocket::{ | ||||
|     http::CookieJar, | ||||
|     serde::json::{json, Value as JsonValue}, | ||||
|     State, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serenity::model::{id::GuildId, permissions::Permissions}; | ||||
|  | ||||
| use crate::consts::DISCORD_API; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| struct GuildInfo { | ||||
|     id: String, | ||||
|     name: String, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| struct PartialGuild { | ||||
|     pub id: GuildId, | ||||
|     pub name: String, | ||||
|     #[serde(default)] | ||||
|     pub owner: bool, | ||||
|     #[serde(rename = "permissions_new")] | ||||
|     pub permissions: Option<String>, | ||||
| } | ||||
|  | ||||
| #[get("/api/user/guilds")] | ||||
| pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue { | ||||
|     offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }])); | ||||
|  | ||||
|     if let Some(access_token) = cookies.get_private("access_token") { | ||||
|         let request_res = reqwest_client | ||||
|             .get(format!("{}/users/@me/guilds", DISCORD_API)) | ||||
|             .bearer_auth(access_token.value()) | ||||
|             .send() | ||||
|             .await; | ||||
|  | ||||
|         match request_res { | ||||
|             Ok(response) => { | ||||
|                 let guilds_res = response.json::<Vec<PartialGuild>>().await; | ||||
|  | ||||
|                 match guilds_res { | ||||
|                     Ok(guilds) => { | ||||
|                         let reduced_guilds = guilds | ||||
|                             .iter() | ||||
|                             .filter(|g| { | ||||
|                                 g.owner | ||||
|                                     || g.permissions.as_ref().map_or(false, |p| { | ||||
|                                         let permissions = | ||||
|                                             Permissions::from_bits_truncate(p.parse().unwrap()); | ||||
|  | ||||
|                                         permissions.manage_messages() | ||||
|                                             || permissions.manage_guild() | ||||
|                                             || permissions.administrator() | ||||
|                                     }) | ||||
|                             }) | ||||
|                             .map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() }) | ||||
|                             .collect::<Vec<GuildInfo>>(); | ||||
|  | ||||
|                         json!(reduced_guilds) | ||||
|                     } | ||||
|  | ||||
|                     Err(e) => { | ||||
|                         warn!("Error constructing user from request: {:?}", e); | ||||
|  | ||||
|                         json!({"error": "Could not get user details"}) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Err(e) => { | ||||
|                 warn!("Error getting user guilds: {:?}", e); | ||||
|  | ||||
|                 json!({"error": "Could not reach Discord"}) | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         json!({"error": "Not authorized"}) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										97
									
								
								web/src/routes/dashboard/api/user/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								web/src/routes/dashboard/api/user/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| mod guilds; | ||||
|  | ||||
| use std::env; | ||||
|  | ||||
| use chrono_tz::Tz; | ||||
| pub use guilds::*; | ||||
| use rocket::{ | ||||
|     http::CookieJar, | ||||
|     serde::json::{json, Json, Value as JsonValue}, | ||||
|     State, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serenity::{ | ||||
|     client::Context, | ||||
|     model::id::{GuildId, RoleId}, | ||||
| }; | ||||
| use sqlx::{MySql, Pool}; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| struct UserInfo { | ||||
|     name: String, | ||||
|     patreon: bool, | ||||
|     timezone: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| pub struct UpdateUser { | ||||
|     timezone: String, | ||||
| } | ||||
|  | ||||
| #[get("/api/user")] | ||||
| pub async fn get_user_info( | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonValue { | ||||
|     offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None })); | ||||
|  | ||||
|     if let Some(user_id) = | ||||
|         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() | ||||
|     { | ||||
|         let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) | ||||
|             .member(&ctx.inner(), user_id) | ||||
|             .await; | ||||
|  | ||||
|         let timezone = sqlx::query!( | ||||
|             "SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?", | ||||
|             user_id | ||||
|         ) | ||||
|         .fetch_one(pool.inner()) | ||||
|         .await | ||||
|         .map_or(None, |q| Some(q.timezone)); | ||||
|  | ||||
|         let user_info = UserInfo { | ||||
|             name: cookies | ||||
|                 .get_private("username") | ||||
|                 .map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()), | ||||
|             patreon: member_res.map_or(false, |member| { | ||||
|                 member | ||||
|                     .roles | ||||
|                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) | ||||
|             }), | ||||
|             timezone, | ||||
|         }; | ||||
|  | ||||
|         json!(user_info) | ||||
|     } else { | ||||
|         json!({"error": "Not authorized"}) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[patch("/api/user", data = "<user>")] | ||||
| pub async fn update_user_info( | ||||
|     cookies: &CookieJar<'_>, | ||||
|     user: Json<UpdateUser>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonValue { | ||||
|     if let Some(user_id) = | ||||
|         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() | ||||
|     { | ||||
|         if user.timezone.parse::<Tz>().is_ok() { | ||||
|             let _ = sqlx::query!( | ||||
|                 "UPDATE users SET timezone = ? WHERE user = ?", | ||||
|                 user.timezone, | ||||
|                 user_id, | ||||
|             ) | ||||
|             .execute(pool.inner()) | ||||
|             .await; | ||||
|  | ||||
|             json!({}) | ||||
|         } else { | ||||
|             json!({"error": "Timezone not recognized"}) | ||||
|         } | ||||
|     } else { | ||||
|         json!({"error": "Not authorized"}) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										20
									
								
								web/src/routes/dashboard/api/user/models.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								web/src/routes/dashboard/api/user/models.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| use std::env; | ||||
|  | ||||
| use chrono_tz::Tz; | ||||
| use reqwest::Client; | ||||
| use rocket::{ | ||||
|     http::CookieJar, | ||||
|     serde::json::{json, Json, Value as JsonValue}, | ||||
|     State, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serenity::{ | ||||
|     client::Context, | ||||
|     model::{ | ||||
|         id::{GuildId, RoleId}, | ||||
|         permissions::Permissions, | ||||
|     }, | ||||
| }; | ||||
| use sqlx::{MySql, Pool}; | ||||
|  | ||||
| use crate::{consts::DISCORD_API, routes::JsonResult}; | ||||
							
								
								
									
										29
									
								
								web/src/routes/dashboard/api/user/reminders.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								web/src/routes/dashboard/api/user/reminders.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| use std::env; | ||||
|  | ||||
| use chrono_tz::Tz; | ||||
| use reqwest::Client; | ||||
| use rocket::{ | ||||
|     http::CookieJar, | ||||
|     serde::json::{json, Json, Value as JsonValue}, | ||||
|     State, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serenity::{ | ||||
|     client::Context, | ||||
|     model::{ | ||||
|         id::{GuildId, RoleId}, | ||||
|         permissions::Permissions, | ||||
|     }, | ||||
| }; | ||||
| use sqlx::{MySql, Pool}; | ||||
|  | ||||
| use crate::{consts::DISCORD_API, routes::JsonResult}; | ||||
|  | ||||
| #[get("/api/user/reminders")] | ||||
| pub async fn get_reminders( | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     Ok(json! {}) | ||||
| } | ||||
| @@ -6,16 +6,20 @@ use rocket::{ | ||||
| }; | ||||
| use serenity::{ | ||||
|     client::Context, | ||||
|     model::id::{ChannelId, GuildId}, | ||||
|     model::id::{ChannelId, GuildId, UserId}, | ||||
| }; | ||||
| use sqlx::{MySql, Pool}; | ||||
|  | ||||
| use crate::routes::{ | ||||
| use crate::{ | ||||
|     check_authorization, | ||||
|     guards::transaction::Transaction, | ||||
|     routes::{ | ||||
|         dashboard::{ | ||||
|             create_reminder, generate_uid, ImportBody, Reminder, ReminderCsv, ReminderTemplateCsv, | ||||
|             TodoCsv, | ||||
|         }, | ||||
|         JsonResult, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| #[get("/api/guild/<id>/export/reminders")] | ||||
| @@ -25,7 +29,7 @@ pub async fn export_reminders( | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); | ||||
|  | ||||
| @@ -70,7 +74,7 @@ pub async fn export_reminders( | ||||
|                  reminders.utc_time | ||||
|                 FROM reminders | ||||
|                 LEFT JOIN channels ON channels.id = reminders.channel_id | ||||
|                 WHERE FIND_IN_SET(channels.channel, ?)", | ||||
|                 WHERE FIND_IN_SET(channels.channel, ?) AND status = 'pending'", | ||||
|                 channels | ||||
|             ) | ||||
|             .fetch_all(pool.inner()) | ||||
| @@ -118,14 +122,14 @@ pub async fn export_reminders( | ||||
| } | ||||
|  | ||||
| #[put("/api/guild/<id>/export/reminders", data = "<body>")] | ||||
| pub async fn import_reminders( | ||||
| pub(crate) async fn import_reminders( | ||||
|     id: u64, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     body: Json<ImportBody>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
|     mut transaction: Transaction<'_>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     let user_id = | ||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||
| @@ -133,6 +137,7 @@ pub async fn import_reminders( | ||||
|     match base64::decode(&body.body) { | ||||
|         Ok(body) => { | ||||
|             let mut reader = csv::Reader::from_reader(body.as_slice()); | ||||
|             let mut count = 0; | ||||
|  | ||||
|             for result in reader.deserialize::<ReminderCsv>() { | ||||
|                 match result { | ||||
| @@ -175,12 +180,14 @@ pub async fn import_reminders( | ||||
|  | ||||
|                                 create_reminder( | ||||
|                                     ctx.inner(), | ||||
|                                     pool.inner(), | ||||
|                                     &mut transaction, | ||||
|                                     GuildId(id), | ||||
|                                     UserId(user_id), | ||||
|                                     reminder, | ||||
|                                 ) | ||||
|                                 .await?; | ||||
|  | ||||
|                                 count += 1; | ||||
|                             } | ||||
|  | ||||
|                             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(_) => { | ||||
| @@ -216,7 +232,7 @@ pub async fn export_todos( | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); | ||||
|  | ||||
| @@ -271,7 +287,7 @@ pub async fn import_todos( | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     let channels_res = GuildId(id).channels(&ctx.inner()).await; | ||||
|  | ||||
| @@ -366,7 +382,7 @@ pub async fn export_reminder_templates( | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|     check_authorization(cookies, ctx.inner(), id).await?; | ||||
|  | ||||
|     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); | ||||
|  | ||||
| @@ -388,6 +404,9 @@ pub async fn export_reminder_templates( | ||||
|          embed_thumbnail_url, | ||||
|          embed_title, | ||||
|          embed_fields, | ||||
|          interval_seconds, | ||||
|          interval_days, | ||||
|          interval_months, | ||||
|          tts, | ||||
|          username | ||||
|         FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||
|   | ||||
| @@ -1,625 +1 @@ | ||||
| use std::env; | ||||
|  | ||||
| use rocket::{ | ||||
|     http::CookieJar, | ||||
|     serde::json::{json, Json}, | ||||
|     State, | ||||
| }; | ||||
| use serde::Serialize; | ||||
| use serenity::{ | ||||
|     client::Context, | ||||
|     model::{ | ||||
|         channel::GuildChannel, | ||||
|         id::{ChannelId, GuildId, RoleId}, | ||||
|     }, | ||||
| }; | ||||
| use sqlx::{MySql, Pool}; | ||||
|  | ||||
| use crate::{ | ||||
|     check_guild_subscription, check_subscription, | ||||
|     consts::{ | ||||
|         MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, | ||||
|         MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH, | ||||
|         MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, | ||||
|         MIN_INTERVAL, | ||||
|     }, | ||||
|     routes::{ | ||||
|         dashboard::{ | ||||
|             create_database_channel, create_reminder, template_name_default, DeleteReminder, | ||||
|             DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate, | ||||
|         }, | ||||
|         JsonResult, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| struct ChannelInfo { | ||||
|     id: String, | ||||
|     name: String, | ||||
|     webhook_avatar: Option<String>, | ||||
|     webhook_name: Option<String>, | ||||
| } | ||||
|  | ||||
| #[get("/api/guild/<id>/patreon")] | ||||
| pub async fn get_guild_patreon( | ||||
|     id: u64, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
| ) -> JsonResult { | ||||
|     offline!(Ok(json!({ "patreon": true }))); | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|  | ||||
|     match GuildId(id).to_guild_cached(ctx.inner()) { | ||||
|         Some(guild) => { | ||||
|             let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) | ||||
|                 .member(&ctx.inner(), guild.owner_id) | ||||
|                 .await; | ||||
|  | ||||
|             let patreon = member_res.map_or(false, |member| { | ||||
|                 member | ||||
|                     .roles | ||||
|                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) | ||||
|             }); | ||||
|  | ||||
|             Ok(json!({ "patreon": patreon })) | ||||
|         } | ||||
|  | ||||
|         None => json_err!("Bot not in guild"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/api/guild/<id>/channels")] | ||||
| pub async fn get_guild_channels( | ||||
|     id: u64, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
| ) -> JsonResult { | ||||
|     offline!(Ok(json!(vec![ChannelInfo { | ||||
|         name: "general".to_string(), | ||||
|         id: "1".to_string(), | ||||
|         webhook_avatar: None, | ||||
|         webhook_name: None, | ||||
|     }]))); | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|  | ||||
|     match GuildId(id).to_guild_cached(ctx.inner()) { | ||||
|         Some(guild) => { | ||||
|             let mut channels = guild | ||||
|                 .channels | ||||
|                 .iter() | ||||
|                 .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c))) | ||||
|                 .filter(|(_, channel)| channel.is_text_based()) | ||||
|                 .collect::<Vec<(ChannelId, GuildChannel)>>(); | ||||
|  | ||||
|             channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position)); | ||||
|  | ||||
|             let channel_info = channels | ||||
|                 .iter() | ||||
|                 .map(|(channel_id, channel)| ChannelInfo { | ||||
|                     name: channel.name.to_string(), | ||||
|                     id: channel_id.to_string(), | ||||
|                     webhook_avatar: None, | ||||
|                     webhook_name: None, | ||||
|                 }) | ||||
|                 .collect::<Vec<ChannelInfo>>(); | ||||
|  | ||||
|             Ok(json!(channel_info)) | ||||
|         } | ||||
|  | ||||
|         None => json_err!("Bot not in guild"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| struct RoleInfo { | ||||
|     id: String, | ||||
|     name: String, | ||||
| } | ||||
|  | ||||
| #[get("/api/guild/<id>/roles")] | ||||
| pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { | ||||
|     offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }]))); | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|  | ||||
|     let roles_res = ctx.cache.guild_roles(id); | ||||
|  | ||||
|     match roles_res { | ||||
|         Some(roles) => { | ||||
|             let roles = roles | ||||
|                 .iter() | ||||
|                 .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() }) | ||||
|                 .collect::<Vec<RoleInfo>>(); | ||||
|  | ||||
|             Ok(json!(roles)) | ||||
|         } | ||||
|         None => { | ||||
|             warn!("Could not fetch roles from {}", id); | ||||
|  | ||||
|             json_err!("Could not get roles") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/api/guild/<id>/templates")] | ||||
| pub async fn get_reminder_templates( | ||||
|     id: u64, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|  | ||||
|     match sqlx::query_as_unchecked!( | ||||
|         ReminderTemplate, | ||||
|         "SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||
|         id | ||||
|     ) | ||||
|     .fetch_all(pool.inner()) | ||||
|     .await | ||||
|     { | ||||
|         Ok(templates) => Ok(json!(templates)), | ||||
|         Err(e) => { | ||||
|             warn!("Could not fetch templates from {}: {:?}", id, e); | ||||
|  | ||||
|             json_err!("Could not get templates") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[post("/api/guild/<id>/templates", data = "<reminder_template>")] | ||||
| pub async fn create_reminder_template( | ||||
|     id: u64, | ||||
|     reminder_template: Json<ReminderTemplate>, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|  | ||||
|     // validate lengths | ||||
|     check_length!(MAX_CONTENT_LENGTH, reminder_template.content); | ||||
|     check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description); | ||||
|     check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title); | ||||
|     check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author); | ||||
|     check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer); | ||||
|     check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields); | ||||
|     if let Some(fields) = &reminder_template.embed_fields { | ||||
|         for field in &fields.0 { | ||||
|             check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value); | ||||
|             check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title); | ||||
|         } | ||||
|     } | ||||
|     check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username); | ||||
|     check_length_opt!( | ||||
|         MAX_URL_LENGTH, | ||||
|         reminder_template.embed_footer_url, | ||||
|         reminder_template.embed_thumbnail_url, | ||||
|         reminder_template.embed_author_url, | ||||
|         reminder_template.embed_image_url, | ||||
|         reminder_template.avatar | ||||
|     ); | ||||
|  | ||||
|     // validate urls | ||||
|     check_url_opt!( | ||||
|         reminder_template.embed_footer_url, | ||||
|         reminder_template.embed_thumbnail_url, | ||||
|         reminder_template.embed_author_url, | ||||
|         reminder_template.embed_image_url, | ||||
|         reminder_template.avatar | ||||
|     ); | ||||
|  | ||||
|     let name = if reminder_template.name.is_empty() { | ||||
|         template_name_default() | ||||
|     } else { | ||||
|         reminder_template.name.clone() | ||||
|     }; | ||||
|  | ||||
|     match sqlx::query!( | ||||
|         "INSERT INTO reminder_template | ||||
|         (guild_id, | ||||
|          name, | ||||
|          attachment, | ||||
|          attachment_name, | ||||
|          avatar, | ||||
|          content, | ||||
|          embed_author, | ||||
|          embed_author_url, | ||||
|          embed_color, | ||||
|          embed_description, | ||||
|          embed_footer, | ||||
|          embed_footer_url, | ||||
|          embed_image_url, | ||||
|          embed_thumbnail_url, | ||||
|          embed_title, | ||||
|          embed_fields, | ||||
|          tts, | ||||
|          username | ||||
|         ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | ||||
|         id, name, | ||||
|         reminder_template.attachment, | ||||
|         reminder_template.attachment_name, | ||||
|         reminder_template.avatar, | ||||
|         reminder_template.content, | ||||
|         reminder_template.embed_author, | ||||
|         reminder_template.embed_author_url, | ||||
|         reminder_template.embed_color, | ||||
|         reminder_template.embed_description, | ||||
|         reminder_template.embed_footer, | ||||
|         reminder_template.embed_footer_url, | ||||
|         reminder_template.embed_image_url, | ||||
|         reminder_template.embed_thumbnail_url, | ||||
|         reminder_template.embed_title, | ||||
|         reminder_template.embed_fields, | ||||
|         reminder_template.tts, | ||||
|         reminder_template.username, | ||||
|     ) | ||||
|     .fetch_all(pool.inner()) | ||||
|     .await | ||||
|     { | ||||
|         Ok(_) => { | ||||
|             Ok(json!({})) | ||||
|         } | ||||
|         Err(e) => { | ||||
|             warn!("Could not create template for {}: {:?}", id, e); | ||||
|  | ||||
|             json_err!("Could not create template") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")] | ||||
| pub async fn delete_reminder_template( | ||||
|     id: u64, | ||||
|     delete_reminder_template: Json<DeleteReminderTemplate>, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, ctx.inner(), id); | ||||
|  | ||||
|     match sqlx::query!( | ||||
|         "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?", | ||||
|         id, delete_reminder_template.id | ||||
|     ) | ||||
|     .fetch_all(pool.inner()) | ||||
|     .await | ||||
|     { | ||||
|         Ok(_) => { | ||||
|             Ok(json!({})) | ||||
|         } | ||||
|         Err(e) => { | ||||
|             warn!("Could not delete template from {}: {:?}", id, e); | ||||
|  | ||||
|             json_err!("Could not delete template") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[post("/api/guild/<id>/reminders", data = "<reminder>")] | ||||
| pub async fn create_guild_reminder( | ||||
|     id: u64, | ||||
|     reminder: Json<Reminder>, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     serenity_context: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, serenity_context.inner(), id); | ||||
|  | ||||
|     let user_id = | ||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||
|  | ||||
|     create_reminder( | ||||
|         serenity_context.inner(), | ||||
|         pool.inner(), | ||||
|         GuildId(id), | ||||
|         UserId(user_id), | ||||
|         reminder.into_inner(), | ||||
|     ) | ||||
|     .await | ||||
| } | ||||
|  | ||||
| #[get("/api/guild/<id>/reminders")] | ||||
| pub async fn get_reminders( | ||||
|     id: u64, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     serenity_context: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, serenity_context.inner(), id); | ||||
|  | ||||
|     let channels_res = GuildId(id).channels(&ctx.inner()).await; | ||||
|  | ||||
|     match channels_res { | ||||
|         Ok(channels) => { | ||||
|             let channels = channels | ||||
|                 .keys() | ||||
|                 .into_iter() | ||||
|                 .map(|k| k.as_u64().to_string()) | ||||
|                 .collect::<Vec<String>>() | ||||
|                 .join(","); | ||||
|  | ||||
|             sqlx::query_as_unchecked!( | ||||
|                 Reminder, | ||||
|                 "SELECT | ||||
|                  reminders.attachment, | ||||
|                  reminders.attachment_name, | ||||
|                  reminders.avatar, | ||||
|                  channels.channel, | ||||
|                  reminders.content, | ||||
|                  reminders.embed_author, | ||||
|                  reminders.embed_author_url, | ||||
|                  reminders.embed_color, | ||||
|                  reminders.embed_description, | ||||
|                  reminders.embed_footer, | ||||
|                  reminders.embed_footer_url, | ||||
|                  reminders.embed_image_url, | ||||
|                  reminders.embed_thumbnail_url, | ||||
|                  reminders.embed_title, | ||||
|                  IFNULL(reminders.embed_fields, '[]') AS embed_fields, | ||||
|                  reminders.enabled, | ||||
|                  reminders.expires, | ||||
|                  reminders.interval_seconds, | ||||
|                  reminders.interval_days, | ||||
|                  reminders.interval_months, | ||||
|                  reminders.name, | ||||
|                  reminders.restartable, | ||||
|                  reminders.tts, | ||||
|                  reminders.uid, | ||||
|                  reminders.username, | ||||
|                  reminders.utc_time | ||||
|                 FROM reminders | ||||
|                 LEFT JOIN channels ON channels.id = reminders.channel_id | ||||
|                 WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)", | ||||
|                 channels | ||||
|             ) | ||||
|             .fetch_all(pool.inner()) | ||||
|             .await | ||||
|             .map(|r| Ok(json!(r))) | ||||
|             .unwrap_or_else(|e| { | ||||
|                 warn!("Failed to complete SQL query: {:?}", e); | ||||
|  | ||||
|                 json_err!("Could not load reminders") | ||||
|             }) | ||||
|         } | ||||
|         Err(e) => { | ||||
|             warn!("Could not fetch channels from {}: {:?}", id, e); | ||||
|  | ||||
|             Ok(json!([])) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[patch("/api/guild/<id>/reminders", data = "<reminder>")] | ||||
| pub async fn edit_reminder( | ||||
|     id: u64, | ||||
|     reminder: Json<PatchReminder>, | ||||
|     serenity_context: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
|     cookies: &CookieJar<'_>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, serenity_context.inner(), id); | ||||
|  | ||||
|     let mut error = vec![]; | ||||
|  | ||||
|     let user_id = | ||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||
|  | ||||
|     if reminder.message_ok() { | ||||
|         update_field!(pool.inner(), error, reminder.[ | ||||
|             content, | ||||
|             embed_author, | ||||
|             embed_description, | ||||
|             embed_footer, | ||||
|             embed_title, | ||||
|             embed_fields, | ||||
|             username | ||||
|         ]); | ||||
|     } else { | ||||
|         error.push("Message exceeds limits.".to_string()); | ||||
|     } | ||||
|  | ||||
|     update_field!(pool.inner(), error, reminder.[ | ||||
|         attachment, | ||||
|         attachment_name, | ||||
|         avatar, | ||||
|         embed_author_url, | ||||
|         embed_color, | ||||
|         embed_footer_url, | ||||
|         embed_image_url, | ||||
|         embed_thumbnail_url, | ||||
|         enabled, | ||||
|         expires, | ||||
|         name, | ||||
|         restartable, | ||||
|         tts, | ||||
|         utc_time | ||||
|     ]); | ||||
|  | ||||
|     if reminder.interval_days.flatten().is_some() | ||||
|         || reminder.interval_months.flatten().is_some() | ||||
|         || reminder.interval_seconds.flatten().is_some() | ||||
|     { | ||||
|         if check_guild_subscription(&serenity_context.inner(), id).await | ||||
|             || check_subscription(&serenity_context.inner(), user_id).await | ||||
|         { | ||||
|             let new_interval_length = match reminder.interval_days { | ||||
|                 Some(interval) => interval.unwrap_or(0), | ||||
|                 None => sqlx::query!( | ||||
|                     "SELECT interval_days AS days FROM reminders WHERE uid = ?", | ||||
|                     reminder.uid | ||||
|                 ) | ||||
|                 .fetch_one(pool.inner()) | ||||
|                 .await | ||||
|                 .map_err(|e| { | ||||
|                     warn!("Error updating reminder interval: {:?}", e); | ||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) | ||||
|                 })? | ||||
|                 .days | ||||
|                 .unwrap_or(0), | ||||
|             } * 86400 + match reminder.interval_months { | ||||
|                 Some(interval) => interval.unwrap_or(0), | ||||
|                 None => sqlx::query!( | ||||
|                     "SELECT interval_months AS months FROM reminders WHERE uid = ?", | ||||
|                     reminder.uid | ||||
|                 ) | ||||
|                 .fetch_one(pool.inner()) | ||||
|                 .await | ||||
|                 .map_err(|e| { | ||||
|                     warn!("Error updating reminder interval: {:?}", e); | ||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) | ||||
|                 })? | ||||
|                 .months | ||||
|                 .unwrap_or(0), | ||||
|             } * 2592000 + match reminder.interval_seconds { | ||||
|                 Some(interval) => interval.unwrap_or(0), | ||||
|                 None => sqlx::query!( | ||||
|                     "SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?", | ||||
|                     reminder.uid | ||||
|                 ) | ||||
|                 .fetch_one(pool.inner()) | ||||
|                 .await | ||||
|                 .map_err(|e| { | ||||
|                     warn!("Error updating reminder interval: {:?}", e); | ||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) | ||||
|                 })? | ||||
|                 .seconds | ||||
|                 .unwrap_or(0), | ||||
|             }; | ||||
|  | ||||
|             if new_interval_length < *MIN_INTERVAL { | ||||
|                 error.push(String::from("New interval is too short.")); | ||||
|             } else { | ||||
|                 update_field!(pool.inner(), error, reminder.[ | ||||
|                     interval_days, | ||||
|                     interval_months, | ||||
|                     interval_seconds | ||||
|                 ]); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if reminder.channel > 0 { | ||||
|         let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner()); | ||||
|         match channel { | ||||
|             Some(channel) => { | ||||
|                 let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id); | ||||
|  | ||||
|                 if !channel_matches_guild { | ||||
|                     warn!( | ||||
|                         "Error in `edit_reminder`: channel {:?} not found for guild {}", | ||||
|                         reminder.channel, id | ||||
|                     ); | ||||
|  | ||||
|                     return Err(json!({"error": "Channel not found"})); | ||||
|                 } | ||||
|  | ||||
|                 let channel = create_database_channel( | ||||
|                     serenity_context.inner(), | ||||
|                     ChannelId(reminder.channel), | ||||
|                     pool.inner(), | ||||
|                 ) | ||||
|                 .await; | ||||
|  | ||||
|                 if let Err(e) = channel { | ||||
|                     warn!("`create_database_channel` returned an error code: {:?}", e); | ||||
|  | ||||
|                     return Err( | ||||
|                         json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}), | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 let channel = channel.unwrap(); | ||||
|  | ||||
|                 match sqlx::query!( | ||||
|                     "UPDATE reminders SET channel_id = ? WHERE uid = ?", | ||||
|                     channel, | ||||
|                     reminder.uid | ||||
|                 ) | ||||
|                 .execute(pool.inner()) | ||||
|                 .await | ||||
|                 { | ||||
|                     Ok(_) => {} | ||||
|                     Err(e) => { | ||||
|                         warn!("Error setting channel: {:?}", e); | ||||
|  | ||||
|                         error.push("Couldn't set channel".to_string()) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             None => { | ||||
|                 warn!( | ||||
|                     "Error in `edit_reminder`: channel {:?} not found for guild {}", | ||||
|                     reminder.channel, id | ||||
|                 ); | ||||
|  | ||||
|                 return Err(json!({"error": "Channel not found"})); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     match sqlx::query_as_unchecked!( | ||||
|         Reminder, | ||||
|         "SELECT reminders.attachment, | ||||
|          reminders.attachment_name, | ||||
|          reminders.avatar, | ||||
|          channels.channel, | ||||
|          reminders.content, | ||||
|          reminders.embed_author, | ||||
|          reminders.embed_author_url, | ||||
|          reminders.embed_color, | ||||
|          reminders.embed_description, | ||||
|          reminders.embed_footer, | ||||
|          reminders.embed_footer_url, | ||||
|          reminders.embed_image_url, | ||||
|          reminders.embed_thumbnail_url, | ||||
|          reminders.embed_title, | ||||
|          reminders.embed_fields, | ||||
|          reminders.enabled, | ||||
|          reminders.expires, | ||||
|          reminders.interval_seconds, | ||||
|          reminders.interval_days, | ||||
|          reminders.interval_months, | ||||
|          reminders.name, | ||||
|          reminders.restartable, | ||||
|          reminders.tts, | ||||
|          reminders.uid, | ||||
|          reminders.username, | ||||
|          reminders.utc_time | ||||
|         FROM reminders | ||||
|         LEFT JOIN channels ON channels.id = reminders.channel_id | ||||
|         WHERE uid = ?", | ||||
|         reminder.uid | ||||
|     ) | ||||
|     .fetch_one(pool.inner()) | ||||
|     .await | ||||
|     { | ||||
|         Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})), | ||||
|  | ||||
|         Err(e) => { | ||||
|             warn!("Error exiting `edit_reminder': {:?}", e); | ||||
|  | ||||
|             Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]})) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[delete("/api/guild/<_>/reminders", data = "<reminder>")] | ||||
| pub async fn delete_reminder( | ||||
|     reminder: Json<DeleteReminder>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) | ||||
|         .execute(pool.inner()) | ||||
|         .await | ||||
|     { | ||||
|         Ok(_) => Ok(json!({})), | ||||
|  | ||||
|         Err(e) => { | ||||
|             warn!("Error in `delete_reminder`: {:?}", e); | ||||
|  | ||||
|             Err(json!({"error": "Could not delete reminder"})) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -10,7 +10,7 @@ use serenity::{ | ||||
|     http::Http, | ||||
|     model::id::{ChannelId, GuildId, UserId}, | ||||
| }; | ||||
| use sqlx::{types::Json, Executor}; | ||||
| use sqlx::types::Json; | ||||
|  | ||||
| use crate::{ | ||||
|     check_guild_subscription, check_subscription, | ||||
| @@ -20,13 +20,14 @@ use crate::{ | ||||
|         MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, | ||||
|         MAX_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL, | ||||
|     }, | ||||
|     guards::transaction::Transaction, | ||||
|     routes::JsonResult, | ||||
|     Database, Error, | ||||
|     Error, | ||||
| }; | ||||
|  | ||||
| pub mod api; | ||||
| pub mod export; | ||||
| pub mod guild; | ||||
| pub mod user; | ||||
|  | ||||
| type Unset<T> = Option<T>; | ||||
|  | ||||
| @@ -80,6 +81,9 @@ pub struct ReminderTemplate { | ||||
|     embed_thumbnail_url: Option<String>, | ||||
|     embed_title: String, | ||||
|     embed_fields: Option<Json<Vec<EmbedField>>>, | ||||
|     interval_seconds: Option<u32>, | ||||
|     interval_days: Option<u32>, | ||||
|     interval_months: Option<u32>, | ||||
|     tts: bool, | ||||
|     username: Option<String>, | ||||
| } | ||||
| @@ -102,6 +106,9 @@ pub struct ReminderTemplateCsv { | ||||
|     embed_thumbnail_url: Option<String>, | ||||
|     embed_title: String, | ||||
|     embed_fields: Option<String>, | ||||
|     interval_seconds: Option<u32>, | ||||
|     interval_days: Option<u32>, | ||||
|     interval_months: Option<u32>, | ||||
|     tts: bool, | ||||
|     username: Option<String>, | ||||
| } | ||||
| @@ -347,21 +354,21 @@ pub struct TodoCsv { | ||||
|     channel_id: Option<String>, | ||||
| } | ||||
|  | ||||
| pub async fn create_reminder( | ||||
| pub(crate) async fn create_reminder( | ||||
|     ctx: &Context, | ||||
|     pool: impl sqlx::Executor<'_, Database = Database> + Copy, | ||||
|     transaction: &mut Transaction<'_>, | ||||
|     guild_id: GuildId, | ||||
|     user_id: UserId, | ||||
|     reminder: Reminder, | ||||
| ) -> JsonResult { | ||||
|     // check guild in db | ||||
|     match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0) | ||||
|         .fetch_one(pool) | ||||
|         .fetch_one(transaction.executor()) | ||||
|         .await | ||||
|     { | ||||
|         Err(sqlx::Error::RowNotFound) => { | ||||
|             if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.0) | ||||
|                 .execute(pool) | ||||
|                 .execute(transaction.executor()) | ||||
|                 .await | ||||
|                 .is_err() | ||||
|             { | ||||
| @@ -387,7 +394,7 @@ pub async fn create_reminder( | ||||
|         return Err(json!({"error": "Channel not found"})); | ||||
|     } | ||||
|  | ||||
|     let channel = create_database_channel(&ctx, ChannelId(reminder.channel), pool).await; | ||||
|     let channel = create_database_channel(&ctx, ChannelId(reminder.channel), transaction).await; | ||||
|  | ||||
|     if let Err(e) = channel { | ||||
|         warn!("`create_database_channel` returned an error code: {:?}", e); | ||||
| @@ -461,8 +468,6 @@ pub async fn create_reminder( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // base64 decode error dropped here | ||||
|     let attachment_data = reminder.attachment.as_ref().map(|s| base64::decode(s).ok()).flatten(); | ||||
|     let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() }; | ||||
|     let username = if reminder.username.as_ref().map(|s| s.is_empty()).unwrap_or(true) { | ||||
|         None | ||||
| @@ -503,7 +508,7 @@ pub async fn create_reminder( | ||||
|          `utc_time` | ||||
|         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | ||||
|         new_uid, | ||||
|         attachment_data, | ||||
|         reminder.attachment, | ||||
|         reminder.attachment_name, | ||||
|         channel, | ||||
|         reminder.avatar, | ||||
| @@ -529,7 +534,7 @@ pub async fn create_reminder( | ||||
|         username, | ||||
|         reminder.utc_time, | ||||
|     ) | ||||
|     .execute(pool) | ||||
|     .execute(transaction.executor()) | ||||
|     .await | ||||
|     { | ||||
|         Ok(_) => sqlx::query_as_unchecked!( | ||||
| @@ -566,7 +571,7 @@ pub async fn create_reminder( | ||||
|             WHERE uid = ?", | ||||
|             new_uid | ||||
|         ) | ||||
|         .fetch_one(pool) | ||||
|         .fetch_one(transaction.executor()) | ||||
|         .await | ||||
|         .map(|r| Ok(json!(r))) | ||||
|         .unwrap_or_else(|e| { | ||||
| @@ -586,11 +591,11 @@ pub async fn create_reminder( | ||||
| async fn create_database_channel( | ||||
|     ctx: impl AsRef<Http>, | ||||
|     channel: ChannelId, | ||||
|     pool: impl Executor<'_, Database = Database> + Copy, | ||||
|     transaction: &mut Transaction<'_>, | ||||
| ) -> Result<u32, crate::Error> { | ||||
|     let row = | ||||
|         sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0) | ||||
|             .fetch_one(pool) | ||||
|             .fetch_one(transaction.executor()) | ||||
|             .await; | ||||
|  | ||||
|     match row { | ||||
| @@ -607,7 +612,7 @@ async fn create_database_channel( | ||||
|                     webhook.token, | ||||
|                     channel.0 | ||||
|                 ) | ||||
|                 .execute(pool) | ||||
|                 .execute(transaction.executor()) | ||||
|                 .await | ||||
|                 .map_err(|e| Error::SQLx(e))?; | ||||
|             } | ||||
| @@ -633,7 +638,7 @@ async fn create_database_channel( | ||||
|                 webhook.token, | ||||
|                 channel.0 | ||||
|             ) | ||||
|             .execute(pool) | ||||
|             .execute(transaction.executor()) | ||||
|             .await | ||||
|             .map_err(|e| Error::SQLx(e))?; | ||||
|  | ||||
| @@ -644,7 +649,7 @@ async fn create_database_channel( | ||||
|     }?; | ||||
|  | ||||
|     let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.0) | ||||
|         .fetch_one(pool) | ||||
|         .fetch_one(transaction.executor()) | ||||
|         .await | ||||
|         .map_err(|e| Error::SQLx(e))?; | ||||
|  | ||||
| @@ -654,17 +659,19 @@ async fn create_database_channel( | ||||
| #[get("/")] | ||||
| pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | ||||
|     if cookies.get_private("userid").is_some() { | ||||
|         let map: HashMap<&str, String> = HashMap::new(); | ||||
|         let mut map = HashMap::new(); | ||||
|         map.insert("version", env!("CARGO_PKG_VERSION")); | ||||
|         Ok(Template::render("dashboard", &map)) | ||||
|     } else { | ||||
|         Err(Redirect::to("/login/discord")) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/<_>")] | ||||
| #[get("/<_..>")] | ||||
| pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | ||||
|     if cookies.get_private("userid").is_some() { | ||||
|         let map: HashMap<&str, String> = HashMap::new(); | ||||
|         let mut map = HashMap::new(); | ||||
|         map.insert("version", env!("CARGO_PKG_VERSION")); | ||||
|         Ok(Template::render("dashboard", &map)) | ||||
|     } else { | ||||
|         Err(Redirect::to("/login/discord")) | ||||
|   | ||||
| @@ -1,172 +0,0 @@ | ||||
| use std::env; | ||||
|  | ||||
| use chrono_tz::Tz; | ||||
| use reqwest::Client; | ||||
| use rocket::{ | ||||
|     http::CookieJar, | ||||
|     serde::json::{json, Json, Value as JsonValue}, | ||||
|     State, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serenity::{ | ||||
|     client::Context, | ||||
|     model::{ | ||||
|         id::{GuildId, RoleId}, | ||||
|         permissions::Permissions, | ||||
|     }, | ||||
| }; | ||||
| use sqlx::{MySql, Pool}; | ||||
|  | ||||
| use crate::consts::DISCORD_API; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| struct UserInfo { | ||||
|     name: String, | ||||
|     patreon: bool, | ||||
|     timezone: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| pub struct UpdateUser { | ||||
|     timezone: String, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| struct GuildInfo { | ||||
|     id: String, | ||||
|     name: String, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| pub struct PartialGuild { | ||||
|     pub id: GuildId, | ||||
|     pub icon: Option<String>, | ||||
|     pub name: String, | ||||
|     #[serde(default)] | ||||
|     pub owner: bool, | ||||
|     #[serde(rename = "permissions_new")] | ||||
|     pub permissions: Option<String>, | ||||
| } | ||||
|  | ||||
| #[get("/api/user")] | ||||
| pub async fn get_user_info( | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonValue { | ||||
|     offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None })); | ||||
|  | ||||
|     if let Some(user_id) = | ||||
|         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() | ||||
|     { | ||||
|         let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) | ||||
|             .member(&ctx.inner(), user_id) | ||||
|             .await; | ||||
|  | ||||
|         let timezone = sqlx::query!( | ||||
|             "SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?", | ||||
|             user_id | ||||
|         ) | ||||
|         .fetch_one(pool.inner()) | ||||
|         .await | ||||
|         .map_or(None, |q| Some(q.timezone)); | ||||
|  | ||||
|         let user_info = UserInfo { | ||||
|             name: cookies | ||||
|                 .get_private("username") | ||||
|                 .map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()), | ||||
|             patreon: member_res.map_or(false, |member| { | ||||
|                 member | ||||
|                     .roles | ||||
|                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) | ||||
|             }), | ||||
|             timezone, | ||||
|         }; | ||||
|  | ||||
|         json!(user_info) | ||||
|     } else { | ||||
|         json!({"error": "Not authorized"}) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[patch("/api/user", data = "<user>")] | ||||
| pub async fn update_user_info( | ||||
|     cookies: &CookieJar<'_>, | ||||
|     user: Json<UpdateUser>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonValue { | ||||
|     if let Some(user_id) = | ||||
|         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() | ||||
|     { | ||||
|         if user.timezone.parse::<Tz>().is_ok() { | ||||
|             let _ = sqlx::query!( | ||||
|                 "UPDATE users SET timezone = ? WHERE user = ?", | ||||
|                 user.timezone, | ||||
|                 user_id, | ||||
|             ) | ||||
|             .execute(pool.inner()) | ||||
|             .await; | ||||
|  | ||||
|             json!({}) | ||||
|         } else { | ||||
|             json!({"error": "Timezone not recognized"}) | ||||
|         } | ||||
|     } else { | ||||
|         json!({"error": "Not authorized"}) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/api/user/guilds")] | ||||
| pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue { | ||||
|     offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }])); | ||||
|  | ||||
|     if let Some(access_token) = cookies.get_private("access_token") { | ||||
|         let request_res = reqwest_client | ||||
|             .get(format!("{}/users/@me/guilds", DISCORD_API)) | ||||
|             .bearer_auth(access_token.value()) | ||||
|             .send() | ||||
|             .await; | ||||
|  | ||||
|         match request_res { | ||||
|             Ok(response) => { | ||||
|                 let guilds_res = response.json::<Vec<PartialGuild>>().await; | ||||
|  | ||||
|                 match guilds_res { | ||||
|                     Ok(guilds) => { | ||||
|                         let reduced_guilds = guilds | ||||
|                             .iter() | ||||
|                             .filter(|g| { | ||||
|                                 g.owner | ||||
|                                     || g.permissions.as_ref().map_or(false, |p| { | ||||
|                                         let permissions = | ||||
|                                             Permissions::from_bits_truncate(p.parse().unwrap()); | ||||
|  | ||||
|                                         permissions.manage_messages() | ||||
|                                             || permissions.manage_guild() | ||||
|                                             || permissions.administrator() | ||||
|                                     }) | ||||
|                             }) | ||||
|                             .map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() }) | ||||
|                             .collect::<Vec<GuildInfo>>(); | ||||
|  | ||||
|                         json!(reduced_guilds) | ||||
|                     } | ||||
|  | ||||
|                     Err(e) => { | ||||
|                         warn!("Error constructing user from request: {:?}", e); | ||||
|  | ||||
|                         json!({"error": "Could not get user details"}) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Err(e) => { | ||||
|                 warn!("Error getting user guilds: {:?}", e); | ||||
|  | ||||
|                 json!({"error": "Could not reach Discord"}) | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         json!({"error": "Not authorized"}) | ||||
|     } | ||||
| } | ||||
| @@ -15,6 +15,22 @@ div.reminderContent.is-collapsed .column.settings { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| div.reminderContent.is-collapsed .reminder-settings { | ||||
|     margin-bottom: 0; | ||||
| } | ||||
|  | ||||
| div.reminderContent.is-collapsed .button-row { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| div.reminderContent.is-collapsed .button-row-edit { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| div.reminderContent.is-collapsed .reminder-topbar { | ||||
|     padding-bottom: 0; | ||||
| } | ||||
|  | ||||
| div.reminderContent.is-collapsed .invert-collapses { | ||||
|     display: inline-flex; | ||||
| } | ||||
| @@ -129,6 +145,12 @@ div.split-controls { | ||||
|     margin-top: 0 !important; | ||||
| } | ||||
|  | ||||
| .reminder-settings > .column { | ||||
|     flex-grow: 0; | ||||
|     flex-shrink: 0; | ||||
|     flex-basis: 50%; | ||||
| } | ||||
|  | ||||
| div.reminderContent { | ||||
|     margin-top: 10px; | ||||
|     margin-bottom: 10px; | ||||
| @@ -286,17 +308,24 @@ div.dashboard-sidebar { | ||||
|     padding-right: 0; | ||||
| } | ||||
|  | ||||
| div.dashboard-sidebar:not(.mobile-sidebar) { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
| ul.guildList { | ||||
|     flex-grow: 1; | ||||
|     flex-shrink: 1; | ||||
|     overflow: auto; | ||||
| } | ||||
|  | ||||
| div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | ||||
|     flex-shrink: 0; | ||||
|     flex-grow: 0; | ||||
|     position: fixed; | ||||
|     bottom: 0; | ||||
|     width: 226px; | ||||
| } | ||||
|  | ||||
| div.dashboard-sidebar svg { | ||||
|     flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| div.mobile-sidebar { | ||||
|     z-index: 100; | ||||
|     min-height: 100vh; | ||||
| @@ -444,8 +473,7 @@ input.default-width { | ||||
| .customizable.is-400x300 img { | ||||
|     margin-top: 10px; | ||||
|     width: 100%; | ||||
|     min-height: 100px; | ||||
|     max-height: 400px; | ||||
|     height: 100px; | ||||
| } | ||||
|  | ||||
| .customizable.is-32x32 img { | ||||
| @@ -589,6 +617,14 @@ input.default-width { | ||||
|     border-bottom: 1px solid #fff; | ||||
| } | ||||
|  | ||||
| .channel-selector { | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| .select { | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| li.highlight { | ||||
|     margin-bottom: 0 !important; | ||||
| } | ||||
| @@ -612,7 +648,22 @@ li.highlight { | ||||
|     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 { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
| @@ -630,37 +681,13 @@ li.highlight { | ||||
|     .button-row button { | ||||
|         width: 100%; | ||||
|     } | ||||
|  | ||||
|     .reminder-settings { | ||||
|         margin-bottom: 0 !important; | ||||
|     } | ||||
|  | ||||
| @media only screen and (max-width: 768px) { | ||||
|     .button-row-edit { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|     } | ||||
|  | ||||
|     .button-row-edit > button { | ||||
|         width: 100%; | ||||
|         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; | ||||
|     .tts-row { | ||||
|         padding-bottom: 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -679,6 +706,86 @@ li.highlight { | ||||
|  | ||||
| /* 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 */ | ||||
|  | ||||
| .half-rem { | ||||
| @@ -716,6 +823,18 @@ a.switch-pane { | ||||
|     text-overflow: ellipsis; | ||||
| } | ||||
|  | ||||
| .guild-submenu { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| .guild-submenu li { | ||||
|     font-size: 0.8rem; | ||||
| } | ||||
|  | ||||
| a.switch-pane.is-active ~ .guild-submenu { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| .feedback { | ||||
|     background-color: #5865F2; | ||||
| } | ||||
|   | ||||
| @@ -33,7 +33,16 @@ let globalPatreon = false; | ||||
| let guildPatreon = false; | ||||
|  | ||||
| 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) { | ||||
| @@ -96,7 +105,7 @@ function reset_guild_pane() { | ||||
| } | ||||
|  | ||||
| async function fetch_patreon(guild_id) { | ||||
|     fetch(`/dashboard/api/guild/${guild_id}/patreon`) | ||||
|     fetch(`/dashboard/api/guild/${guild_id}`) | ||||
|         .then((response) => response.json()) | ||||
|         .then((data) => { | ||||
|             if (data.error) { | ||||
| @@ -223,11 +232,10 @@ async function fetch_reminders(guild_id) { | ||||
| } | ||||
|  | ||||
| async function serialize_reminder(node, mode) { | ||||
|     let interval, utc_time, expiration_time; | ||||
|     let utc_time, expiration_time; | ||||
|     let interval = get_interval(node); | ||||
|  | ||||
|     if (mode !== "template") { | ||||
|         interval = get_interval(node); | ||||
|  | ||||
|         utc_time = luxon.DateTime.fromISO( | ||||
|             node.querySelector('input[name="time"]').value | ||||
|         ).setZone("UTC"); | ||||
| @@ -356,9 +364,9 @@ async function serialize_reminder(node, mode) { | ||||
|         embed_title: embed_title, | ||||
|         embed_fields: fields, | ||||
|         expires: expiration_time, | ||||
|         interval_seconds: mode !== "template" ? interval.seconds : null, | ||||
|         interval_days: mode !== "template" ? interval.days : null, | ||||
|         interval_months: mode !== "template" ? interval.months : null, | ||||
|         interval_seconds: interval.seconds, | ||||
|         interval_days: interval.days, | ||||
|         interval_months: interval.months, | ||||
|         name: node.querySelector('input[name="name"]').value, | ||||
|         tts: node.querySelector('input[name="tts"]').checked, | ||||
|         username: node.querySelector('input[name="username"]').value, | ||||
| @@ -420,9 +428,9 @@ function deserialize_reminder(reminder, frame, mode) { | ||||
|             .insertBefore(embed_field, lastChild); | ||||
|     } | ||||
|  | ||||
|     if (mode !== "template") { | ||||
|     if (reminder["interval_seconds"]) update_interval(frame); | ||||
|  | ||||
|     if (mode !== "template") { | ||||
|         let $enableBtn = frame.querySelector(".disable-enable"); | ||||
|         $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable"; | ||||
|  | ||||
| @@ -455,15 +463,17 @@ document.addEventListener("guildSwitched", async (e) => { | ||||
|  | ||||
|     let hasError = false; | ||||
|  | ||||
|     if ($anchor === null) { | ||||
|         switch_pane("user-error"); | ||||
|         hasError = true; | ||||
|         return; | ||||
|     if (pane() === null) { | ||||
|         window.history.replaceState({}, "", `/dashboard/${guildId()}/reminders`); | ||||
|     } | ||||
|  | ||||
|     switch_pane($anchor.dataset["pane"]); | ||||
|     reset_guild_pane(); | ||||
|     switch_pane(pane()); | ||||
|  | ||||
|     if ($anchor !== null) { | ||||
|         $anchor.classList.add("is-active"); | ||||
|     } | ||||
|  | ||||
|     reset_guild_pane(); | ||||
|  | ||||
|     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { | ||||
|         document | ||||
| @@ -604,6 +614,16 @@ function show_error(error) { | ||||
|     }, 5000); | ||||
| } | ||||
|  | ||||
| function show_success(error) { | ||||
|     document.getElementById("success").querySelector("span.success-message").textContent = | ||||
|         error; | ||||
|     document.getElementById("success").classList.add("is-active"); | ||||
|  | ||||
|     window.setTimeout(() => { | ||||
|         document.getElementById("success").classList.remove("is-active"); | ||||
|     }, 5000); | ||||
| } | ||||
|  | ||||
| $colorPickerInput.value = colorPicker.color.hexString; | ||||
|  | ||||
| $colorPickerInput.addEventListener("input", () => { | ||||
| @@ -686,11 +706,15 @@ document.addEventListener("DOMContentLoaded", async () => { | ||||
|                         ); | ||||
|                         $anchor.dataset["guild"] = guild.id; | ||||
|                         $anchor.dataset["name"] = guild.name; | ||||
|                         $anchor.href = `/dashboard/${guild.id}?name=${guild.name}`; | ||||
|                         $anchor.href = `/dashboard/${guild.id}/reminders`; | ||||
|  | ||||
|                         $anchor.addEventListener("click", async (e) => { | ||||
|                             e.preventDefault(); | ||||
|                             window.history.pushState({}, "", `/dashboard/${guild.id}`); | ||||
|                             window.history.pushState( | ||||
|                                 {}, | ||||
|                                 "", | ||||
|                                 `/dashboard/${guild.id}/reminders` | ||||
|                             ); | ||||
|                             const event = new CustomEvent("guildSwitched", { | ||||
|                                 detail: { | ||||
|                                     guild_name: guild.name, | ||||
| @@ -754,11 +778,25 @@ $uploader.addEventListener("change", (ev) => { | ||||
|         fileReader.onload = (e) => resolve(fileReader.result); | ||||
|         fileReader.readAsDataURL($uploader.files[0]); | ||||
|     }).then((dataUrl) => { | ||||
|         $importBtn.setAttribute("disabled", true); | ||||
|  | ||||
|         fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, { | ||||
|             method: "PUT", | ||||
|             body: JSON.stringify({ body: dataUrl.split(",")[1] }), | ||||
|         }).then(() => { | ||||
|         }) | ||||
|             .then((response) => response.json()) | ||||
|             .then((data) => { | ||||
|                 $importBtn.removeAttribute("disabled"); | ||||
|  | ||||
|                 if (data.error) { | ||||
|                     show_error(data.error); | ||||
|                 } else { | ||||
|                     show_success(data.message); | ||||
|                 } | ||||
|             }) | ||||
|             .then(() => { | ||||
|                 delete $uploader.files[0]; | ||||
|                 fetch_reminders(guild); | ||||
|             }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -1,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/fa.css"> | ||||
|     <link rel="stylesheet" href="/static/css/font.css"> | ||||
|     <link rel="stylesheet" href="/static/css/style.css"> | ||||
|     <link rel="stylesheet" href="/static/css/style.css?v{{ version }}"> | ||||
|     <link rel="stylesheet" href="/static/css/dtsel.css"> | ||||
|     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tributejs/5.1.3/tribute.css" integrity="sha512-GnwBnXd+ZGO9CdP343MUr0jCcJXCr++JVtQRnllexRW2IDq4Zvrh/McTQjooAKnSUbXZ7wamp7AQSweTnfMVoA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | ||||
|  | ||||
| @@ -40,7 +40,7 @@ | ||||
|     <div class="navbar-brand"> | ||||
|         <a class="navbar-item" href="/"> | ||||
|             <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> | ||||
|         </a> | ||||
|  | ||||
| @@ -76,6 +76,10 @@ | ||||
|     <span class="icon"><i class="far fa-exclamation-circle"></i></span> <span class="error-message"></span> | ||||
| </div> | ||||
|  | ||||
| <div class="notification is-success flash-message" id="success"> | ||||
|     <span class="icon"><i class="far fa-check"></i></span> <span class="success-message"></span> | ||||
| </div> | ||||
|  | ||||
| <div class="modal" id="addImageModal"> | ||||
|     <div class="modal-background"></div> | ||||
|     <div class="modal-card"> | ||||
| @@ -185,14 +189,6 @@ | ||||
|                     </label> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="control"> | ||||
|                 <div class="field"> | ||||
|                     <label> | ||||
|                         <input type="radio" class="default-width" name="exportSelect" value="todos"> | ||||
|                         Todo Lists | ||||
|                     </label> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <br> | ||||
|             <div class="has-text-centered"> | ||||
|                 <div style="color: red"> | ||||
| @@ -234,6 +230,7 @@ | ||||
|         <a href="/"> | ||||
|             <div class="brand"> | ||||
|                 <img src="/static/img/logo_nobg.webp" alt="Reminder bot logo" | ||||
|                      width="52px" height="52px" | ||||
|                      class="dashboard-brand"> | ||||
|             </div> | ||||
|         </a> | ||||
| @@ -256,11 +253,9 @@ | ||||
|                 </p> | ||||
|                 <ul class="menu-list"> | ||||
|                     <li> | ||||
|                         {# | ||||
|                         <a class="show-modal" data-modal="dataManagerModal"> | ||||
|                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export | ||||
|                         </a> | ||||
|                         #} | ||||
|                         <a class="show-modal" data-modal="chooseTimezoneModal"> | ||||
|                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone | ||||
|                         </a> | ||||
| @@ -302,11 +297,9 @@ | ||||
|                 </p> | ||||
|                 <ul class="menu-list"> | ||||
|                     <li> | ||||
|                         {# | ||||
|                         <a class="show-modal" data-modal="dataManagerModal"> | ||||
|                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export | ||||
|                         </a> | ||||
|                         #} | ||||
|                         <a class="show-modal" data-modal="chooseTimezoneModal"> | ||||
|                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone | ||||
|                         </a> | ||||
| @@ -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> | ||||
|             </div> | ||||
|         </section> | ||||
|         <section id="guild" class="is-hidden"> | ||||
|         <section id="reminders" class="is-hidden"> | ||||
|             {% include "reminder_dashboard/reminder_dashboard" %} | ||||
|         </section> | ||||
|         <section id="reminder-errors" class="is-hidden"> | ||||
| @@ -388,9 +381,9 @@ | ||||
| <script src="/static/js/iro.js"></script> | ||||
| <script src="/static/js/dtsel.js"></script> | ||||
|  | ||||
| <script src="/static/js/interval.js"></script> | ||||
| <script src="/static/js/timezone.js" defer></script> | ||||
| <script src="/static/js/main.js" defer></script> | ||||
| <script src="/static/js/interval.js?v{{ version }}"></script> | ||||
| <script src="/static/js/timezone.js?v{{ version }}" defer></script> | ||||
| <script src="/static/js/main.js?v{{ version }}" defer></script> | ||||
|  | ||||
| </body> | ||||
| </html> | ||||
|   | ||||
| @@ -133,8 +133,6 @@ | ||||
|             </article> | ||||
|         </div> | ||||
|         <div class="column settings"> | ||||
|             <div class="columns"> | ||||
|                 <div class="column"> | ||||
|             <div class="field channel-field"> | ||||
|                 <div class="collapses"> | ||||
|                     <label class="label" for="channelOption">Channel*</label> | ||||
| @@ -149,8 +147,7 @@ | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|                 </div> | ||||
|                 <div class="column"> | ||||
|  | ||||
|             <div class="field"> | ||||
|                 <div class="control"> | ||||
|                     <label class="label collapses"> | ||||
| @@ -159,8 +156,6 @@ | ||||
|                     </label> | ||||
|                 </div> | ||||
|             </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div class="collapses split-controls"> | ||||
|                 <div> | ||||
| @@ -236,7 +231,9 @@ | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     {% if creating %} | ||||
|         <div class="button-row"> | ||||
|             <div class="button-row-reminder"> | ||||
| @@ -270,6 +267,3 @@ | ||||
|         </div> | ||||
|     {% endif %} | ||||
| </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|   | ||||
| @@ -2,4 +2,4 @@ | ||||
|  | ||||
| </div> | ||||
|  | ||||
| <script src="/static/js/reminder_errors.js"></script> | ||||
| <!--<script src="/static/js/reminder_errors.js"></script>--> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user