Compare commits
	
		
			30 Commits
		
	
	
		
			next
			...
			06e1474396
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 06e1474396 | |||
|  | adb9c728f4 | ||
|  | fc02eaea4a | ||
|  | 91f87302fb | ||
|  | 97f186dc33 | ||
|  | 6eaa6f0f28 | ||
|  | 9db0fa2513 | ||
|  | ca13fd4fa7 | ||
|  | 55acc8fd16 | ||
|  | 145711fa5d | ||
|  | 5524215786 | ||
|  | e8bd05893f | ||
|  | e3d3418f99 | ||
|  | 2681280a39 | ||
|  | 00579428a1 | ||
|  | b8ef999710 | ||
|  | e8f84e281a | ||
|  | 8ddff698e5 | ||
|  | 541633270c | ||
|  | 25286da5e0 | ||
|  | 4bad1324b9 | ||
|  | bd1462a00c | ||
|  | 56ffc43616 | ||
|  | 52cf642455 | ||
|  | 0bf578357a | ||
|  | 6e9eccb62e | ||
|  | 6ea28284ce | ||
|  | a6525f3052 | ||
|  | 348639270d | ||
|  | 37177c2431 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -3,3 +3,5 @@ | |||||||
| /venv | /venv | ||||||
| .cargo | .cargo | ||||||
| /.idea | /.idea | ||||||
|  | web/static/index.html | ||||||
|  | web/static/assets | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | [submodule "reminder-dashboard"] | ||||||
|  | 	path = reminder-dashboard | ||||||
|  | 	url = gitea@gitea.jellypro.xyz:jude/reminder-dashboard | ||||||
							
								
								
									
										1517
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1517
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										11
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -1,18 +1,18 @@ | |||||||
| [package] | [package] | ||||||
| name = "reminder-rs" | name = "reminder-rs" | ||||||
| version = "1.6.38" | version = "1.6.48" | ||||||
| authors = ["Jude Southworth <judesouthworth@pm.me>"] | authors = ["Jude Southworth <judesouthworth@pm.me>"] | ||||||
| edition = "2021" | edition = "2021" | ||||||
| license = "AGPL-3.0 only" | license = "AGPL-3.0 only" | ||||||
| description = "Reminder Bot for Discord, now in Rust" | description = "Reminder Bot for Discord, now in Rust" | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| poise = "0.5.5" | poise = "0.5" | ||||||
| dotenv = "0.15" | dotenv = "0.15" | ||||||
| tokio = { version = "1", features = ["process", "full"] } | tokio = { version = "1", features = ["process", "full"] } | ||||||
| reqwest = "0.11" | reqwest = "0.11" | ||||||
| lazy-regex = "2.3.0" | lazy-regex = "3.0.2" | ||||||
| regex = "1.6" | regex = "1.9" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| env_logger = "0.10" | env_logger = "0.10" | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| @@ -25,7 +25,7 @@ serde_repr = "0.1" | |||||||
| rmp-serde = "1.1" | rmp-serde = "1.1" | ||||||
| rand = "0.8" | rand = "0.8" | ||||||
| levenshtein = "1.0" | levenshtein = "1.0" | ||||||
| sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]} | sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]} | ||||||
| base64 = "0.21.0" | base64 = "0.21.0" | ||||||
|  |  | ||||||
| [dependencies.postman] | [dependencies.postman] | ||||||
| @@ -43,6 +43,7 @@ assets = [ | |||||||
|     ["conf/default.env", "etc/reminder-rs/config.env", "600"], |     ["conf/default.env", "etc/reminder-rs/config.env", "600"], | ||||||
|     ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"], |     ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"], | ||||||
|     ["web/static/**/*", "lib/reminder-rs/static", "644"], |     ["web/static/**/*", "lib/reminder-rs/static", "644"], | ||||||
|  |     ["reminder-dashboard/dist/static/**/*", "lib/reminder-rs/static", "644"], | ||||||
|     ["web/templates/**/*", "lib/reminder-rs/templates", "644"], |     ["web/templates/**/*", "lib/reminder-rs/templates", "644"], | ||||||
|     ["healthcheck", "lib/reminder-rs/healthcheck", "755"], |     ["healthcheck", "lib/reminder-rs/healthcheck", "755"], | ||||||
|     ["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"], |     ["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"], | ||||||
|   | |||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | ALTER TABLE `reminder_template` ADD COLUMN `interval_seconds` INT UNSIGNED; | ||||||
|  | ALTER TABLE `reminder_template` ADD COLUMN `interval_days` INT UNSIGNED; | ||||||
|  | ALTER TABLE `reminder_template` ADD COLUMN `interval_months` INT UNSIGNED; | ||||||
| @@ -5,12 +5,12 @@ edition = "2021" | |||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| tokio = { version = "1", features = ["process", "full"] } | tokio = { version = "1", features = ["process", "full"] } | ||||||
| regex = "1.4" | regex = "1.9" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| chrono-tz = { version = "0.5", features = ["serde"] } | chrono-tz = { version = "0.8", features = ["serde"] } | ||||||
| lazy_static = "1.4" | lazy_static = "1.4" | ||||||
| num-integer = "0.1" | num-integer = "0.1" | ||||||
| serde = "1.0" | serde = "1.0" | ||||||
| sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]} | sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]} | ||||||
| serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } | serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								reminder-dashboard
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								reminder-dashboard
									
									
									
									
									
										Submodule
									
								
							 Submodule reminder-dashboard added at 8ba7a39ce5
									
								
							| @@ -27,7 +27,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|         "SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", |         "SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||||
|         guild_id.0 |         guild_id.0 | ||||||
|     ) |     ) | ||||||
|     .fetch_all(&mut transaction) |     .fetch_all(&mut *transaction) | ||||||
|     .await?; |     .await?; | ||||||
|  |  | ||||||
|     let mut added_aliases = 0; |     let mut added_aliases = 0; | ||||||
| @@ -42,7 +42,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> { | |||||||
|                     cmd_macro.description, |                     cmd_macro.description, | ||||||
|                     cmd_macro.commands |                     cmd_macro.commands | ||||||
|                 ) |                 ) | ||||||
|                 .execute(&mut transaction) |                 .execute(&mut *transaction) | ||||||
|                 .await?; |                 .await?; | ||||||
|  |  | ||||||
|                 added_aliases += 1; |                 added_aliases += 1; | ||||||
|   | |||||||
| @@ -114,6 +114,8 @@ pub async fn offset( | |||||||
|     #[description = "Number of minutes to offset by"] minutes: Option<isize>, |     #[description = "Number of minutes to offset by"] minutes: Option<isize>, | ||||||
|     #[description = "Number of seconds to offset by"] seconds: Option<isize>, |     #[description = "Number of seconds to offset by"] seconds: Option<isize>, | ||||||
| ) -> Result<(), Error> { | ) -> Result<(), Error> { | ||||||
|  |     ctx.defer().await?; | ||||||
|  |  | ||||||
|     let combined_time = hours.map_or(0, |h| h * HOUR as isize) |     let combined_time = hours.map_or(0, |h| h * HOUR as isize) | ||||||
|         + minutes.map_or(0, |m| m * MINUTE as isize) |         + minutes.map_or(0, |m| m * MINUTE as isize) | ||||||
|         + seconds.map_or(0, |s| s); |         + seconds.map_or(0, |s| s); | ||||||
| @@ -619,7 +621,7 @@ pub async fn multiline( | |||||||
| )] | )] | ||||||
| pub async fn remind( | pub async fn remind( | ||||||
|     ctx: ApplicationContext<'_>, |     ctx: ApplicationContext<'_>, | ||||||
|     #[description = "A description of the time to set the reminder for"] |     #[description = "The time (and optionally date) to set the reminder for"] | ||||||
|     #[autocomplete = "time_hint_autocomplete"] |     #[autocomplete = "time_hint_autocomplete"] | ||||||
|     time: String, |     time: String, | ||||||
|     #[description = "The message content to send"] content: String, |     #[description = "The message content to send"] content: String, | ||||||
| @@ -773,7 +775,7 @@ async fn create_reminder( | |||||||
|                                     b.emoji(ReactionType::Unicode("📝".to_string())) |                                     b.emoji(ReactionType::Unicode("📝".to_string())) | ||||||
|                                         .label("Edit") |                                         .label("Edit") | ||||||
|                                         .style(ButtonStyle::Link) |                                         .style(ButtonStyle::Link) | ||||||
|                                         .url("https://reminder-bot.com/dashboard") |                                         .url("https://beta.reminder-bot.com/dashboard") | ||||||
|                                 }) |                                 }) | ||||||
|                             }) |                             }) | ||||||
|                         }) |                         }) | ||||||
|   | |||||||
| @@ -150,7 +150,7 @@ impl<'a> Parser<'a> { | |||||||
|             "hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0), |             "hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0), | ||||||
|             "days" | "day" | "d" => (0, n, 0, 0), |             "days" | "day" | "d" => (0, n, 0, 0), | ||||||
|             "weeks" | "week" | "w" => (0, n.mul(7)?, 0, 0), |             "weeks" | "week" | "w" => (0, n.mul(7)?, 0, 0), | ||||||
|             "months" | "month" | "M" => (n, 0, 0, 0), |             "months" | "month" => (n, 0, 0, 0), | ||||||
|             "years" | "year" | "y" => (n.mul(12)?, 0, 0, 0), |             "years" | "year" | "y" => (n.mul(12)?, 0, 0, 0), | ||||||
|             _ => { |             _ => { | ||||||
|                 return Err(Error::UnknownUnit { |                 return Err(Error::UnknownUnit { | ||||||
| @@ -255,7 +255,7 @@ impl<'a> Parser<'a> { | |||||||
| /// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000))); | /// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000))); | ||||||
| /// ``` | /// ``` | ||||||
| pub fn parse_duration(s: &str) -> Result<Interval, Error> { | pub fn parse_duration(s: &str) -> Result<Interval, Error> { | ||||||
|     Parser { iter: s.chars(), src: s, current: (0, 0, 0, 0) }.parse() |     Parser { iter: s.to_lowercase().chars(), src: &s.to_lowercase(), current: (0, 0, 0, 0) }.parse() | ||||||
| } | } | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| @@ -324,4 +324,13 @@ mod tests { | |||||||
|         assert_eq!(interval.day, 0); |         assert_eq!(interval.day, 0); | ||||||
|         assert_eq!(interval.month, 120); |         assert_eq!(interval.month, 120); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn parse_case() { | ||||||
|  |         let interval = parse_duration("200 Seconds").unwrap(); | ||||||
|  |  | ||||||
|  |         assert_eq!(interval.sec, 200); | ||||||
|  |         assert_eq!(interval.day, 0); | ||||||
|  |         assert_eq!(interval.month, 0); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,21 +1,22 @@ | |||||||
| [package] | [package] | ||||||
| name = "reminder_web" | name = "reminder_web" | ||||||
| version = "0.1.0" | version = "0.1.3" | ||||||
| authors = ["jellywx <judesouthworth@pm.me>"] | authors = ["jellywx <judesouthworth@pm.me>"] | ||||||
| edition = "2018" | edition = "2018" | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] } | rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] } | ||||||
| rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] } | rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] } | ||||||
| serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } | serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } | ||||||
| oauth2 = "4" | oauth2 = "4" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| reqwest = "0.11" | reqwest = "0.11" | ||||||
| serde = { version = "1.0", features = ["derive"] } | serde = { version = "1.0", features = ["derive"] } | ||||||
| sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] } | sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] } | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| chrono-tz = "0.5" | chrono-tz = "0.8" | ||||||
| lazy_static = "1.4.0" | lazy_static = "1.4.0" | ||||||
| rand = "0.7" | rand = "0.8" | ||||||
| base64 = "0.13" | base64 = "0.13" | ||||||
| csv = "1.1" | csv = "1.2" | ||||||
|  | prometheus = "0.13.3" | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								web/src/catchers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								web/src/catchers.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | use std::collections::HashMap; | ||||||
|  |  | ||||||
|  | use rocket::serde::json::json; | ||||||
|  | use rocket_dyn_templates::Template; | ||||||
|  |  | ||||||
|  | use crate::JsonValue; | ||||||
|  |  | ||||||
|  | #[catch(403)] | ||||||
|  | pub(crate) async fn forbidden() -> Template { | ||||||
|  |     let map: HashMap<String, String> = HashMap::new(); | ||||||
|  |     Template::render("errors/403", &map) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[catch(500)] | ||||||
|  | pub(crate) async fn internal_server_error() -> Template { | ||||||
|  |     let map: HashMap<String, String> = HashMap::new(); | ||||||
|  |     Template::render("errors/500", &map) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[catch(401)] | ||||||
|  | pub(crate) async fn not_authorized() -> Template { | ||||||
|  |     let map: HashMap<String, String> = HashMap::new(); | ||||||
|  |     Template::render("errors/401", &map) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[catch(404)] | ||||||
|  | pub(crate) async fn not_found() -> Template { | ||||||
|  |     let map: HashMap<String, String> = HashMap::new(); | ||||||
|  |     Template::render("errors/404", &map) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[catch(413)] | ||||||
|  | pub(crate) async fn payload_too_large() -> JsonValue { | ||||||
|  |     json!({"error": "Data too large.", "errors": ["Data too large."]}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[catch(422)] | ||||||
|  | pub(crate) async fn unprocessable_entity() -> JsonValue { | ||||||
|  |     json!({"error": "Invalid request.", "errors": ["Invalid request."]}) | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								web/src/guards/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/src/guards/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | pub(crate) mod transaction; | ||||||
							
								
								
									
										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), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										155
									
								
								web/src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										155
									
								
								web/src/lib.rs
									
									
									
									
									
								
							| @@ -4,13 +4,17 @@ extern crate rocket; | |||||||
| mod consts; | mod consts; | ||||||
| #[macro_use] | #[macro_use] | ||||||
| mod macros; | mod macros; | ||||||
|  | mod catchers; | ||||||
|  | mod guards; | ||||||
|  | mod metrics; | ||||||
| mod routes; | mod routes; | ||||||
|  |  | ||||||
| use std::{collections::HashMap, env, path::Path}; | use std::{env, path::Path}; | ||||||
|  |  | ||||||
| use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; | use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; | ||||||
| use rocket::{ | use rocket::{ | ||||||
|     fs::FileServer, |     fs::FileServer, | ||||||
|  |     http::CookieJar, | ||||||
|     serde::json::{json, Value as JsonValue}, |     serde::json::{json, Value as JsonValue}, | ||||||
|     tokio::sync::broadcast::Sender, |     tokio::sync::broadcast::Sender, | ||||||
| }; | }; | ||||||
| @@ -22,7 +26,10 @@ use serenity::{ | |||||||
| }; | }; | ||||||
| use sqlx::{MySql, Pool}; | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
| use crate::consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES}; | use crate::{ | ||||||
|  |     consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES}, | ||||||
|  |     metrics::{init_metrics, MetricProducer}, | ||||||
|  | }; | ||||||
|  |  | ||||||
| type Database = MySql; | type Database = MySql; | ||||||
|  |  | ||||||
| @@ -32,40 +39,6 @@ enum Error { | |||||||
|     Serenity(serenity::Error), |     Serenity(serenity::Error), | ||||||
| } | } | ||||||
|  |  | ||||||
| #[catch(401)] |  | ||||||
| async fn not_authorized() -> Template { |  | ||||||
|     let map: HashMap<String, String> = HashMap::new(); |  | ||||||
|     Template::render("errors/401", &map) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[catch(403)] |  | ||||||
| async fn forbidden() -> Template { |  | ||||||
|     let map: HashMap<String, String> = HashMap::new(); |  | ||||||
|     Template::render("errors/403", &map) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[catch(404)] |  | ||||||
| async fn not_found() -> Template { |  | ||||||
|     let map: HashMap<String, String> = HashMap::new(); |  | ||||||
|     Template::render("errors/404", &map) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[catch(413)] |  | ||||||
| async fn payload_too_large() -> JsonValue { |  | ||||||
|     json!({"error": "Data too large.", "errors": ["Data too large."]}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[catch(422)] |  | ||||||
| async fn unprocessable_entity() -> JsonValue { |  | ||||||
|     json!({"error": "Invalid request.", "errors": ["Invalid request."]}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[catch(500)] |  | ||||||
| async fn internal_server_error() -> Template { |  | ||||||
|     let map: HashMap<String, String> = HashMap::new(); |  | ||||||
|     Template::render("errors/500", &map) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub async fn initialize( | pub async fn initialize( | ||||||
|     kill_channel: Sender<()>, |     kill_channel: Sender<()>, | ||||||
|     serenity_context: Context, |     serenity_context: Context, | ||||||
| @@ -95,17 +68,20 @@ pub async fn initialize( | |||||||
|     let static_path = |     let static_path = | ||||||
|         if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" }; |         if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" }; | ||||||
|  |  | ||||||
|  |     init_metrics(); | ||||||
|  |  | ||||||
|     rocket::build() |     rocket::build() | ||||||
|  |         .attach(MetricProducer) | ||||||
|         .attach(Template::fairing()) |         .attach(Template::fairing()) | ||||||
|         .register( |         .register( | ||||||
|             "/", |             "/", | ||||||
|             catchers![ |             catchers![ | ||||||
|                 not_authorized, |                 catchers::not_authorized, | ||||||
|                 forbidden, |                 catchers::forbidden, | ||||||
|                 not_found, |                 catchers::not_found, | ||||||
|                 internal_server_error, |                 catchers::internal_server_error, | ||||||
|                 unprocessable_entity, |                 catchers::unprocessable_entity, | ||||||
|                 payload_too_large, |                 catchers::payload_too_large, | ||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|         .manage(oauth2_client) |         .manage(oauth2_client) | ||||||
| @@ -116,12 +92,13 @@ pub async fn initialize( | |||||||
|         .mount( |         .mount( | ||||||
|             "/", |             "/", | ||||||
|             routes![ |             routes![ | ||||||
|                 routes::index, |  | ||||||
|                 routes::cookies, |                 routes::cookies, | ||||||
|  |                 routes::index, | ||||||
|  |                 routes::metrics::metrics, | ||||||
|                 routes::privacy, |                 routes::privacy, | ||||||
|                 routes::terms, |  | ||||||
|                 routes::return_to_same_site, |  | ||||||
|                 routes::report::report_error, |                 routes::report::report_error, | ||||||
|  |                 routes::return_to_same_site, | ||||||
|  |                 routes::terms, | ||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|         .mount( |         .mount( | ||||||
| @@ -152,19 +129,19 @@ pub async fn initialize( | |||||||
|             routes![ |             routes![ | ||||||
|                 routes::dashboard::dashboard, |                 routes::dashboard::dashboard, | ||||||
|                 routes::dashboard::dashboard_home, |                 routes::dashboard::dashboard_home, | ||||||
|                 routes::dashboard::user::get_user_info, |                 routes::dashboard::api::user::get_user_info, | ||||||
|                 routes::dashboard::user::update_user_info, |                 routes::dashboard::api::user::update_user_info, | ||||||
|                 routes::dashboard::user::get_user_guilds, |                 routes::dashboard::api::user::get_user_guilds, | ||||||
|                 routes::dashboard::guild::get_guild_patreon, |                 routes::dashboard::api::guild::get_guild_info, | ||||||
|                 routes::dashboard::guild::get_guild_channels, |                 routes::dashboard::api::guild::get_guild_channels, | ||||||
|                 routes::dashboard::guild::get_guild_roles, |                 routes::dashboard::api::guild::get_guild_roles, | ||||||
|                 routes::dashboard::guild::get_reminder_templates, |                 routes::dashboard::api::guild::get_reminder_templates, | ||||||
|                 routes::dashboard::guild::create_reminder_template, |                 routes::dashboard::api::guild::create_reminder_template, | ||||||
|                 routes::dashboard::guild::delete_reminder_template, |                 routes::dashboard::api::guild::delete_reminder_template, | ||||||
|                 routes::dashboard::guild::create_guild_reminder, |                 routes::dashboard::api::guild::create_guild_reminder, | ||||||
|                 routes::dashboard::guild::get_reminders, |                 routes::dashboard::api::guild::get_reminders, | ||||||
|                 routes::dashboard::guild::edit_reminder, |                 routes::dashboard::api::guild::edit_reminder, | ||||||
|                 routes::dashboard::guild::delete_reminder, |                 routes::dashboard::api::guild::delete_reminder, | ||||||
|                 routes::dashboard::export::export_reminders, |                 routes::dashboard::export::export_reminders, | ||||||
|                 routes::dashboard::export::export_reminder_templates, |                 routes::dashboard::export::export_reminder_templates, | ||||||
|                 routes::dashboard::export::export_todos, |                 routes::dashboard::export::export_todos, | ||||||
| @@ -222,3 +199,65 @@ pub async fn check_guild_subscription( | |||||||
|         false |         false | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub async fn check_authorization( | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &Context, | ||||||
|  |     guild: u64, | ||||||
|  | ) -> Result<(), JsonValue> { | ||||||
|  |     let user_id = cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten(); | ||||||
|  |  | ||||||
|  |     if std::env::var("OFFLINE").map_or(true, |v| v != "1") { | ||||||
|  |         match user_id { | ||||||
|  |             Some(user_id) => { | ||||||
|  |                 let admin_id = std::env::var("ADMIN_ID") | ||||||
|  |                     .map_or(false, |u| u.parse::<u64>().map_or(false, |u| u == user_id)); | ||||||
|  |  | ||||||
|  |                 if admin_id { | ||||||
|  |                     return Ok(()); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 match GuildId(guild).to_guild_cached(ctx) { | ||||||
|  |                     Some(guild) => { | ||||||
|  |                         let member_res = guild.member(ctx, UserId(user_id)).await; | ||||||
|  |  | ||||||
|  |                         match member_res { | ||||||
|  |                             Err(_) => { | ||||||
|  |                                 return Err(json!({"error": "User not in guild"})); | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             Ok(member) => { | ||||||
|  |                                 let permissions_res = member.permissions(ctx); | ||||||
|  |  | ||||||
|  |                                 match permissions_res { | ||||||
|  |                                     Err(_) => { | ||||||
|  |                                         return Err(json!({"error": "Couldn't fetch permissions"})); | ||||||
|  |                                     } | ||||||
|  |  | ||||||
|  |                                     Ok(permissions) => { | ||||||
|  |                                         if !(permissions.manage_messages() | ||||||
|  |                                             || permissions.manage_guild() | ||||||
|  |                                             || permissions.administrator()) | ||||||
|  |                                         { | ||||||
|  |                                             return Err(json!({"error": "Incorrect permissions"})); | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     None => { | ||||||
|  |                         return Err(json!({"error": "Bot not in guild"})); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             None => { | ||||||
|  |                 return Err(json!({"error": "User not authorized"})); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -54,56 +54,6 @@ macro_rules! check_url_opt { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
| macro_rules! check_authorization { |  | ||||||
|     ($cookies:expr, $ctx:expr, $guild:expr) => { |  | ||||||
|         use serenity::model::id::UserId; |  | ||||||
|  |  | ||||||
|         let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten(); |  | ||||||
|  |  | ||||||
|         if std::env::var("OFFLINE").map_or(true, |v| v != "1") { |  | ||||||
|             match user_id { |  | ||||||
|                 Some(user_id) => { |  | ||||||
|                     match GuildId($guild).to_guild_cached($ctx) { |  | ||||||
|                         Some(guild) => { |  | ||||||
|                             let member_res = guild.member($ctx, UserId(user_id)).await; |  | ||||||
|  |  | ||||||
|                             match member_res { |  | ||||||
|                                 Err(_) => { |  | ||||||
|                                     return Err(json!({"error": "User not in guild"})); |  | ||||||
|                                 } |  | ||||||
|  |  | ||||||
|                                 Ok(member) => { |  | ||||||
|                                     let permissions_res = member.permissions($ctx); |  | ||||||
|  |  | ||||||
|                                     match permissions_res { |  | ||||||
|                                         Err(_) => { |  | ||||||
|                                             return Err(json!({"error": "Couldn't fetch permissions"})); |  | ||||||
|                                         } |  | ||||||
|  |  | ||||||
|                                         Ok(permissions) => { |  | ||||||
|                                             if !(permissions.manage_messages() || permissions.manage_guild() || permissions.administrator()) { |  | ||||||
|                                                 return Err(json!({"error": "Incorrect permissions"})); |  | ||||||
|                                             } |  | ||||||
|                                         } |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         None => { |  | ||||||
|                             return Err(json!({"error": "Bot not in guild"})); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 None => { |  | ||||||
|                     return Err(json!({"error": "User not authorized"})); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| macro_rules! update_field { | macro_rules! update_field { | ||||||
|     ($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => { |     ($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => { | ||||||
|         if let Some(value) = &$reminder.$field { |         if let Some(value) = &$reminder.$field { | ||||||
|   | |||||||
							
								
								
									
										43
									
								
								web/src/metrics.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								web/src/metrics.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | use lazy_static::lazy_static; | ||||||
|  | use prometheus::{IntCounterVec, Opts, Registry}; | ||||||
|  | use rocket::{ | ||||||
|  |     fairing::{Fairing, Info, Kind}, | ||||||
|  |     Data, Request, Response, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | lazy_static! { | ||||||
|  |     pub static ref REGISTRY: Registry = Registry::new(); | ||||||
|  |     static ref REQUEST_COUNTER: IntCounterVec = | ||||||
|  |         IntCounterVec::new(Opts::new("requests", "Requests"), &["method", "route"]).unwrap(); | ||||||
|  |     static ref RESPONSE_COUNTER: IntCounterVec = | ||||||
|  |         IntCounterVec::new(Opts::new("responses", "Responses"), &["status", "route"]).unwrap(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn init_metrics() { | ||||||
|  |     REGISTRY.register(Box::new(REQUEST_COUNTER.clone())).unwrap(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct MetricProducer; | ||||||
|  |  | ||||||
|  | #[rocket::async_trait] | ||||||
|  | impl Fairing for MetricProducer { | ||||||
|  |     fn info(&self) -> Info { | ||||||
|  |         Info { name: "Metrics fairing", kind: Kind::Request } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) { | ||||||
|  |         if let Some(route) = req.route() { | ||||||
|  |             REQUEST_COUNTER | ||||||
|  |                 .with_label_values(&[req.method().as_str(), &route.uri.to_string()]) | ||||||
|  |                 .inc(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn on_response<'r>(&self, req: &'r Request<'_>, resp: &mut Response<'r>) { | ||||||
|  |         if let Some(route) = req.route() { | ||||||
|  |             RESPONSE_COUNTER | ||||||
|  |                 .with_label_values(&[&resp.status().code.to_string(), &route.uri.to_string()]) | ||||||
|  |                 .inc(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								web/src/routes/dashboard/api/guild/channels.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								web/src/routes/dashboard/api/guild/channels.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | use rocket::{http::CookieJar, serde::json::json, State}; | ||||||
|  | use serde::Serialize; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::{ | ||||||
|  |         channel::GuildChannel, | ||||||
|  |         id::{ChannelId, GuildId}, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use crate::{check_authorization, routes::JsonResult}; | ||||||
|  |  | ||||||
|  | #[derive(Serialize)] | ||||||
|  | struct ChannelInfo { | ||||||
|  |     id: String, | ||||||
|  |     name: String, | ||||||
|  |     webhook_avatar: Option<String>, | ||||||
|  |     webhook_name: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[get("/api/guild/<id>/channels")] | ||||||
|  | pub async fn get_guild_channels( | ||||||
|  |     id: u64, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     offline!(Ok(json!(vec![ChannelInfo { | ||||||
|  |         name: "general".to_string(), | ||||||
|  |         id: "1".to_string(), | ||||||
|  |         webhook_avatar: None, | ||||||
|  |         webhook_name: None, | ||||||
|  |     }]))); | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     match GuildId(id).to_guild_cached(ctx.inner()) { | ||||||
|  |         Some(guild) => { | ||||||
|  |             let mut channels = guild | ||||||
|  |                 .channels | ||||||
|  |                 .iter() | ||||||
|  |                 .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c))) | ||||||
|  |                 .filter(|(_, channel)| channel.is_text_based()) | ||||||
|  |                 .collect::<Vec<(ChannelId, GuildChannel)>>(); | ||||||
|  |  | ||||||
|  |             channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position)); | ||||||
|  |  | ||||||
|  |             let channel_info = channels | ||||||
|  |                 .iter() | ||||||
|  |                 .map(|(channel_id, channel)| ChannelInfo { | ||||||
|  |                     name: channel.name.to_string(), | ||||||
|  |                     id: channel_id.to_string(), | ||||||
|  |                     webhook_avatar: None, | ||||||
|  |                     webhook_name: None, | ||||||
|  |                 }) | ||||||
|  |                 .collect::<Vec<ChannelInfo>>(); | ||||||
|  |  | ||||||
|  |             Ok(json!(channel_info)) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         None => json_err!("Bot not in guild"), | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								web/src/routes/dashboard/api/guild/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								web/src/routes/dashboard/api/guild/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | mod channels; | ||||||
|  | mod reminders; | ||||||
|  | mod roles; | ||||||
|  | mod templates; | ||||||
|  |  | ||||||
|  | use std::env; | ||||||
|  |  | ||||||
|  | pub use channels::*; | ||||||
|  | pub use reminders::*; | ||||||
|  | use rocket::{http::CookieJar, serde::json::json, State}; | ||||||
|  | pub use roles::*; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::id::{GuildId, RoleId}, | ||||||
|  | }; | ||||||
|  | pub use templates::*; | ||||||
|  |  | ||||||
|  | use crate::{check_authorization, routes::JsonResult}; | ||||||
|  |  | ||||||
|  | #[get("/api/guild/<id>")] | ||||||
|  | pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { | ||||||
|  |     offline!(Ok(json!({ "patreon": true, "name": "Guild" }))); | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     match GuildId(id).to_guild_cached(ctx.inner()) { | ||||||
|  |         Some(guild) => { | ||||||
|  |             let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) | ||||||
|  |                 .member(&ctx.inner(), guild.owner_id) | ||||||
|  |                 .await; | ||||||
|  |  | ||||||
|  |             let patreon = member_res.map_or(false, |member| { | ||||||
|  |                 member | ||||||
|  |                     .roles | ||||||
|  |                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             Ok(json!({ "patreon": patreon, "name": guild.name })) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         None => json_err!("Bot not in guild"), | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,321 +1,59 @@ | |||||||
| use std::env; |  | ||||||
| 
 |  | ||||||
| use rocket::{ | use rocket::{ | ||||||
|     http::CookieJar, |     http::CookieJar, | ||||||
|     serde::json::{json, Json}, |     serde::json::{json, Json}, | ||||||
|     State, |     State, | ||||||
| }; | }; | ||||||
| use serde::Serialize; |  | ||||||
| use serenity::{ | use serenity::{ | ||||||
|     client::Context, |     client::Context, | ||||||
|     model::{ |     model::id::{ChannelId, GuildId, UserId}, | ||||||
|         channel::GuildChannel, |  | ||||||
|         id::{ChannelId, GuildId, RoleId}, |  | ||||||
|     }, |  | ||||||
| }; | }; | ||||||
| use sqlx::{MySql, Pool}; | use sqlx::{MySql, Pool}; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     check_guild_subscription, check_subscription, |     check_authorization, check_guild_subscription, check_subscription, | ||||||
|     consts::{ |     consts::MIN_INTERVAL, | ||||||
|         MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, |     guards::transaction::Transaction, | ||||||
|         MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH, |  | ||||||
|         MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, |  | ||||||
|         MIN_INTERVAL, |  | ||||||
|     }, |  | ||||||
|     routes::{ |     routes::{ | ||||||
|         dashboard::{ |         dashboard::{ | ||||||
|             create_database_channel, create_reminder, template_name_default, DeleteReminder, |             create_database_channel, create_reminder, DeleteReminder, PatchReminder, Reminder, | ||||||
|             DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate, |  | ||||||
|         }, |         }, | ||||||
|         JsonResult, |         JsonResult, | ||||||
|     }, |     }, | ||||||
|  |     Database, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #[derive(Serialize)] |  | ||||||
| struct ChannelInfo { |  | ||||||
|     id: String, |  | ||||||
|     name: String, |  | ||||||
|     webhook_avatar: Option<String>, |  | ||||||
|     webhook_name: Option<String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[get("/api/guild/<id>/patreon")] |  | ||||||
| pub async fn get_guild_patreon( |  | ||||||
|     id: u64, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     offline!(Ok(json!({ "patreon": true }))); |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
| 
 |  | ||||||
|     match GuildId(id).to_guild_cached(ctx.inner()) { |  | ||||||
|         Some(guild) => { |  | ||||||
|             let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) |  | ||||||
|                 .member(&ctx.inner(), guild.owner_id) |  | ||||||
|                 .await; |  | ||||||
| 
 |  | ||||||
|             let patreon = member_res.map_or(false, |member| { |  | ||||||
|                 member |  | ||||||
|                     .roles |  | ||||||
|                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             Ok(json!({ "patreon": patreon })) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         None => json_err!("Bot not in guild"), |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[get("/api/guild/<id>/channels")] |  | ||||||
| pub async fn get_guild_channels( |  | ||||||
|     id: u64, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     offline!(Ok(json!(vec![ChannelInfo { |  | ||||||
|         name: "general".to_string(), |  | ||||||
|         id: "1".to_string(), |  | ||||||
|         webhook_avatar: None, |  | ||||||
|         webhook_name: None, |  | ||||||
|     }]))); |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
| 
 |  | ||||||
|     match GuildId(id).to_guild_cached(ctx.inner()) { |  | ||||||
|         Some(guild) => { |  | ||||||
|             let mut channels = guild |  | ||||||
|                 .channels |  | ||||||
|                 .iter() |  | ||||||
|                 .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c))) |  | ||||||
|                 .filter(|(_, channel)| channel.is_text_based()) |  | ||||||
|                 .collect::<Vec<(ChannelId, GuildChannel)>>(); |  | ||||||
| 
 |  | ||||||
|             channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position)); |  | ||||||
| 
 |  | ||||||
|             let channel_info = channels |  | ||||||
|                 .iter() |  | ||||||
|                 .map(|(channel_id, channel)| ChannelInfo { |  | ||||||
|                     name: channel.name.to_string(), |  | ||||||
|                     id: channel_id.to_string(), |  | ||||||
|                     webhook_avatar: None, |  | ||||||
|                     webhook_name: None, |  | ||||||
|                 }) |  | ||||||
|                 .collect::<Vec<ChannelInfo>>(); |  | ||||||
| 
 |  | ||||||
|             Ok(json!(channel_info)) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         None => json_err!("Bot not in guild"), |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize)] |  | ||||||
| struct RoleInfo { |  | ||||||
|     id: String, |  | ||||||
|     name: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[get("/api/guild/<id>/roles")] |  | ||||||
| pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { |  | ||||||
|     offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }]))); |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
| 
 |  | ||||||
|     let roles_res = ctx.cache.guild_roles(id); |  | ||||||
| 
 |  | ||||||
|     match roles_res { |  | ||||||
|         Some(roles) => { |  | ||||||
|             let roles = roles |  | ||||||
|                 .iter() |  | ||||||
|                 .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() }) |  | ||||||
|                 .collect::<Vec<RoleInfo>>(); |  | ||||||
| 
 |  | ||||||
|             Ok(json!(roles)) |  | ||||||
|         } |  | ||||||
|         None => { |  | ||||||
|             warn!("Could not fetch roles from {}", id); |  | ||||||
| 
 |  | ||||||
|             json_err!("Could not get roles") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[get("/api/guild/<id>/templates")] |  | ||||||
| pub async fn get_reminder_templates( |  | ||||||
|     id: u64, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
| 
 |  | ||||||
|     match sqlx::query_as_unchecked!( |  | ||||||
|         ReminderTemplate, |  | ||||||
|         "SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", |  | ||||||
|         id |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     { |  | ||||||
|         Ok(templates) => Ok(json!(templates)), |  | ||||||
|         Err(e) => { |  | ||||||
|             warn!("Could not fetch templates from {}: {:?}", id, e); |  | ||||||
| 
 |  | ||||||
|             json_err!("Could not get templates") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[post("/api/guild/<id>/templates", data = "<reminder_template>")] |  | ||||||
| pub async fn create_reminder_template( |  | ||||||
|     id: u64, |  | ||||||
|     reminder_template: Json<ReminderTemplate>, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
| 
 |  | ||||||
|     // validate lengths
 |  | ||||||
|     check_length!(MAX_CONTENT_LENGTH, reminder_template.content); |  | ||||||
|     check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description); |  | ||||||
|     check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title); |  | ||||||
|     check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author); |  | ||||||
|     check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer); |  | ||||||
|     check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields); |  | ||||||
|     if let Some(fields) = &reminder_template.embed_fields { |  | ||||||
|         for field in &fields.0 { |  | ||||||
|             check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value); |  | ||||||
|             check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username); |  | ||||||
|     check_length_opt!( |  | ||||||
|         MAX_URL_LENGTH, |  | ||||||
|         reminder_template.embed_footer_url, |  | ||||||
|         reminder_template.embed_thumbnail_url, |  | ||||||
|         reminder_template.embed_author_url, |  | ||||||
|         reminder_template.embed_image_url, |  | ||||||
|         reminder_template.avatar |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     // validate urls
 |  | ||||||
|     check_url_opt!( |  | ||||||
|         reminder_template.embed_footer_url, |  | ||||||
|         reminder_template.embed_thumbnail_url, |  | ||||||
|         reminder_template.embed_author_url, |  | ||||||
|         reminder_template.embed_image_url, |  | ||||||
|         reminder_template.avatar |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     let name = if reminder_template.name.is_empty() { |  | ||||||
|         template_name_default() |  | ||||||
|     } else { |  | ||||||
|         reminder_template.name.clone() |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     match sqlx::query!( |  | ||||||
|         "INSERT INTO reminder_template
 |  | ||||||
|         (guild_id, |  | ||||||
|          name, |  | ||||||
|          attachment, |  | ||||||
|          attachment_name, |  | ||||||
|          avatar, |  | ||||||
|          content, |  | ||||||
|          embed_author, |  | ||||||
|          embed_author_url, |  | ||||||
|          embed_color, |  | ||||||
|          embed_description, |  | ||||||
|          embed_footer, |  | ||||||
|          embed_footer_url, |  | ||||||
|          embed_image_url, |  | ||||||
|          embed_thumbnail_url, |  | ||||||
|          embed_title, |  | ||||||
|          embed_fields, |  | ||||||
|          tts, |  | ||||||
|          username |  | ||||||
|         ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
 |  | ||||||
|         id, name, |  | ||||||
|         reminder_template.attachment, |  | ||||||
|         reminder_template.attachment_name, |  | ||||||
|         reminder_template.avatar, |  | ||||||
|         reminder_template.content, |  | ||||||
|         reminder_template.embed_author, |  | ||||||
|         reminder_template.embed_author_url, |  | ||||||
|         reminder_template.embed_color, |  | ||||||
|         reminder_template.embed_description, |  | ||||||
|         reminder_template.embed_footer, |  | ||||||
|         reminder_template.embed_footer_url, |  | ||||||
|         reminder_template.embed_image_url, |  | ||||||
|         reminder_template.embed_thumbnail_url, |  | ||||||
|         reminder_template.embed_title, |  | ||||||
|         reminder_template.embed_fields, |  | ||||||
|         reminder_template.tts, |  | ||||||
|         reminder_template.username, |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     { |  | ||||||
|         Ok(_) => { |  | ||||||
|             Ok(json!({})) |  | ||||||
|         } |  | ||||||
|         Err(e) => { |  | ||||||
|             warn!("Could not create template for {}: {:?}", id, e); |  | ||||||
| 
 |  | ||||||
|             json_err!("Could not create template") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")] |  | ||||||
| pub async fn delete_reminder_template( |  | ||||||
|     id: u64, |  | ||||||
|     delete_reminder_template: Json<DeleteReminderTemplate>, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
| 
 |  | ||||||
|     match sqlx::query!( |  | ||||||
|         "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?", |  | ||||||
|         id, delete_reminder_template.id |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     { |  | ||||||
|         Ok(_) => { |  | ||||||
|             Ok(json!({})) |  | ||||||
|         } |  | ||||||
|         Err(e) => { |  | ||||||
|             warn!("Could not delete template from {}: {:?}", id, e); |  | ||||||
| 
 |  | ||||||
|             json_err!("Could not delete template") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[post("/api/guild/<id>/reminders", data = "<reminder>")] | #[post("/api/guild/<id>/reminders", data = "<reminder>")] | ||||||
| pub async fn create_guild_reminder( | pub async fn create_guild_reminder( | ||||||
|     id: u64, |     id: u64, | ||||||
|     reminder: Json<Reminder>, |     reminder: Json<Reminder>, | ||||||
|     cookies: &CookieJar<'_>, |     cookies: &CookieJar<'_>, | ||||||
|     serenity_context: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     mut transaction: Transaction<'_>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, serenity_context.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
| 
 | 
 | ||||||
|     let user_id = |     let user_id = | ||||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); |         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||||
| 
 | 
 | ||||||
|     create_reminder( |     match create_reminder( | ||||||
|         serenity_context.inner(), |         ctx.inner(), | ||||||
|         pool.inner(), |         &mut transaction, | ||||||
|         GuildId(id), |         GuildId(id), | ||||||
|         UserId(user_id), |         UserId(user_id), | ||||||
|         reminder.into_inner(), |         reminder.into_inner(), | ||||||
|     ) |     ) | ||||||
|     .await |     .await | ||||||
|  |     { | ||||||
|  |         Ok(r) => match transaction.commit().await { | ||||||
|  |             Ok(_) => Ok(r), | ||||||
|  |             Err(e) => { | ||||||
|  |                 warn!("Couldn't commit transaction: {:?}", e); | ||||||
|  |                 json_err!("Couldn't commit transaction.") | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  | 
 | ||||||
|  |         Err(e) => Err(e), | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[get("/api/guild/<id>/reminders")] | #[get("/api/guild/<id>/reminders")] | ||||||
| @@ -323,10 +61,9 @@ pub async fn get_reminders( | |||||||
|     id: u64, |     id: u64, | ||||||
|     cookies: &CookieJar<'_>, |     cookies: &CookieJar<'_>, | ||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     serenity_context: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, serenity_context.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
| 
 | 
 | ||||||
|     let channels_res = GuildId(id).channels(&ctx.inner()).await; |     let channels_res = GuildId(id).channels(&ctx.inner()).await; | ||||||
| 
 | 
 | ||||||
| @@ -394,11 +131,12 @@ pub async fn get_reminders( | |||||||
| pub async fn edit_reminder( | pub async fn edit_reminder( | ||||||
|     id: u64, |     id: u64, | ||||||
|     reminder: Json<PatchReminder>, |     reminder: Json<PatchReminder>, | ||||||
|     serenity_context: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     mut transaction: Transaction<'_>, | ||||||
|  |     pool: &State<Pool<Database>>, | ||||||
|     cookies: &CookieJar<'_>, |     cookies: &CookieJar<'_>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, serenity_context.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
| 
 | 
 | ||||||
|     let mut error = vec![]; |     let mut error = vec![]; | ||||||
| 
 | 
 | ||||||
| @@ -406,7 +144,7 @@ pub async fn edit_reminder( | |||||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); |         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||||
| 
 | 
 | ||||||
|     if reminder.message_ok() { |     if reminder.message_ok() { | ||||||
|         update_field!(pool.inner(), error, reminder.[ |         update_field!(transaction.executor(), error, reminder.[ | ||||||
|             content, |             content, | ||||||
|             embed_author, |             embed_author, | ||||||
|             embed_description, |             embed_description, | ||||||
| @@ -419,7 +157,7 @@ pub async fn edit_reminder( | |||||||
|         error.push("Message exceeds limits.".to_string()); |         error.push("Message exceeds limits.".to_string()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     update_field!(pool.inner(), error, reminder.[ |     update_field!(transaction.executor(), error, reminder.[ | ||||||
|         attachment, |         attachment, | ||||||
|         attachment_name, |         attachment_name, | ||||||
|         avatar, |         avatar, | ||||||
| @@ -440,8 +178,8 @@ pub async fn edit_reminder( | |||||||
|         || reminder.interval_months.flatten().is_some() |         || reminder.interval_months.flatten().is_some() | ||||||
|         || reminder.interval_seconds.flatten().is_some() |         || reminder.interval_seconds.flatten().is_some() | ||||||
|     { |     { | ||||||
|         if check_guild_subscription(&serenity_context.inner(), id).await |         if check_guild_subscription(&ctx.inner(), id).await | ||||||
|             || check_subscription(&serenity_context.inner(), user_id).await |             || check_subscription(&ctx.inner(), user_id).await | ||||||
|         { |         { | ||||||
|             let new_interval_length = match reminder.interval_days { |             let new_interval_length = match reminder.interval_days { | ||||||
|                 Some(interval) => interval.unwrap_or(0), |                 Some(interval) => interval.unwrap_or(0), | ||||||
| @@ -449,7 +187,7 @@ pub async fn edit_reminder( | |||||||
|                     "SELECT interval_days AS days FROM reminders WHERE uid = ?", |                     "SELECT interval_days AS days FROM reminders WHERE uid = ?", | ||||||
|                     reminder.uid |                     reminder.uid | ||||||
|                 ) |                 ) | ||||||
|                 .fetch_one(pool.inner()) |                 .fetch_one(transaction.executor()) | ||||||
|                 .await |                 .await | ||||||
|                 .map_err(|e| { |                 .map_err(|e| { | ||||||
|                     warn!("Error updating reminder interval: {:?}", e); |                     warn!("Error updating reminder interval: {:?}", e); | ||||||
| @@ -463,7 +201,7 @@ pub async fn edit_reminder( | |||||||
|                     "SELECT interval_months AS months FROM reminders WHERE uid = ?", |                     "SELECT interval_months AS months FROM reminders WHERE uid = ?", | ||||||
|                     reminder.uid |                     reminder.uid | ||||||
|                 ) |                 ) | ||||||
|                 .fetch_one(pool.inner()) |                 .fetch_one(transaction.executor()) | ||||||
|                 .await |                 .await | ||||||
|                 .map_err(|e| { |                 .map_err(|e| { | ||||||
|                     warn!("Error updating reminder interval: {:?}", e); |                     warn!("Error updating reminder interval: {:?}", e); | ||||||
| @@ -477,7 +215,7 @@ pub async fn edit_reminder( | |||||||
|                     "SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?", |                     "SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?", | ||||||
|                     reminder.uid |                     reminder.uid | ||||||
|                 ) |                 ) | ||||||
|                 .fetch_one(pool.inner()) |                 .fetch_one(transaction.executor()) | ||||||
|                 .await |                 .await | ||||||
|                 .map_err(|e| { |                 .map_err(|e| { | ||||||
|                     warn!("Error updating reminder interval: {:?}", e); |                     warn!("Error updating reminder interval: {:?}", e); | ||||||
| @@ -490,7 +228,7 @@ pub async fn edit_reminder( | |||||||
|             if new_interval_length < *MIN_INTERVAL { |             if new_interval_length < *MIN_INTERVAL { | ||||||
|                 error.push(String::from("New interval is too short.")); |                 error.push(String::from("New interval is too short.")); | ||||||
|             } else { |             } else { | ||||||
|                 update_field!(pool.inner(), error, reminder.[ |                 update_field!(transaction.executor(), error, reminder.[ | ||||||
|                     interval_days, |                     interval_days, | ||||||
|                     interval_months, |                     interval_months, | ||||||
|                     interval_seconds |                     interval_seconds | ||||||
| @@ -500,7 +238,7 @@ pub async fn edit_reminder( | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if reminder.channel > 0 { |     if reminder.channel > 0 { | ||||||
|         let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner()); |         let channel = ChannelId(reminder.channel).to_channel_cached(&ctx.inner()); | ||||||
|         match channel { |         match channel { | ||||||
|             Some(channel) => { |             Some(channel) => { | ||||||
|                 let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id); |                 let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id); | ||||||
| @@ -515,9 +253,9 @@ pub async fn edit_reminder( | |||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 let channel = create_database_channel( |                 let channel = create_database_channel( | ||||||
|                     serenity_context.inner(), |                     ctx.inner(), | ||||||
|                     ChannelId(reminder.channel), |                     ChannelId(reminder.channel), | ||||||
|                     pool.inner(), |                     &mut transaction, | ||||||
|                 ) |                 ) | ||||||
|                 .await; |                 .await; | ||||||
| 
 | 
 | ||||||
| @@ -536,7 +274,7 @@ pub async fn edit_reminder( | |||||||
|                     channel, |                     channel, | ||||||
|                     reminder.uid |                     reminder.uid | ||||||
|                 ) |                 ) | ||||||
|                 .execute(pool.inner()) |                 .execute(transaction.executor()) | ||||||
|                 .await |                 .await | ||||||
|                 { |                 { | ||||||
|                     Ok(_) => {} |                     Ok(_) => {} | ||||||
| @@ -559,6 +297,11 @@ pub async fn edit_reminder( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if let Err(e) = transaction.commit().await { | ||||||
|  |         warn!("Couldn't commit transaction: {:?}", e); | ||||||
|  |         return json_err!("Couldn't commit transaction"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     match sqlx::query_as_unchecked!( |     match sqlx::query_as_unchecked!( | ||||||
|         Reminder, |         Reminder, | ||||||
|         "SELECT reminders.attachment,
 |         "SELECT reminders.attachment,
 | ||||||
| @@ -605,11 +348,16 @@ pub async fn edit_reminder( | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[delete("/api/guild/<_>/reminders", data = "<reminder>")] | #[delete("/api/guild/<id>/reminders", data = "<reminder>")] | ||||||
| pub async fn delete_reminder( | pub async fn delete_reminder( | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     id: u64, | ||||||
|     reminder: Json<DeleteReminder>, |     reminder: Json<DeleteReminder>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  | 
 | ||||||
|     match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) |     match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) | ||||||
|         .execute(pool.inner()) |         .execute(pool.inner()) | ||||||
|         .await |         .await | ||||||
							
								
								
									
										35
									
								
								web/src/routes/dashboard/api/guild/roles.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/src/routes/dashboard/api/guild/roles.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | use rocket::{http::CookieJar, serde::json::json, State}; | ||||||
|  | use serde::Serialize; | ||||||
|  | use serenity::client::Context; | ||||||
|  |  | ||||||
|  | use crate::{check_authorization, routes::JsonResult}; | ||||||
|  |  | ||||||
|  | #[derive(Serialize)] | ||||||
|  | struct RoleInfo { | ||||||
|  |     id: String, | ||||||
|  |     name: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[get("/api/guild/<id>/roles")] | ||||||
|  | pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { | ||||||
|  |     offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }]))); | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     let roles_res = ctx.cache.guild_roles(id); | ||||||
|  |  | ||||||
|  |     match roles_res { | ||||||
|  |         Some(roles) => { | ||||||
|  |             let roles = roles | ||||||
|  |                 .iter() | ||||||
|  |                 .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() }) | ||||||
|  |                 .collect::<Vec<RoleInfo>>(); | ||||||
|  |  | ||||||
|  |             Ok(json!(roles)) | ||||||
|  |         } | ||||||
|  |         None => { | ||||||
|  |             warn!("Could not fetch roles from {}", id); | ||||||
|  |  | ||||||
|  |             json_err!("Could not get roles") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										181
									
								
								web/src/routes/dashboard/api/guild/templates.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								web/src/routes/dashboard/api/guild/templates.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | |||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Json}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serenity::client::Context; | ||||||
|  | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
|  | use crate::{ | ||||||
|  |     check_authorization, | ||||||
|  |     consts::{ | ||||||
|  |         MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, | ||||||
|  |         MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH, | ||||||
|  |         MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, | ||||||
|  |     }, | ||||||
|  |     routes::{ | ||||||
|  |         dashboard::{template_name_default, DeleteReminderTemplate, ReminderTemplate}, | ||||||
|  |         JsonResult, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | #[get("/api/guild/<id>/templates")] | ||||||
|  | pub async fn get_reminder_templates( | ||||||
|  |     id: u64, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     match sqlx::query_as_unchecked!( | ||||||
|  |         ReminderTemplate, | ||||||
|  |         "SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||||
|  |         id | ||||||
|  |     ) | ||||||
|  |     .fetch_all(pool.inner()) | ||||||
|  |     .await | ||||||
|  |     { | ||||||
|  |         Ok(templates) => Ok(json!(templates)), | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Could not fetch templates from {}: {:?}", id, e); | ||||||
|  |  | ||||||
|  |             json_err!("Could not get templates") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[post("/api/guild/<id>/templates", data = "<reminder_template>")] | ||||||
|  | pub async fn create_reminder_template( | ||||||
|  |     id: u64, | ||||||
|  |     reminder_template: Json<ReminderTemplate>, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     // validate lengths | ||||||
|  |     check_length!(MAX_CONTENT_LENGTH, reminder_template.content); | ||||||
|  |     check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description); | ||||||
|  |     check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title); | ||||||
|  |     check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author); | ||||||
|  |     check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer); | ||||||
|  |     check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields); | ||||||
|  |     if let Some(fields) = &reminder_template.embed_fields { | ||||||
|  |         for field in &fields.0 { | ||||||
|  |             check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value); | ||||||
|  |             check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username); | ||||||
|  |     check_length_opt!( | ||||||
|  |         MAX_URL_LENGTH, | ||||||
|  |         reminder_template.embed_footer_url, | ||||||
|  |         reminder_template.embed_thumbnail_url, | ||||||
|  |         reminder_template.embed_author_url, | ||||||
|  |         reminder_template.embed_image_url, | ||||||
|  |         reminder_template.avatar | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // validate urls | ||||||
|  |     check_url_opt!( | ||||||
|  |         reminder_template.embed_footer_url, | ||||||
|  |         reminder_template.embed_thumbnail_url, | ||||||
|  |         reminder_template.embed_author_url, | ||||||
|  |         reminder_template.embed_image_url, | ||||||
|  |         reminder_template.avatar | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     let name = if reminder_template.name.is_empty() { | ||||||
|  |         template_name_default() | ||||||
|  |     } else { | ||||||
|  |         reminder_template.name.clone() | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     match sqlx::query!( | ||||||
|  |         "INSERT INTO reminder_template | ||||||
|  |         (guild_id, | ||||||
|  |          name, | ||||||
|  |          attachment, | ||||||
|  |          attachment_name, | ||||||
|  |          avatar, | ||||||
|  |          content, | ||||||
|  |          embed_author, | ||||||
|  |          embed_author_url, | ||||||
|  |          embed_color, | ||||||
|  |          embed_description, | ||||||
|  |          embed_footer, | ||||||
|  |          embed_footer_url, | ||||||
|  |          embed_image_url, | ||||||
|  |          embed_thumbnail_url, | ||||||
|  |          embed_title, | ||||||
|  |          embed_fields, | ||||||
|  |          interval_seconds, | ||||||
|  |          interval_days, | ||||||
|  |          interval_months, | ||||||
|  |          tts, | ||||||
|  |          username | ||||||
|  |         ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, | ||||||
|  |          ?, ?, ?, ?, ?, ?, ?)", | ||||||
|  |         id, | ||||||
|  |         name, | ||||||
|  |         reminder_template.attachment, | ||||||
|  |         reminder_template.attachment_name, | ||||||
|  |         reminder_template.avatar, | ||||||
|  |         reminder_template.content, | ||||||
|  |         reminder_template.embed_author, | ||||||
|  |         reminder_template.embed_author_url, | ||||||
|  |         reminder_template.embed_color, | ||||||
|  |         reminder_template.embed_description, | ||||||
|  |         reminder_template.embed_footer, | ||||||
|  |         reminder_template.embed_footer_url, | ||||||
|  |         reminder_template.embed_image_url, | ||||||
|  |         reminder_template.embed_thumbnail_url, | ||||||
|  |         reminder_template.embed_title, | ||||||
|  |         reminder_template.embed_fields, | ||||||
|  |         reminder_template.interval_seconds, | ||||||
|  |         reminder_template.interval_days, | ||||||
|  |         reminder_template.interval_months, | ||||||
|  |         reminder_template.tts, | ||||||
|  |         reminder_template.username, | ||||||
|  |     ) | ||||||
|  |     .fetch_all(pool.inner()) | ||||||
|  |     .await | ||||||
|  |     { | ||||||
|  |         Ok(_) => Ok(json!({})), | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Could not create template for {}: {:?}", id, e); | ||||||
|  |  | ||||||
|  |             json_err!("Could not create template") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")] | ||||||
|  | pub async fn delete_reminder_template( | ||||||
|  |     id: u64, | ||||||
|  |     delete_reminder_template: Json<DeleteReminderTemplate>, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     match sqlx::query!( | ||||||
|  |         "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?", | ||||||
|  |         id, delete_reminder_template.id | ||||||
|  |     ) | ||||||
|  |     .fetch_all(pool.inner()) | ||||||
|  |     .await | ||||||
|  |     { | ||||||
|  |         Ok(_) => { | ||||||
|  |             Ok(json!({})) | ||||||
|  |         } | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Could not delete template from {}: {:?}", id, e); | ||||||
|  |  | ||||||
|  |             json_err!("Could not delete template") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								web/src/routes/dashboard/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web/src/routes/dashboard/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | pub mod guild; | ||||||
|  | pub mod user; | ||||||
							
								
								
									
										81
									
								
								web/src/routes/dashboard/api/user/guilds.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								web/src/routes/dashboard/api/user/guilds.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | use reqwest::Client; | ||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Value as JsonValue}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use serenity::model::{id::GuildId, permissions::Permissions}; | ||||||
|  |  | ||||||
|  | use crate::consts::DISCORD_API; | ||||||
|  |  | ||||||
|  | #[derive(Serialize)] | ||||||
|  | struct GuildInfo { | ||||||
|  |     id: String, | ||||||
|  |     name: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Deserialize)] | ||||||
|  | struct PartialGuild { | ||||||
|  |     pub id: GuildId, | ||||||
|  |     pub name: String, | ||||||
|  |     #[serde(default)] | ||||||
|  |     pub owner: bool, | ||||||
|  |     #[serde(rename = "permissions_new")] | ||||||
|  |     pub permissions: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[get("/api/user/guilds")] | ||||||
|  | pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue { | ||||||
|  |     offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }])); | ||||||
|  |  | ||||||
|  |     if let Some(access_token) = cookies.get_private("access_token") { | ||||||
|  |         let request_res = reqwest_client | ||||||
|  |             .get(format!("{}/users/@me/guilds", DISCORD_API)) | ||||||
|  |             .bearer_auth(access_token.value()) | ||||||
|  |             .send() | ||||||
|  |             .await; | ||||||
|  |  | ||||||
|  |         match request_res { | ||||||
|  |             Ok(response) => { | ||||||
|  |                 let guilds_res = response.json::<Vec<PartialGuild>>().await; | ||||||
|  |  | ||||||
|  |                 match guilds_res { | ||||||
|  |                     Ok(guilds) => { | ||||||
|  |                         let reduced_guilds = guilds | ||||||
|  |                             .iter() | ||||||
|  |                             .filter(|g| { | ||||||
|  |                                 g.owner | ||||||
|  |                                     || g.permissions.as_ref().map_or(false, |p| { | ||||||
|  |                                         let permissions = | ||||||
|  |                                             Permissions::from_bits_truncate(p.parse().unwrap()); | ||||||
|  |  | ||||||
|  |                                         permissions.manage_messages() | ||||||
|  |                                             || permissions.manage_guild() | ||||||
|  |                                             || permissions.administrator() | ||||||
|  |                                     }) | ||||||
|  |                             }) | ||||||
|  |                             .map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() }) | ||||||
|  |                             .collect::<Vec<GuildInfo>>(); | ||||||
|  |  | ||||||
|  |                         json!(reduced_guilds) | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     Err(e) => { | ||||||
|  |                         warn!("Error constructing user from request: {:?}", e); | ||||||
|  |  | ||||||
|  |                         json!({"error": "Could not get user details"}) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             Err(e) => { | ||||||
|  |                 warn!("Error getting user guilds: {:?}", e); | ||||||
|  |  | ||||||
|  |                 json!({"error": "Could not reach Discord"}) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         json!({"error": "Not authorized"}) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										97
									
								
								web/src/routes/dashboard/api/user/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								web/src/routes/dashboard/api/user/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | mod guilds; | ||||||
|  |  | ||||||
|  | use std::env; | ||||||
|  |  | ||||||
|  | use chrono_tz::Tz; | ||||||
|  | pub use guilds::*; | ||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Json, Value as JsonValue}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::id::{GuildId, RoleId}, | ||||||
|  | }; | ||||||
|  | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
|  | #[derive(Serialize)] | ||||||
|  | struct UserInfo { | ||||||
|  |     name: String, | ||||||
|  |     patreon: bool, | ||||||
|  |     timezone: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Deserialize)] | ||||||
|  | pub struct UpdateUser { | ||||||
|  |     timezone: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[get("/api/user")] | ||||||
|  | pub async fn get_user_info( | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonValue { | ||||||
|  |     offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None })); | ||||||
|  |  | ||||||
|  |     if let Some(user_id) = | ||||||
|  |         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() | ||||||
|  |     { | ||||||
|  |         let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) | ||||||
|  |             .member(&ctx.inner(), user_id) | ||||||
|  |             .await; | ||||||
|  |  | ||||||
|  |         let timezone = sqlx::query!( | ||||||
|  |             "SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?", | ||||||
|  |             user_id | ||||||
|  |         ) | ||||||
|  |         .fetch_one(pool.inner()) | ||||||
|  |         .await | ||||||
|  |         .map_or(None, |q| Some(q.timezone)); | ||||||
|  |  | ||||||
|  |         let user_info = UserInfo { | ||||||
|  |             name: cookies | ||||||
|  |                 .get_private("username") | ||||||
|  |                 .map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()), | ||||||
|  |             patreon: member_res.map_or(false, |member| { | ||||||
|  |                 member | ||||||
|  |                     .roles | ||||||
|  |                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) | ||||||
|  |             }), | ||||||
|  |             timezone, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         json!(user_info) | ||||||
|  |     } else { | ||||||
|  |         json!({"error": "Not authorized"}) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[patch("/api/user", data = "<user>")] | ||||||
|  | pub async fn update_user_info( | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     user: Json<UpdateUser>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonValue { | ||||||
|  |     if let Some(user_id) = | ||||||
|  |         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() | ||||||
|  |     { | ||||||
|  |         if user.timezone.parse::<Tz>().is_ok() { | ||||||
|  |             let _ = sqlx::query!( | ||||||
|  |                 "UPDATE users SET timezone = ? WHERE user = ?", | ||||||
|  |                 user.timezone, | ||||||
|  |                 user_id, | ||||||
|  |             ) | ||||||
|  |             .execute(pool.inner()) | ||||||
|  |             .await; | ||||||
|  |  | ||||||
|  |             json!({}) | ||||||
|  |         } else { | ||||||
|  |             json!({"error": "Timezone not recognized"}) | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         json!({"error": "Not authorized"}) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								web/src/routes/dashboard/api/user/models.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								web/src/routes/dashboard/api/user/models.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | use std::env; | ||||||
|  |  | ||||||
|  | use chrono_tz::Tz; | ||||||
|  | use reqwest::Client; | ||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Json, Value as JsonValue}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::{ | ||||||
|  |         id::{GuildId, RoleId}, | ||||||
|  |         permissions::Permissions, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
|  | use crate::{consts::DISCORD_API, routes::JsonResult}; | ||||||
							
								
								
									
										29
									
								
								web/src/routes/dashboard/api/user/reminders.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								web/src/routes/dashboard/api/user/reminders.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | use std::env; | ||||||
|  |  | ||||||
|  | use chrono_tz::Tz; | ||||||
|  | use reqwest::Client; | ||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Json, Value as JsonValue}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::{ | ||||||
|  |         id::{GuildId, RoleId}, | ||||||
|  |         permissions::Permissions, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
|  | use crate::{consts::DISCORD_API, routes::JsonResult}; | ||||||
|  |  | ||||||
|  | #[get("/api/user/reminders")] | ||||||
|  | pub async fn get_reminders( | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     Ok(json! {}) | ||||||
|  | } | ||||||
| @@ -6,16 +6,20 @@ use rocket::{ | |||||||
| }; | }; | ||||||
| use serenity::{ | use serenity::{ | ||||||
|     client::Context, |     client::Context, | ||||||
|     model::id::{ChannelId, GuildId}, |     model::id::{ChannelId, GuildId, UserId}, | ||||||
| }; | }; | ||||||
| use sqlx::{MySql, Pool}; | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
| use crate::routes::{ | use crate::{ | ||||||
|     dashboard::{ |     check_authorization, | ||||||
|         create_reminder, generate_uid, ImportBody, Reminder, ReminderCsv, ReminderTemplateCsv, |     guards::transaction::Transaction, | ||||||
|         TodoCsv, |     routes::{ | ||||||
|  |         dashboard::{ | ||||||
|  |             create_reminder, generate_uid, ImportBody, Reminder, ReminderCsv, ReminderTemplateCsv, | ||||||
|  |             TodoCsv, | ||||||
|  |         }, | ||||||
|  |         JsonResult, | ||||||
|     }, |     }, | ||||||
|     JsonResult, |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #[get("/api/guild/<id>/export/reminders")] | #[get("/api/guild/<id>/export/reminders")] | ||||||
| @@ -25,7 +29,7 @@ pub async fn export_reminders( | |||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); |     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); | ||||||
|  |  | ||||||
| @@ -70,7 +74,7 @@ pub async fn export_reminders( | |||||||
|                  reminders.utc_time |                  reminders.utc_time | ||||||
|                 FROM reminders |                 FROM reminders | ||||||
|                 LEFT JOIN channels ON channels.id = reminders.channel_id |                 LEFT JOIN channels ON channels.id = reminders.channel_id | ||||||
|                 WHERE FIND_IN_SET(channels.channel, ?)", |                 WHERE FIND_IN_SET(channels.channel, ?) AND status = 'pending'", | ||||||
|                 channels |                 channels | ||||||
|             ) |             ) | ||||||
|             .fetch_all(pool.inner()) |             .fetch_all(pool.inner()) | ||||||
| @@ -118,14 +122,14 @@ pub async fn export_reminders( | |||||||
| } | } | ||||||
|  |  | ||||||
| #[put("/api/guild/<id>/export/reminders", data = "<body>")] | #[put("/api/guild/<id>/export/reminders", data = "<body>")] | ||||||
| pub async fn import_reminders( | pub(crate) async fn import_reminders( | ||||||
|     id: u64, |     id: u64, | ||||||
|     cookies: &CookieJar<'_>, |     cookies: &CookieJar<'_>, | ||||||
|     body: Json<ImportBody>, |     body: Json<ImportBody>, | ||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     mut transaction: Transaction<'_>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|     let user_id = |     let user_id = | ||||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); |         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||||
| @@ -133,6 +137,7 @@ pub async fn import_reminders( | |||||||
|     match base64::decode(&body.body) { |     match base64::decode(&body.body) { | ||||||
|         Ok(body) => { |         Ok(body) => { | ||||||
|             let mut reader = csv::Reader::from_reader(body.as_slice()); |             let mut reader = csv::Reader::from_reader(body.as_slice()); | ||||||
|  |             let mut count = 0; | ||||||
|  |  | ||||||
|             for result in reader.deserialize::<ReminderCsv>() { |             for result in reader.deserialize::<ReminderCsv>() { | ||||||
|                 match result { |                 match result { | ||||||
| @@ -175,12 +180,14 @@ pub async fn import_reminders( | |||||||
|  |  | ||||||
|                                 create_reminder( |                                 create_reminder( | ||||||
|                                     ctx.inner(), |                                     ctx.inner(), | ||||||
|                                     pool.inner(), |                                     &mut transaction, | ||||||
|                                     GuildId(id), |                                     GuildId(id), | ||||||
|                                     UserId(user_id), |                                     UserId(user_id), | ||||||
|                                     reminder, |                                     reminder, | ||||||
|                                 ) |                                 ) | ||||||
|                                 .await?; |                                 .await?; | ||||||
|  |  | ||||||
|  |                                 count += 1; | ||||||
|                             } |                             } | ||||||
|  |  | ||||||
|                             Err(_) => { |                             Err(_) => { | ||||||
| @@ -200,7 +207,16 @@ pub async fn import_reminders( | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             Ok(json!({})) |             match transaction.commit().await { | ||||||
|  |                 Ok(_) => Ok(json!({ | ||||||
|  |                     "message": format!("Imported {} reminders", count) | ||||||
|  |                 })), | ||||||
|  |  | ||||||
|  |                 Err(e) => { | ||||||
|  |                     warn!("Failed to commit transaction: {:?}", e); | ||||||
|  |                     json_err!("Couldn't commit transaction") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Err(_) => { |         Err(_) => { | ||||||
| @@ -216,7 +232,7 @@ pub async fn export_todos( | |||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); |     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); | ||||||
|  |  | ||||||
| @@ -271,7 +287,7 @@ pub async fn import_todos( | |||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|     let channels_res = GuildId(id).channels(&ctx.inner()).await; |     let channels_res = GuildId(id).channels(&ctx.inner()).await; | ||||||
|  |  | ||||||
| @@ -366,7 +382,7 @@ pub async fn export_reminder_templates( | |||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); |     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); | ||||||
|  |  | ||||||
| @@ -388,6 +404,9 @@ pub async fn export_reminder_templates( | |||||||
|          embed_thumbnail_url, |          embed_thumbnail_url, | ||||||
|          embed_title, |          embed_title, | ||||||
|          embed_fields, |          embed_fields, | ||||||
|  |          interval_seconds, | ||||||
|  |          interval_days, | ||||||
|  |          interval_months, | ||||||
|          tts, |          tts, | ||||||
|          username |          username | ||||||
|         FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", |         FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||||
|   | |||||||
| @@ -1,16 +1,20 @@ | |||||||
| use std::collections::HashMap; | use std::path::Path; | ||||||
|  |  | ||||||
| use chrono::{naive::NaiveDateTime, Utc}; | use chrono::{naive::NaiveDateTime, Utc}; | ||||||
| use rand::{rngs::OsRng, seq::IteratorRandom}; | use rand::{rngs::OsRng, seq::IteratorRandom}; | ||||||
| use rocket::{http::CookieJar, response::Redirect, serde::json::json}; | use rocket::{ | ||||||
| use rocket_dyn_templates::Template; |     fs::{relative, NamedFile}, | ||||||
|  |     http::CookieJar, | ||||||
|  |     response::Redirect, | ||||||
|  |     serde::json::json, | ||||||
|  | }; | ||||||
| use serde::{Deserialize, Deserializer, Serialize}; | use serde::{Deserialize, Deserializer, Serialize}; | ||||||
| use serenity::{ | use serenity::{ | ||||||
|     client::Context, |     client::Context, | ||||||
|     http::Http, |     http::Http, | ||||||
|     model::id::{ChannelId, GuildId, UserId}, |     model::id::{ChannelId, GuildId, UserId}, | ||||||
| }; | }; | ||||||
| use sqlx::{types::Json, Executor}; | use sqlx::types::Json; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     check_guild_subscription, check_subscription, |     check_guild_subscription, check_subscription, | ||||||
| @@ -20,13 +24,13 @@ use crate::{ | |||||||
|         MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, |         MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, | ||||||
|         MAX_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL, |         MAX_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL, | ||||||
|     }, |     }, | ||||||
|  |     guards::transaction::Transaction, | ||||||
|     routes::JsonResult, |     routes::JsonResult, | ||||||
|     Database, Error, |     Error, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | pub mod api; | ||||||
| pub mod export; | pub mod export; | ||||||
| pub mod guild; |  | ||||||
| pub mod user; |  | ||||||
|  |  | ||||||
| type Unset<T> = Option<T>; | type Unset<T> = Option<T>; | ||||||
|  |  | ||||||
| @@ -80,6 +84,9 @@ pub struct ReminderTemplate { | |||||||
|     embed_thumbnail_url: Option<String>, |     embed_thumbnail_url: Option<String>, | ||||||
|     embed_title: String, |     embed_title: String, | ||||||
|     embed_fields: Option<Json<Vec<EmbedField>>>, |     embed_fields: Option<Json<Vec<EmbedField>>>, | ||||||
|  |     interval_seconds: Option<u32>, | ||||||
|  |     interval_days: Option<u32>, | ||||||
|  |     interval_months: Option<u32>, | ||||||
|     tts: bool, |     tts: bool, | ||||||
|     username: Option<String>, |     username: Option<String>, | ||||||
| } | } | ||||||
| @@ -102,6 +109,9 @@ pub struct ReminderTemplateCsv { | |||||||
|     embed_thumbnail_url: Option<String>, |     embed_thumbnail_url: Option<String>, | ||||||
|     embed_title: String, |     embed_title: String, | ||||||
|     embed_fields: Option<String>, |     embed_fields: Option<String>, | ||||||
|  |     interval_seconds: Option<u32>, | ||||||
|  |     interval_days: Option<u32>, | ||||||
|  |     interval_months: Option<u32>, | ||||||
|     tts: bool, |     tts: bool, | ||||||
|     username: Option<String>, |     username: Option<String>, | ||||||
| } | } | ||||||
| @@ -347,21 +357,21 @@ pub struct TodoCsv { | |||||||
|     channel_id: Option<String>, |     channel_id: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn create_reminder( | pub(crate) async fn create_reminder( | ||||||
|     ctx: &Context, |     ctx: &Context, | ||||||
|     pool: impl sqlx::Executor<'_, Database = Database> + Copy, |     transaction: &mut Transaction<'_>, | ||||||
|     guild_id: GuildId, |     guild_id: GuildId, | ||||||
|     user_id: UserId, |     user_id: UserId, | ||||||
|     reminder: Reminder, |     reminder: Reminder, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     // check guild in db |     // check guild in db | ||||||
|     match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0) |     match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0) | ||||||
|         .fetch_one(pool) |         .fetch_one(transaction.executor()) | ||||||
|         .await |         .await | ||||||
|     { |     { | ||||||
|         Err(sqlx::Error::RowNotFound) => { |         Err(sqlx::Error::RowNotFound) => { | ||||||
|             if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.0) |             if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.0) | ||||||
|                 .execute(pool) |                 .execute(transaction.executor()) | ||||||
|                 .await |                 .await | ||||||
|                 .is_err() |                 .is_err() | ||||||
|             { |             { | ||||||
| @@ -387,7 +397,7 @@ pub async fn create_reminder( | |||||||
|         return Err(json!({"error": "Channel not found"})); |         return Err(json!({"error": "Channel not found"})); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let channel = create_database_channel(&ctx, ChannelId(reminder.channel), pool).await; |     let channel = create_database_channel(&ctx, ChannelId(reminder.channel), transaction).await; | ||||||
|  |  | ||||||
|     if let Err(e) = channel { |     if let Err(e) = channel { | ||||||
|         warn!("`create_database_channel` returned an error code: {:?}", e); |         warn!("`create_database_channel` returned an error code: {:?}", e); | ||||||
| @@ -461,8 +471,6 @@ pub async fn create_reminder( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // base64 decode error dropped here |  | ||||||
|     let attachment_data = reminder.attachment.as_ref().map(|s| base64::decode(s).ok()).flatten(); |  | ||||||
|     let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() }; |     let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() }; | ||||||
|     let username = if reminder.username.as_ref().map(|s| s.is_empty()).unwrap_or(true) { |     let username = if reminder.username.as_ref().map(|s| s.is_empty()).unwrap_or(true) { | ||||||
|         None |         None | ||||||
| @@ -503,7 +511,7 @@ pub async fn create_reminder( | |||||||
|          `utc_time` |          `utc_time` | ||||||
|         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | ||||||
|         new_uid, |         new_uid, | ||||||
|         attachment_data, |         reminder.attachment, | ||||||
|         reminder.attachment_name, |         reminder.attachment_name, | ||||||
|         channel, |         channel, | ||||||
|         reminder.avatar, |         reminder.avatar, | ||||||
| @@ -529,7 +537,7 @@ pub async fn create_reminder( | |||||||
|         username, |         username, | ||||||
|         reminder.utc_time, |         reminder.utc_time, | ||||||
|     ) |     ) | ||||||
|     .execute(pool) |     .execute(transaction.executor()) | ||||||
|     .await |     .await | ||||||
|     { |     { | ||||||
|         Ok(_) => sqlx::query_as_unchecked!( |         Ok(_) => sqlx::query_as_unchecked!( | ||||||
| @@ -566,7 +574,7 @@ pub async fn create_reminder( | |||||||
|             WHERE uid = ?", |             WHERE uid = ?", | ||||||
|             new_uid |             new_uid | ||||||
|         ) |         ) | ||||||
|         .fetch_one(pool) |         .fetch_one(transaction.executor()) | ||||||
|         .await |         .await | ||||||
|         .map(|r| Ok(json!(r))) |         .map(|r| Ok(json!(r))) | ||||||
|         .unwrap_or_else(|e| { |         .unwrap_or_else(|e| { | ||||||
| @@ -586,11 +594,11 @@ pub async fn create_reminder( | |||||||
| async fn create_database_channel( | async fn create_database_channel( | ||||||
|     ctx: impl AsRef<Http>, |     ctx: impl AsRef<Http>, | ||||||
|     channel: ChannelId, |     channel: ChannelId, | ||||||
|     pool: impl Executor<'_, Database = Database> + Copy, |     transaction: &mut Transaction<'_>, | ||||||
| ) -> Result<u32, crate::Error> { | ) -> Result<u32, crate::Error> { | ||||||
|     let row = |     let row = | ||||||
|         sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0) |         sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0) | ||||||
|             .fetch_one(pool) |             .fetch_one(transaction.executor()) | ||||||
|             .await; |             .await; | ||||||
|  |  | ||||||
|     match row { |     match row { | ||||||
| @@ -607,7 +615,7 @@ async fn create_database_channel( | |||||||
|                     webhook.token, |                     webhook.token, | ||||||
|                     channel.0 |                     channel.0 | ||||||
|                 ) |                 ) | ||||||
|                 .execute(pool) |                 .execute(transaction.executor()) | ||||||
|                 .await |                 .await | ||||||
|                 .map_err(|e| Error::SQLx(e))?; |                 .map_err(|e| Error::SQLx(e))?; | ||||||
|             } |             } | ||||||
| @@ -633,7 +641,7 @@ async fn create_database_channel( | |||||||
|                 webhook.token, |                 webhook.token, | ||||||
|                 channel.0 |                 channel.0 | ||||||
|             ) |             ) | ||||||
|             .execute(pool) |             .execute(transaction.executor()) | ||||||
|             .await |             .await | ||||||
|             .map_err(|e| Error::SQLx(e))?; |             .map_err(|e| Error::SQLx(e))?; | ||||||
|  |  | ||||||
| @@ -644,7 +652,7 @@ async fn create_database_channel( | |||||||
|     }?; |     }?; | ||||||
|  |  | ||||||
|     let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.0) |     let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.0) | ||||||
|         .fetch_one(pool) |         .fetch_one(transaction.executor()) | ||||||
|         .await |         .await | ||||||
|         .map_err(|e| Error::SQLx(e))?; |         .map_err(|e| Error::SQLx(e))?; | ||||||
|  |  | ||||||
| @@ -652,20 +660,26 @@ async fn create_database_channel( | |||||||
| } | } | ||||||
|  |  | ||||||
| #[get("/")] | #[get("/")] | ||||||
| pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<NamedFile, Redirect> { | ||||||
|     if cookies.get_private("userid").is_some() { |     if cookies.get_private("userid").is_some() { | ||||||
|         let map: HashMap<&str, String> = HashMap::new(); |         NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| { | ||||||
|         Ok(Template::render("dashboard", &map)) |             warn!("Couldn't render dashboard: {:?}", e); | ||||||
|  |  | ||||||
|  |             Redirect::to("/login/discord") | ||||||
|  |         }) | ||||||
|     } else { |     } else { | ||||||
|         Err(Redirect::to("/login/discord")) |         Err(Redirect::to("/login/discord")) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[get("/<_>")] | #[get("/<_..>")] | ||||||
| pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<NamedFile, Redirect> { | ||||||
|     if cookies.get_private("userid").is_some() { |     if cookies.get_private("userid").is_some() { | ||||||
|         let map: HashMap<&str, String> = HashMap::new(); |         NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| { | ||||||
|         Ok(Template::render("dashboard", &map)) |             warn!("Couldn't render dashboard: {:?}", e); | ||||||
|  |  | ||||||
|  |             Redirect::to("/login/discord") | ||||||
|  |         }) | ||||||
|     } else { |     } else { | ||||||
|         Err(Redirect::to("/login/discord")) |         Err(Redirect::to("/login/discord")) | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,172 +0,0 @@ | |||||||
| use std::env; |  | ||||||
|  |  | ||||||
| use chrono_tz::Tz; |  | ||||||
| use reqwest::Client; |  | ||||||
| use rocket::{ |  | ||||||
|     http::CookieJar, |  | ||||||
|     serde::json::{json, Json, Value as JsonValue}, |  | ||||||
|     State, |  | ||||||
| }; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
| use serenity::{ |  | ||||||
|     client::Context, |  | ||||||
|     model::{ |  | ||||||
|         id::{GuildId, RoleId}, |  | ||||||
|         permissions::Permissions, |  | ||||||
|     }, |  | ||||||
| }; |  | ||||||
| use sqlx::{MySql, Pool}; |  | ||||||
|  |  | ||||||
| use crate::consts::DISCORD_API; |  | ||||||
|  |  | ||||||
| #[derive(Serialize)] |  | ||||||
| struct UserInfo { |  | ||||||
|     name: String, |  | ||||||
|     patreon: bool, |  | ||||||
|     timezone: Option<String>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Deserialize)] |  | ||||||
| pub struct UpdateUser { |  | ||||||
|     timezone: String, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Serialize)] |  | ||||||
| struct GuildInfo { |  | ||||||
|     id: String, |  | ||||||
|     name: String, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Deserialize)] |  | ||||||
| pub struct PartialGuild { |  | ||||||
|     pub id: GuildId, |  | ||||||
|     pub icon: Option<String>, |  | ||||||
|     pub name: String, |  | ||||||
|     #[serde(default)] |  | ||||||
|     pub owner: bool, |  | ||||||
|     #[serde(rename = "permissions_new")] |  | ||||||
|     pub permissions: Option<String>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/api/user")] |  | ||||||
| pub async fn get_user_info( |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonValue { |  | ||||||
|     offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None })); |  | ||||||
|  |  | ||||||
|     if let Some(user_id) = |  | ||||||
|         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() |  | ||||||
|     { |  | ||||||
|         let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) |  | ||||||
|             .member(&ctx.inner(), user_id) |  | ||||||
|             .await; |  | ||||||
|  |  | ||||||
|         let timezone = sqlx::query!( |  | ||||||
|             "SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?", |  | ||||||
|             user_id |  | ||||||
|         ) |  | ||||||
|         .fetch_one(pool.inner()) |  | ||||||
|         .await |  | ||||||
|         .map_or(None, |q| Some(q.timezone)); |  | ||||||
|  |  | ||||||
|         let user_info = UserInfo { |  | ||||||
|             name: cookies |  | ||||||
|                 .get_private("username") |  | ||||||
|                 .map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()), |  | ||||||
|             patreon: member_res.map_or(false, |member| { |  | ||||||
|                 member |  | ||||||
|                     .roles |  | ||||||
|                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) |  | ||||||
|             }), |  | ||||||
|             timezone, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         json!(user_info) |  | ||||||
|     } else { |  | ||||||
|         json!({"error": "Not authorized"}) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[patch("/api/user", data = "<user>")] |  | ||||||
| pub async fn update_user_info( |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     user: Json<UpdateUser>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonValue { |  | ||||||
|     if let Some(user_id) = |  | ||||||
|         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() |  | ||||||
|     { |  | ||||||
|         if user.timezone.parse::<Tz>().is_ok() { |  | ||||||
|             let _ = sqlx::query!( |  | ||||||
|                 "UPDATE users SET timezone = ? WHERE user = ?", |  | ||||||
|                 user.timezone, |  | ||||||
|                 user_id, |  | ||||||
|             ) |  | ||||||
|             .execute(pool.inner()) |  | ||||||
|             .await; |  | ||||||
|  |  | ||||||
|             json!({}) |  | ||||||
|         } else { |  | ||||||
|             json!({"error": "Timezone not recognized"}) |  | ||||||
|         } |  | ||||||
|     } else { |  | ||||||
|         json!({"error": "Not authorized"}) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/api/user/guilds")] |  | ||||||
| pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue { |  | ||||||
|     offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }])); |  | ||||||
|  |  | ||||||
|     if let Some(access_token) = cookies.get_private("access_token") { |  | ||||||
|         let request_res = reqwest_client |  | ||||||
|             .get(format!("{}/users/@me/guilds", DISCORD_API)) |  | ||||||
|             .bearer_auth(access_token.value()) |  | ||||||
|             .send() |  | ||||||
|             .await; |  | ||||||
|  |  | ||||||
|         match request_res { |  | ||||||
|             Ok(response) => { |  | ||||||
|                 let guilds_res = response.json::<Vec<PartialGuild>>().await; |  | ||||||
|  |  | ||||||
|                 match guilds_res { |  | ||||||
|                     Ok(guilds) => { |  | ||||||
|                         let reduced_guilds = guilds |  | ||||||
|                             .iter() |  | ||||||
|                             .filter(|g| { |  | ||||||
|                                 g.owner |  | ||||||
|                                     || g.permissions.as_ref().map_or(false, |p| { |  | ||||||
|                                         let permissions = |  | ||||||
|                                             Permissions::from_bits_truncate(p.parse().unwrap()); |  | ||||||
|  |  | ||||||
|                                         permissions.manage_messages() |  | ||||||
|                                             || permissions.manage_guild() |  | ||||||
|                                             || permissions.administrator() |  | ||||||
|                                     }) |  | ||||||
|                             }) |  | ||||||
|                             .map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() }) |  | ||||||
|                             .collect::<Vec<GuildInfo>>(); |  | ||||||
|  |  | ||||||
|                         json!(reduced_guilds) |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     Err(e) => { |  | ||||||
|                         warn!("Error constructing user from request: {:?}", e); |  | ||||||
|  |  | ||||||
|                         json!({"error": "Could not get user details"}) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             Err(e) => { |  | ||||||
|                 warn!("Error getting user guilds: {:?}", e); |  | ||||||
|  |  | ||||||
|                 json!({"error": "Could not reach Discord"}) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } else { |  | ||||||
|         json!({"error": "Not authorized"}) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										18
									
								
								web/src/routes/metrics.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								web/src/routes/metrics.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | use prometheus; | ||||||
|  |  | ||||||
|  | use crate::metrics::REGISTRY; | ||||||
|  |  | ||||||
|  | #[get("/metrics")] | ||||||
|  | pub async fn metrics() -> String { | ||||||
|  |     let encoder = prometheus::TextEncoder::new(); | ||||||
|  |     let res_custom = encoder.encode_to_string(®ISTRY.gather()); | ||||||
|  |  | ||||||
|  |     match res_custom { | ||||||
|  |         Ok(s) => s, | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Error encoding metrics: {:?}", e); | ||||||
|  |  | ||||||
|  |             String::new() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| pub mod admin; | pub mod admin; | ||||||
| pub mod dashboard; | pub mod dashboard; | ||||||
| pub mod login; | pub mod login; | ||||||
|  | pub mod metrics; | ||||||
| pub mod report; | pub mod report; | ||||||
|  |  | ||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
|   | |||||||
| @@ -15,6 +15,22 @@ div.reminderContent.is-collapsed .column.settings { | |||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | div.reminderContent.is-collapsed .reminder-settings { | ||||||
|  |     margin-bottom: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderContent.is-collapsed .button-row { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderContent.is-collapsed .button-row-edit { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderContent.is-collapsed .reminder-topbar { | ||||||
|  |     padding-bottom: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
| div.reminderContent.is-collapsed .invert-collapses { | div.reminderContent.is-collapsed .invert-collapses { | ||||||
|     display: inline-flex; |     display: inline-flex; | ||||||
| } | } | ||||||
| @@ -129,6 +145,12 @@ div.split-controls { | |||||||
|     margin-top: 0 !important; |     margin-top: 0 !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .reminder-settings > .column { | ||||||
|  |     flex-grow: 0; | ||||||
|  |     flex-shrink: 0; | ||||||
|  |     flex-basis: 50%; | ||||||
|  | } | ||||||
|  |  | ||||||
| div.reminderContent { | div.reminderContent { | ||||||
|     margin-top: 10px; |     margin-top: 10px; | ||||||
|     margin-bottom: 10px; |     margin-bottom: 10px; | ||||||
| @@ -196,17 +218,16 @@ div.inset-content { | |||||||
|     margin-right: 10%; |     margin-right: 10%; | ||||||
| } | } | ||||||
|  |  | ||||||
| div.flash-message { | div.flash-container { | ||||||
|     position: fixed; |     position: fixed; | ||||||
|  |     width: 100%; | ||||||
|  |     bottom: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.flash-message { | ||||||
|     width: calc(100% - 32px); |     width: calc(100% - 32px); | ||||||
|     margin: 16px !important; |     margin: 16px !important; | ||||||
|     z-index: 99; |     z-index: 99; | ||||||
|     bottom: 0; |  | ||||||
|     display: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| div.flash-message.is-active { |  | ||||||
|     display: block; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| body { | body { | ||||||
| @@ -286,17 +307,24 @@ div.dashboard-sidebar { | |||||||
|     padding-right: 0; |     padding-right: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| div.dashboard-sidebar:not(.mobile-sidebar) { | ul.guildList { | ||||||
|     display: flex; |     flex-grow: 1; | ||||||
|     flex-direction: column; |     flex-shrink: 1; | ||||||
|  |     overflow: auto; | ||||||
| } | } | ||||||
|  |  | ||||||
| div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | ||||||
|  |     flex-shrink: 0; | ||||||
|  |     flex-grow: 0; | ||||||
|     position: fixed; |     position: fixed; | ||||||
|     bottom: 0; |     bottom: 0; | ||||||
|     width: 226px; |     width: 226px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | div.dashboard-sidebar svg { | ||||||
|  |     flex-shrink: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
| div.mobile-sidebar { | div.mobile-sidebar { | ||||||
|     z-index: 100; |     z-index: 100; | ||||||
|     min-height: 100vh; |     min-height: 100vh; | ||||||
| @@ -444,8 +472,7 @@ input.default-width { | |||||||
| .customizable.is-400x300 img { | .customizable.is-400x300 img { | ||||||
|     margin-top: 10px; |     margin-top: 10px; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|     min-height: 100px; |     height: 100px; | ||||||
|     max-height: 400px; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .customizable.is-32x32 img { | .customizable.is-32x32 img { | ||||||
| @@ -589,6 +616,14 @@ input.default-width { | |||||||
|     border-bottom: 1px solid #fff; |     border-bottom: 1px solid #fff; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .channel-selector { | ||||||
|  |     width: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .select { | ||||||
|  |     width: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
| li.highlight { | li.highlight { | ||||||
|     margin-bottom: 0 !important; |     margin-bottom: 0 !important; | ||||||
| } | } | ||||||
| @@ -597,6 +632,10 @@ li.highlight { | |||||||
|     display: flex; |     display: flex; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .button-row-edit > button { | ||||||
|  |     margin-right: 4px; | ||||||
|  | } | ||||||
|  |  | ||||||
| .button-row .button-row-reminder { | .button-row .button-row-reminder { | ||||||
|     flex-grow: 0; |     flex-grow: 0; | ||||||
|     padding: 2px; |     padding: 2px; | ||||||
| @@ -612,7 +651,22 @@ li.highlight { | |||||||
|     padding: 2px; |     padding: 2px; | ||||||
| } | } | ||||||
|  |  | ||||||
| @media only screen and (max-width: 1408px) { | @media only screen and (max-width: 1023px) { | ||||||
|  |     p.title.pageTitle { | ||||||
|  |         display: none; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .dashboard-frame { | ||||||
|  |         margin-top: 4rem !important; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .customizable.thumbnail img { | ||||||
|  |         width: 60px; | ||||||
|  |         height: 60px; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media only screen and (max-width: 768px) { | ||||||
|     .button-row { |     .button-row { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
| @@ -630,37 +684,13 @@ li.highlight { | |||||||
|     .button-row button { |     .button-row button { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|     } |     } | ||||||
| } |  | ||||||
|  |  | ||||||
| @media only screen and (max-width: 768px) { |     .reminder-settings { | ||||||
|     .button-row-edit { |         margin-bottom: 0 !important; | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .button-row-edit > button { |     .tts-row { | ||||||
|         width: 100%; |         padding-bottom: 0; | ||||||
|         margin: 4px; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     p.title.pageTitle { |  | ||||||
|         display: none; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     .dashboard-frame { |  | ||||||
|         margin-top: 4rem !important; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media only screen and (max-width: 768px) { |  | ||||||
|     .customizable.thumbnail img { |  | ||||||
|         width: 60px; |  | ||||||
|         height: 60px; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     .customizable.is-24x24 img { |  | ||||||
|         width: 16px; |  | ||||||
|         height: 16px; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -679,6 +709,86 @@ li.highlight { | |||||||
|  |  | ||||||
| /* END */ | /* END */ | ||||||
|  |  | ||||||
|  | div.reminderError { | ||||||
|  |     margin: 10px; | ||||||
|  |     padding: 14px; | ||||||
|  |     background-color: #f5f5f5; | ||||||
|  |     border-radius: 8px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError .errorHead { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: row; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError .errorIcon { | ||||||
|  |     padding: 8px; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     margin-right: 12px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError .errorIcon .fas { | ||||||
|  |     display: none | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError[data-case="deleted"] .errorIcon { | ||||||
|  |     background-color: #e7e5e4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError[data-case="failed"] .errorIcon { | ||||||
|  |     background-color: #fecaca; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError[data-case="sent"] .errorIcon { | ||||||
|  |     background-color: #d9f99d; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash { | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle { | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError[data-case="sent"] .errorIcon .fas.fa-check { | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError .errorHead .reminderName { | ||||||
|  |     font-size: 1rem; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     justify-content: center; | ||||||
|  |     color: rgb(54, 54, 54); | ||||||
|  |     flex-grow: 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError .errorHead .reminderTime { | ||||||
|  |     font-size: 1rem; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     flex-shrink: 1; | ||||||
|  |     justify-content: center; | ||||||
|  |     color: rgb(54, 54, 54); | ||||||
|  |     background-color: #ffffff; | ||||||
|  |     padding: 8px; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     border-color: #e5e5e5; | ||||||
|  |     border-width: 1px; | ||||||
|  |     border-style: solid; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.reminderError .reminderMessage { | ||||||
|  |     font-size: 1rem; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     justify-content: center; | ||||||
|  |     color: rgb(54, 54, 54); | ||||||
|  |     flex-grow: 1; | ||||||
|  |     font-style: italic; | ||||||
|  | } | ||||||
|  |  | ||||||
| /* other stuff */ | /* other stuff */ | ||||||
|  |  | ||||||
| .half-rem { | .half-rem { | ||||||
| @@ -716,6 +826,18 @@ a.switch-pane { | |||||||
|     text-overflow: ellipsis; |     text-overflow: ellipsis; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .guild-submenu { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .guild-submenu li { | ||||||
|  |     font-size: 0.8rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | a.switch-pane.is-active ~ .guild-submenu { | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  |  | ||||||
| .feedback { | .feedback { | ||||||
|     background-color: #5865F2; |     background-color: #5865F2; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -33,7 +33,16 @@ let globalPatreon = false; | |||||||
| let guildPatreon = false; | let guildPatreon = false; | ||||||
|  |  | ||||||
| function guildId() { | function guildId() { | ||||||
|     return document.querySelector(".guildList a.is-active").dataset["guild"]; |     return window.location.pathname.match(/dashboard\/(\d+)/)[1]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function pane() { | ||||||
|  |     const match = window.location.pathname.match(/dashboard\/\d+\/(.+)/); | ||||||
|  |     if (match === null) { | ||||||
|  |         return null; | ||||||
|  |     } else { | ||||||
|  |         return match[1]; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| function colorToInt(r, g, b) { | function colorToInt(r, g, b) { | ||||||
| @@ -96,7 +105,7 @@ function reset_guild_pane() { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function fetch_patreon(guild_id) { | async function fetch_patreon(guild_id) { | ||||||
|     fetch(`/dashboard/api/guild/${guild_id}/patreon`) |     fetch(`/dashboard/api/guild/${guild_id}`) | ||||||
|         .then((response) => response.json()) |         .then((response) => response.json()) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|             if (data.error) { |             if (data.error) { | ||||||
| @@ -223,11 +232,10 @@ async function fetch_reminders(guild_id) { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function serialize_reminder(node, mode) { | async function serialize_reminder(node, mode) { | ||||||
|     let interval, utc_time, expiration_time; |     let utc_time, expiration_time; | ||||||
|  |     let interval = get_interval(node); | ||||||
|  |  | ||||||
|     if (mode !== "template") { |     if (mode !== "template") { | ||||||
|         interval = get_interval(node); |  | ||||||
|  |  | ||||||
|         utc_time = luxon.DateTime.fromISO( |         utc_time = luxon.DateTime.fromISO( | ||||||
|             node.querySelector('input[name="time"]').value |             node.querySelector('input[name="time"]').value | ||||||
|         ).setZone("UTC"); |         ).setZone("UTC"); | ||||||
| @@ -356,9 +364,9 @@ async function serialize_reminder(node, mode) { | |||||||
|         embed_title: embed_title, |         embed_title: embed_title, | ||||||
|         embed_fields: fields, |         embed_fields: fields, | ||||||
|         expires: expiration_time, |         expires: expiration_time, | ||||||
|         interval_seconds: mode !== "template" ? interval.seconds : null, |         interval_seconds: interval.seconds, | ||||||
|         interval_days: mode !== "template" ? interval.days : null, |         interval_days: interval.days, | ||||||
|         interval_months: mode !== "template" ? interval.months : null, |         interval_months: interval.months, | ||||||
|         name: node.querySelector('input[name="name"]').value, |         name: node.querySelector('input[name="name"]').value, | ||||||
|         tts: node.querySelector('input[name="tts"]').checked, |         tts: node.querySelector('input[name="tts"]').checked, | ||||||
|         username: node.querySelector('input[name="username"]').value, |         username: node.querySelector('input[name="username"]').value, | ||||||
| @@ -420,9 +428,9 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
|             .insertBefore(embed_field, lastChild); |             .insertBefore(embed_field, lastChild); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (mode !== "template") { |     if (reminder["interval_seconds"]) update_interval(frame); | ||||||
|         if (reminder["interval_seconds"]) update_interval(frame); |  | ||||||
|  |  | ||||||
|  |     if (mode !== "template") { | ||||||
|         let $enableBtn = frame.querySelector(".disable-enable"); |         let $enableBtn = frame.querySelector(".disable-enable"); | ||||||
|         $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable"; |         $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable"; | ||||||
|  |  | ||||||
| @@ -455,15 +463,17 @@ document.addEventListener("guildSwitched", async (e) => { | |||||||
|  |  | ||||||
|     let hasError = false; |     let hasError = false; | ||||||
|  |  | ||||||
|     if ($anchor === null) { |     if (pane() === null) { | ||||||
|         switch_pane("user-error"); |         window.history.replaceState({}, "", `/dashboard/${guildId()}/reminders`); | ||||||
|         hasError = true; |     } | ||||||
|         return; |  | ||||||
|  |     switch_pane(pane()); | ||||||
|  |  | ||||||
|  |     if ($anchor !== null) { | ||||||
|  |         $anchor.classList.add("is-active"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     switch_pane($anchor.dataset["pane"]); |  | ||||||
|     reset_guild_pane(); |     reset_guild_pane(); | ||||||
|     $anchor.classList.add("is-active"); |  | ||||||
|  |  | ||||||
|     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { |     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { | ||||||
|         document |         document | ||||||
| @@ -604,6 +614,16 @@ function show_error(error) { | |||||||
|     }, 5000); |     }, 5000); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function show_success(error) { | ||||||
|  |     document.getElementById("success").querySelector("span.success-message").textContent = | ||||||
|  |         error; | ||||||
|  |     document.getElementById("success").classList.add("is-active"); | ||||||
|  |  | ||||||
|  |     window.setTimeout(() => { | ||||||
|  |         document.getElementById("success").classList.remove("is-active"); | ||||||
|  |     }, 5000); | ||||||
|  | } | ||||||
|  |  | ||||||
| $colorPickerInput.value = colorPicker.color.hexString; | $colorPickerInput.value = colorPicker.color.hexString; | ||||||
|  |  | ||||||
| $colorPickerInput.addEventListener("input", () => { | $colorPickerInput.addEventListener("input", () => { | ||||||
| @@ -686,11 +706,15 @@ document.addEventListener("DOMContentLoaded", async () => { | |||||||
|                         ); |                         ); | ||||||
|                         $anchor.dataset["guild"] = guild.id; |                         $anchor.dataset["guild"] = guild.id; | ||||||
|                         $anchor.dataset["name"] = guild.name; |                         $anchor.dataset["name"] = guild.name; | ||||||
|                         $anchor.href = `/dashboard/${guild.id}?name=${guild.name}`; |                         $anchor.href = `/dashboard/${guild.id}/reminders`; | ||||||
|  |  | ||||||
|                         $anchor.addEventListener("click", async (e) => { |                         $anchor.addEventListener("click", async (e) => { | ||||||
|                             e.preventDefault(); |                             e.preventDefault(); | ||||||
|                             window.history.pushState({}, "", `/dashboard/${guild.id}`); |                             window.history.pushState( | ||||||
|  |                                 {}, | ||||||
|  |                                 "", | ||||||
|  |                                 `/dashboard/${guild.id}/reminders` | ||||||
|  |                             ); | ||||||
|                             const event = new CustomEvent("guildSwitched", { |                             const event = new CustomEvent("guildSwitched", { | ||||||
|                                 detail: { |                                 detail: { | ||||||
|                                     guild_name: guild.name, |                                     guild_name: guild.name, | ||||||
| @@ -754,12 +778,26 @@ $uploader.addEventListener("change", (ev) => { | |||||||
|         fileReader.onload = (e) => resolve(fileReader.result); |         fileReader.onload = (e) => resolve(fileReader.result); | ||||||
|         fileReader.readAsDataURL($uploader.files[0]); |         fileReader.readAsDataURL($uploader.files[0]); | ||||||
|     }).then((dataUrl) => { |     }).then((dataUrl) => { | ||||||
|  |         $importBtn.setAttribute("disabled", true); | ||||||
|  |  | ||||||
|         fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, { |         fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, { | ||||||
|             method: "PUT", |             method: "PUT", | ||||||
|             body: JSON.stringify({ body: dataUrl.split(",")[1] }), |             body: JSON.stringify({ body: dataUrl.split(",")[1] }), | ||||||
|         }).then(() => { |         }) | ||||||
|             delete $uploader.files[0]; |             .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/bulma.min.css"> | ||||||
|     <link rel="stylesheet" href="/static/css/fa.css"> |     <link rel="stylesheet" href="/static/css/fa.css"> | ||||||
|     <link rel="stylesheet" href="/static/css/font.css"> |     <link rel="stylesheet" href="/static/css/font.css"> | ||||||
|     <link rel="stylesheet" href="/static/css/style.css"> |     <link rel="stylesheet" href="/static/css/style.css?v{{ version }}"> | ||||||
|     <link rel="stylesheet" href="/static/css/dtsel.css"> |     <link rel="stylesheet" href="/static/css/dtsel.css"> | ||||||
|     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tributejs/5.1.3/tribute.css" integrity="sha512-GnwBnXd+ZGO9CdP343MUr0jCcJXCr++JVtQRnllexRW2IDq4Zvrh/McTQjooAKnSUbXZ7wamp7AQSweTnfMVoA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> |     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tributejs/5.1.3/tribute.css" integrity="sha512-GnwBnXd+ZGO9CdP343MUr0jCcJXCr++JVtQRnllexRW2IDq4Zvrh/McTQjooAKnSUbXZ7wamp7AQSweTnfMVoA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | ||||||
|  |  | ||||||
| @@ -40,7 +40,7 @@ | |||||||
|     <div class="navbar-brand"> |     <div class="navbar-brand"> | ||||||
|         <a class="navbar-item" href="/"> |         <a class="navbar-item" href="/"> | ||||||
|             <figure class="image"> |             <figure class="image"> | ||||||
|                 <img src="/static/img/logo_nobg.webp" alt="Reminder Bot Logo"> |                 <img width="28px" height="28px" src="/static/img/logo_nobg.webp" alt="Reminder Bot Logo"> | ||||||
|             </figure> |             </figure> | ||||||
|         </a> |         </a> | ||||||
|  |  | ||||||
| @@ -76,6 +76,10 @@ | |||||||
|     <span class="icon"><i class="far fa-exclamation-circle"></i></span> <span class="error-message"></span> |     <span class="icon"><i class="far fa-exclamation-circle"></i></span> <span class="error-message"></span> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | <div class="notification is-success flash-message" id="success"> | ||||||
|  |     <span class="icon"><i class="far fa-check"></i></span> <span class="success-message"></span> | ||||||
|  | </div> | ||||||
|  |  | ||||||
| <div class="modal" id="addImageModal"> | <div class="modal" id="addImageModal"> | ||||||
|     <div class="modal-background"></div> |     <div class="modal-background"></div> | ||||||
|     <div class="modal-card"> |     <div class="modal-card"> | ||||||
| @@ -185,14 +189,6 @@ | |||||||
|                     </label> |                     </label> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="control"> |  | ||||||
|                 <div class="field"> |  | ||||||
|                     <label> |  | ||||||
|                         <input type="radio" class="default-width" name="exportSelect" value="todos"> |  | ||||||
|                         Todo Lists |  | ||||||
|                     </label> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <br> |             <br> | ||||||
|             <div class="has-text-centered"> |             <div class="has-text-centered"> | ||||||
|                 <div style="color: red"> |                 <div style="color: red"> | ||||||
| @@ -234,6 +230,7 @@ | |||||||
|         <a href="/"> |         <a href="/"> | ||||||
|             <div class="brand"> |             <div class="brand"> | ||||||
|                 <img src="/static/img/logo_nobg.webp" alt="Reminder bot logo" |                 <img src="/static/img/logo_nobg.webp" alt="Reminder bot logo" | ||||||
|  |                      width="52px" height="52px" | ||||||
|                      class="dashboard-brand"> |                      class="dashboard-brand"> | ||||||
|             </div> |             </div> | ||||||
|         </a> |         </a> | ||||||
| @@ -256,11 +253,9 @@ | |||||||
|                 </p> |                 </p> | ||||||
|                 <ul class="menu-list"> |                 <ul class="menu-list"> | ||||||
|                     <li> |                     <li> | ||||||
|                         {# |  | ||||||
|                         <a class="show-modal" data-modal="dataManagerModal"> |                         <a class="show-modal" data-modal="dataManagerModal"> | ||||||
|                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export |                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export | ||||||
|                         </a> |                         </a> | ||||||
|                         #} |  | ||||||
|                         <a class="show-modal" data-modal="chooseTimezoneModal"> |                         <a class="show-modal" data-modal="chooseTimezoneModal"> | ||||||
|                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone |                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone | ||||||
|                         </a> |                         </a> | ||||||
| @@ -302,11 +297,9 @@ | |||||||
|                 </p> |                 </p> | ||||||
|                 <ul class="menu-list"> |                 <ul class="menu-list"> | ||||||
|                     <li> |                     <li> | ||||||
|                         {# |  | ||||||
|                         <a class="show-modal" data-modal="dataManagerModal"> |                         <a class="show-modal" data-modal="dataManagerModal"> | ||||||
|                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export |                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export | ||||||
|                         </a> |                         </a> | ||||||
|                         #} |  | ||||||
|                         <a class="show-modal" data-modal="chooseTimezoneModal"> |                         <a class="show-modal" data-modal="chooseTimezoneModal"> | ||||||
|                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone |                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone | ||||||
|                         </a> |                         </a> | ||||||
| @@ -332,7 +325,7 @@ | |||||||
|                 <p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p> |                 <p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p> | ||||||
|             </div> |             </div> | ||||||
|         </section> |         </section> | ||||||
|         <section id="guild" class="is-hidden"> |         <section id="reminders" class="is-hidden"> | ||||||
|             {% include "reminder_dashboard/reminder_dashboard" %} |             {% include "reminder_dashboard/reminder_dashboard" %} | ||||||
|         </section> |         </section> | ||||||
|         <section id="reminder-errors" class="is-hidden"> |         <section id="reminder-errors" class="is-hidden"> | ||||||
| @@ -388,9 +381,9 @@ | |||||||
| <script src="/static/js/iro.js"></script> | <script src="/static/js/iro.js"></script> | ||||||
| <script src="/static/js/dtsel.js"></script> | <script src="/static/js/dtsel.js"></script> | ||||||
|  |  | ||||||
| <script src="/static/js/interval.js"></script> | <script src="/static/js/interval.js?v{{ version }}"></script> | ||||||
| <script src="/static/js/timezone.js" defer></script> | <script src="/static/js/timezone.js?v{{ version }}" defer></script> | ||||||
| <script src="/static/js/main.js" defer></script> | <script src="/static/js/main.js?v{{ version }}" defer></script> | ||||||
|  |  | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
| @@ -133,32 +133,27 @@ | |||||||
|             </article> |             </article> | ||||||
|         </div> |         </div> | ||||||
|         <div class="column settings"> |         <div class="column settings"> | ||||||
|             <div class="columns"> |             <div class="field channel-field"> | ||||||
|                 <div class="column"> |                 <div class="collapses"> | ||||||
|                     <div class="field channel-field"> |                     <label class="label" for="channelOption">Channel*</label> | ||||||
|                         <div class="collapses"> |                 </div> | ||||||
|                             <label class="label" for="channelOption">Channel*</label> |                 <div class="control has-icons-left"> | ||||||
|                         </div> |                     <div class="select"> | ||||||
|                         <div class="control has-icons-left"> |                         <select name="channel" class="channel-selector"> | ||||||
|                             <div class="select"> |                         </select> | ||||||
|                                 <select name="channel" class="channel-selector"> |                     </div> | ||||||
|                                 </select> |                     <div class="icon is-small is-left"> | ||||||
|                             </div> |                         <i class="fas fa-hashtag"></i> | ||||||
|                             <div class="icon is-small is-left"> |  | ||||||
|                                 <i class="fas fa-hashtag"></i> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="column"> |             </div> | ||||||
|                     <div class="field"> |  | ||||||
|                         <div class="control"> |             <div class="field"> | ||||||
|                             <label class="label collapses"> |                 <div class="control"> | ||||||
|                                 Time* |                     <label class="label collapses"> | ||||||
|                                 <input class="input prefill-now" type="datetime-local" step="1" name="time"> |                         Time* | ||||||
|                             </label> |                         <input class="input prefill-now" type="datetime-local" step="1" name="time"> | ||||||
|                         </div> |                     </label> | ||||||
|                     </div> |  | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
| @@ -236,40 +231,39 @@ | |||||||
|                         </div> |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|  |  | ||||||
|                 {% if creating %} |  | ||||||
|                     <div class="button-row"> |  | ||||||
|                         <div class="button-row-reminder"> |  | ||||||
|                             <button class="button is-success" id="createReminder"> |  | ||||||
|                                 <span>Create Reminder</span> <span class="icon"><i class="fas fa-sparkles"></i></span> |  | ||||||
|                             </button> |  | ||||||
|                         </div> |  | ||||||
|                         <div class="button-row-template"> |  | ||||||
|                             <div> |  | ||||||
|                                 <button class="button is-success is-outlined" id="createTemplate"> |  | ||||||
|                                     <span>Create Template</span> <span class="icon"><i class="fas fa-file-spreadsheet"></i></span> |  | ||||||
|                                 </button> |  | ||||||
|                             </div> |  | ||||||
|                             <div> |  | ||||||
|                                 <button class="button is-outlined show-modal is-pulled-right" data-modal="chooseTemplateModal"> |  | ||||||
|                                     Load Template |  | ||||||
|                                 </button> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                     </div> |  | ||||||
|                 {% else %} |  | ||||||
|                     <div class="button-row-edit"> |  | ||||||
|                         <button class="button is-success save-btn"> |  | ||||||
|                             <span>Save</span> <span class="icon"><i class="fas fa-save"></i></span> |  | ||||||
|                         </button> |  | ||||||
|                         <button class="button is-warning disable-enable"> |  | ||||||
|                         </button> |  | ||||||
|                         <button class="button is-danger delete-reminder"> |  | ||||||
|                             Delete |  | ||||||
|                         </button> |  | ||||||
|                     </div> |  | ||||||
|                 {% endif %} |  | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|  |     {% if creating %} | ||||||
|  |         <div class="button-row"> | ||||||
|  |             <div class="button-row-reminder"> | ||||||
|  |                 <button class="button is-success" id="createReminder"> | ||||||
|  |                     <span>Create Reminder</span> <span class="icon"><i class="fas fa-sparkles"></i></span> | ||||||
|  |                 </button> | ||||||
|  |             </div> | ||||||
|  |             <div class="button-row-template"> | ||||||
|  |                 <div> | ||||||
|  |                     <button class="button is-success is-outlined" id="createTemplate"> | ||||||
|  |                         <span>Create Template</span> <span class="icon"><i class="fas fa-file-spreadsheet"></i></span> | ||||||
|  |                     </button> | ||||||
|  |                 </div> | ||||||
|  |                 <div> | ||||||
|  |                     <button class="button is-outlined show-modal is-pulled-right" data-modal="chooseTemplateModal"> | ||||||
|  |                         Load Template | ||||||
|  |                     </button> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     {% else %} | ||||||
|  |         <div class="button-row-edit"> | ||||||
|  |             <button class="button is-success save-btn"> | ||||||
|  |                 <span>Save</span> <span class="icon"><i class="fas fa-save"></i></span> | ||||||
|  |             </button> | ||||||
|  |             <button class="button is-warning disable-enable"> | ||||||
|  |             </button> | ||||||
|  |             <button class="button is-danger delete-reminder"> | ||||||
|  |                 Delete | ||||||
|  |             </button> | ||||||
|  |         </div> | ||||||
|  |     {% endif %} | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -2,4 +2,4 @@ | |||||||
|  |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <script src="/static/js/reminder_errors.js"></script> | <!--<script src="/static/js/reminder_errors.js"></script>--> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user