Compare commits
	
		
			11 Commits
		
	
	
		
			1.7.4
			...
			82dab53744
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 82dab53744 | |||
|  | 5f703e8538 | ||
|  | 2993505a47 | ||
|  | b225ad7e45 | ||
|  | ee89cb40c5 | ||
|  | b6b5e6d2b2 | ||
|  | adf29dca5d | ||
|  | ea3fe3f543 | ||
|  | 109cf16dbb | ||
|  | 6726ca0c2d | ||
| 38133be15d | 
							
								
								
									
										19
									
								
								migrations/20230812111348_orphan_reminders.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								migrations/20230812111348_orphan_reminders.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| -- Drop existing constraint | ||||
| ALTER TABLE `reminders` DROP CONSTRAINT `reminders_ibfk_1`; | ||||
|  | ||||
| ALTER TABLE `reminders` MODIFY COLUMN `channel_id` INT UNSIGNED; | ||||
| ALTER TABLE `reminders` ADD COLUMN `guild_id` INT UNSIGNED; | ||||
|  | ||||
| ALTER TABLE `reminders` | ||||
|     ADD CONSTRAINT `guild_id_fk` | ||||
|         FOREIGN KEY (`guild_id`) | ||||
|         REFERENCES `guilds`(`id`) | ||||
|         ON DELETE CASCADE; | ||||
|  | ||||
| ALTER TABLE `reminders` | ||||
|     ADD CONSTRAINT `channel_id_fk` | ||||
|         FOREIGN KEY (`channel_id`) | ||||
|         REFERENCES `channels`(`id`) | ||||
|         ON DELETE SET NULL; | ||||
|  | ||||
| UPDATE `reminders` SET `guild_id` = (SELECT guilds.`id` FROM `channels` INNER JOIN `guilds` ON channels.guild_id = guilds.id WHERE reminders.channel_id = channels.id); | ||||
							
								
								
									
										4
									
								
								migrations/20230903131153_reminder_status_timing.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								migrations/20230903131153_reminder_status_timing.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| ALTER TABLE reminders ADD COLUMN `status_change_time` DATETIME; | ||||
|  | ||||
| -- This is a best-guess as to the status change time. | ||||
| UPDATE reminders SET `status_change_time` = `utc_time`; | ||||
| @@ -310,6 +310,7 @@ WHERE | ||||
|             reminders | ||||
|         WHERE | ||||
|             reminders.`utc_time` <= NOW() AND | ||||
|             reminders.`channel_id` IS NOT NULL AND | ||||
|             `status` = 'pending' AND | ||||
|             ( | ||||
|                 reminders.`interval_seconds` IS NOT NULL | ||||
| @@ -471,7 +472,14 @@ WHERE | ||||
|     } | ||||
|  | ||||
|     async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||
|         sqlx::query!("UPDATE reminders SET `status` = 'sent' WHERE `id` = ?", self.id) | ||||
|         sqlx::query!( | ||||
|             " | ||||
|             UPDATE reminders | ||||
|             SET `status` = 'sent', `status_change_time` = NOW() | ||||
|             WHERE `id` = ? | ||||
|             ", | ||||
|             self.id | ||||
|         ) | ||||
|         .execute(pool) | ||||
|         .await | ||||
|         .expect(&format!("Could not delete Reminder {}", self.id)); | ||||
|   | ||||
| @@ -166,15 +166,21 @@ impl ComponentDataModel { | ||||
|                     .await; | ||||
|             } | ||||
|             ComponentDataModel::DelSelector(selector) => { | ||||
|                 let selected_id = component.data.values.join(","); | ||||
|                 for id in &component.data.values { | ||||
|                     match id.parse::<u32>() { | ||||
|                         Ok(id) => { | ||||
|                             if let Some(reminder) = Reminder::from_id(&data.database, id).await { | ||||
|                                 reminder.delete(&data.database).await.unwrap(); | ||||
|                             } else { | ||||
|                                 warn!("Attempt to delete non-existent reminder"); | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                 sqlx::query!( | ||||
|                     "UPDATE reminders SET `status` = 'deleted' WHERE FIND_IN_SET(id, ?)", | ||||
|                     selected_id | ||||
|                 ) | ||||
|                 .execute(&data.database) | ||||
|                 .await | ||||
|                 .unwrap(); | ||||
|                         Err(e) => { | ||||
|                             warn!("Error casting ID to integer: {:?}.", e); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 let reminders = Reminder::from_guild( | ||||
|                     &ctx, | ||||
|   | ||||
| @@ -10,6 +10,7 @@ pub struct ChannelData { | ||||
|     pub webhook_id: Option<u64>, | ||||
|     pub webhook_token: Option<String>, | ||||
|     pub paused: bool, | ||||
|     pub db_guild_id: Option<u32>, | ||||
|     pub paused_until: Option<NaiveDateTime>, | ||||
| } | ||||
|  | ||||
| @@ -22,7 +23,11 @@ impl ChannelData { | ||||
|  | ||||
|         if let Ok(c) = sqlx::query_as_unchecked!( | ||||
|             Self, | ||||
|             "SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?", | ||||
|             " | ||||
|             SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until, | ||||
|                 guild_id AS db_guild_id | ||||
|             FROM channels WHERE channel = ? | ||||
|             ", | ||||
|             channel_id | ||||
|         ) | ||||
|         .fetch_one(pool) | ||||
| @@ -30,12 +35,18 @@ impl ChannelData { | ||||
|         { | ||||
|             Ok(c) | ||||
|         } else { | ||||
|             let props = channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name)); | ||||
|             let props = | ||||
|                 channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name)); | ||||
|  | ||||
|             let (guild_id, channel_name) = if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) }; | ||||
|             let (guild_id, channel_name) = | ||||
|                 if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) }; | ||||
|  | ||||
|             sqlx::query!( | ||||
|                 "INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))", | ||||
|                 " | ||||
|                 INSERT IGNORE INTO channels | ||||
|                 (channel, name, guild_id) | ||||
|                 VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?)) | ||||
|                 ", | ||||
|                 channel_id, | ||||
|                 channel_name, | ||||
|                 guild_id | ||||
| @@ -46,7 +57,10 @@ impl ChannelData { | ||||
|             Ok(sqlx::query_as_unchecked!( | ||||
|                 Self, | ||||
|                 " | ||||
| SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ? | ||||
|                 SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, | ||||
|                     paused_until, guild_id AS db_guild_id | ||||
|                 FROM channels | ||||
|                 WHERE channel = ? | ||||
|                 ", | ||||
|                 channel_id | ||||
|             ) | ||||
| @@ -58,9 +72,10 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u | ||||
|     pub async fn commit_changes(&self, pool: &MySqlPool) { | ||||
|         sqlx::query!( | ||||
|             " | ||||
| UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until \ | ||||
|              = ? WHERE id = ? | ||||
|             ", | ||||
|             UPDATE channels | ||||
|             SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, | ||||
|                 paused = ?, paused_until = ? | ||||
|             WHERE id = ?", | ||||
|             self.name, | ||||
|             self.nudge, | ||||
|             self.blacklisted, | ||||
|   | ||||
| @@ -51,6 +51,7 @@ pub struct ReminderBuilder { | ||||
|     pool: MySqlPool, | ||||
|     uid: String, | ||||
|     channel: u32, | ||||
|     guild: Option<u32>, | ||||
|     thread_id: Option<u64>, | ||||
|     utc_time: NaiveDateTime, | ||||
|     timezone: String, | ||||
| @@ -86,6 +87,7 @@ impl ReminderBuilder { | ||||
| INSERT INTO reminders ( | ||||
|     `uid`, | ||||
|     `channel_id`, | ||||
|     `guild_id`, | ||||
|     `utc_time`, | ||||
|     `timezone`, | ||||
|     `interval_seconds`, | ||||
| @@ -110,11 +112,13 @@ INSERT INTO reminders ( | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ? | ||||
| ) | ||||
|             ", | ||||
|                         self.uid, | ||||
|                         self.channel, | ||||
|                         self.guild, | ||||
|                         utc_time, | ||||
|                         self.timezone, | ||||
|                         self.interval_seconds, | ||||
| @@ -247,10 +251,10 @@ impl<'a> MultiReminderBuilder<'a> { | ||||
|                                 { | ||||
|                                     Err(ReminderError::UserBlockedDm) | ||||
|                                 } else { | ||||
|                                     Ok(user_data.dm_channel) | ||||
|                                     Ok((user_data.dm_channel, None)) | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 Ok(user_data.dm_channel) | ||||
|                                 Ok((user_data.dm_channel, None)) | ||||
|                             } | ||||
|                         } else { | ||||
|                             Err(ReminderError::InvalidTag) | ||||
| @@ -297,13 +301,13 @@ impl<'a> MultiReminderBuilder<'a> { | ||||
|                                                 .commit_changes(&self.ctx.data().database) | ||||
|                                                 .await; | ||||
|  | ||||
|                                             Ok(channel_data.id) | ||||
|                                             Ok((channel_data.id, channel_data.db_guild_id)) | ||||
|                                         } | ||||
|  | ||||
|                                         Err(e) => Err(ReminderError::DiscordError(e.to_string())), | ||||
|                                     } | ||||
|                                 } else { | ||||
|                                     Ok(channel_data.id) | ||||
|                                     Ok((channel_data.id, channel_data.db_guild_id)) | ||||
|                                 } | ||||
|                             } | ||||
|                         } else { | ||||
| @@ -317,7 +321,8 @@ impl<'a> MultiReminderBuilder<'a> { | ||||
|                         let builder = ReminderBuilder { | ||||
|                             pool: self.ctx.data().database.clone(), | ||||
|                             uid: generate_uid(), | ||||
|                             channel: c, | ||||
|                             channel: c.0, | ||||
|                             guild: c.1, | ||||
|                             thread_id, | ||||
|                             utc_time: self.utc_time, | ||||
|                             timezone: self.timezone.to_string(), | ||||
|   | ||||
| @@ -304,7 +304,10 @@ WHERE | ||||
|         &self, | ||||
|         db: impl Executor<'_, Database = Database>, | ||||
|     ) -> Result<(), sqlx::Error> { | ||||
|         sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", self.uid) | ||||
|         sqlx::query!( | ||||
|             "UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?", | ||||
|             self.uid | ||||
|         ) | ||||
|         .execute(db) | ||||
|         .await | ||||
|         .map(|_| ()) | ||||
|   | ||||
| @@ -150,7 +150,8 @@ pub async fn initialize( | ||||
|         .mount( | ||||
|             "/dashboard", | ||||
|             routes![ | ||||
|                 routes::dashboard::dashboard, | ||||
|                 routes::dashboard::dashboard_1, | ||||
|                 routes::dashboard::dashboard_2, | ||||
|                 routes::dashboard::dashboard_home, | ||||
|                 routes::dashboard::user::get_user_info, | ||||
|                 routes::dashboard::user::update_user_info, | ||||
|   | ||||
| @@ -145,7 +145,7 @@ pub async fn import_reminders( | ||||
|                                     attachment: record.attachment, | ||||
|                                     attachment_name: record.attachment_name, | ||||
|                                     avatar: record.avatar, | ||||
|                                     channel: channel_id, | ||||
|                                     channel: Some(channel_id), | ||||
|                                     content: record.content, | ||||
|                                     embed_author: record.embed_author, | ||||
|                                     embed_author_url: record.embed_author_url, | ||||
| @@ -171,6 +171,8 @@ pub async fn import_reminders( | ||||
|                                     uid: generate_uid(), | ||||
|                                     username: record.username, | ||||
|                                     utc_time: record.utc_time, | ||||
|                                     status: "pending".to_string(), | ||||
|                                     status_change_time: None, | ||||
|                                 }; | ||||
|  | ||||
|                                 create_reminder( | ||||
|   | ||||
| @@ -318,30 +318,22 @@ pub async fn create_guild_reminder( | ||||
|     .await | ||||
| } | ||||
|  | ||||
| #[get("/api/guild/<id>/reminders")] | ||||
| #[get("/api/guild/<id>/reminders?<status>")] | ||||
| pub async fn get_reminders( | ||||
|     id: u64, | ||||
|     cookies: &CookieJar<'_>, | ||||
|     ctx: &State<Context>, | ||||
|     serenity_context: &State<Context>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
|     status: Option<String>, | ||||
| ) -> JsonResult { | ||||
|     check_authorization!(cookies, serenity_context.inner(), id); | ||||
|  | ||||
|     let channels_res = GuildId(id).channels(&ctx.inner()).await; | ||||
|  | ||||
|     match channels_res { | ||||
|         Ok(channels) => { | ||||
|             let channels = channels | ||||
|                 .keys() | ||||
|                 .into_iter() | ||||
|                 .map(|k| k.as_u64().to_string()) | ||||
|                 .collect::<Vec<String>>() | ||||
|                 .join(","); | ||||
|     let status = status.unwrap_or("pending".to_string()); | ||||
|  | ||||
|     sqlx::query_as_unchecked!( | ||||
|         Reminder, | ||||
|                 "SELECT | ||||
|         " | ||||
|         SELECT | ||||
|          reminders.attachment, | ||||
|          reminders.attachment_name, | ||||
|          reminders.avatar, | ||||
| @@ -367,11 +359,14 @@ pub async fn get_reminders( | ||||
|          reminders.tts, | ||||
|          reminders.uid, | ||||
|          reminders.username, | ||||
|                  reminders.utc_time | ||||
|          reminders.utc_time, | ||||
|          reminders.status, | ||||
|          reminders.status_change_time | ||||
|         FROM reminders | ||||
|         LEFT JOIN channels ON channels.id = reminders.channel_id | ||||
|                 WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)", | ||||
|                 channels | ||||
|         WHERE FIND_IN_SET(`status`, ?) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||
|         status, | ||||
|         id | ||||
|     ) | ||||
|     .fetch_all(pool.inner()) | ||||
|     .await | ||||
| @@ -381,13 +376,6 @@ pub async fn get_reminders( | ||||
|  | ||||
|         json_err!("Could not load reminders") | ||||
|     }) | ||||
|         } | ||||
|         Err(e) => { | ||||
|             warn!("Could not fetch channels from {}: {:?}", id, e); | ||||
|  | ||||
|             Ok(json!([])) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[patch("/api/guild/<id>/reminders", data = "<reminder>")] | ||||
| @@ -586,7 +574,9 @@ pub async fn edit_reminder( | ||||
|          reminders.tts, | ||||
|          reminders.uid, | ||||
|          reminders.username, | ||||
|          reminders.utc_time | ||||
|          reminders.utc_time, | ||||
|          reminders.status, | ||||
|          reminders.status_change_time | ||||
|         FROM reminders | ||||
|         LEFT JOIN channels ON channels.id = reminders.channel_id | ||||
|         WHERE uid = ?", | ||||
| @@ -610,7 +600,10 @@ pub async fn delete_reminder( | ||||
|     reminder: Json<DeleteReminder>, | ||||
|     pool: &State<Pool<MySql>>, | ||||
| ) -> JsonResult { | ||||
|     match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) | ||||
|     match sqlx::query!( | ||||
|         "UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?", | ||||
|         reminder.uid | ||||
|     ) | ||||
|     .execute(pool.inner()) | ||||
|     .await | ||||
|     { | ||||
|   | ||||
| @@ -124,8 +124,8 @@ pub struct Reminder { | ||||
|     attachment: Option<Vec<u8>>, | ||||
|     attachment_name: Option<String>, | ||||
|     avatar: Option<String>, | ||||
|     #[serde(with = "string")] | ||||
|     channel: u64, | ||||
|     #[serde(with = "string_opt")] | ||||
|     channel: Option<u64>, | ||||
|     content: String, | ||||
|     embed_author: String, | ||||
|     embed_author_url: Option<String>, | ||||
| @@ -150,6 +150,8 @@ pub struct Reminder { | ||||
|     uid: String, | ||||
|     username: Option<String>, | ||||
|     utc_time: NaiveDateTime, | ||||
|     status: String, | ||||
|     status_change_time: Option<NaiveDateTime>, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize)] | ||||
| @@ -308,6 +310,34 @@ mod string { | ||||
|     } | ||||
| } | ||||
|  | ||||
| mod string_opt { | ||||
|     use std::{fmt::Display, str::FromStr}; | ||||
|  | ||||
|     use serde::{de, Deserialize, Deserializer, Serializer}; | ||||
|  | ||||
|     pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error> | ||||
|     where | ||||
|         T: Display, | ||||
|         S: Serializer, | ||||
|     { | ||||
|         match value { | ||||
|             Some(value) => serializer.collect_str(value), | ||||
|             None => serializer.serialize_none(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error> | ||||
|     where | ||||
|         T: FromStr, | ||||
|         T::Err: Display, | ||||
|         D: Deserializer<'de>, | ||||
|     { | ||||
|         Option::deserialize(deserializer)? | ||||
|             .map(|d: String| d.parse().map_err(de::Error::custom)) | ||||
|             .transpose() | ||||
|     } | ||||
| } | ||||
|  | ||||
| mod base64s { | ||||
|     use serde::{de, Deserialize, Deserializer, Serializer}; | ||||
|  | ||||
| @@ -372,7 +402,7 @@ pub async fn create_reminder( | ||||
|     } | ||||
|  | ||||
|     // validate channel | ||||
|     let channel = ChannelId(reminder.channel).to_channel_cached(&ctx); | ||||
|     let channel = reminder.channel.map(|c| ChannelId(c).to_channel_cached(&ctx)).flatten(); | ||||
|     let channel_exists = channel.is_some(); | ||||
|  | ||||
|     let channel_matches_guild = | ||||
| @@ -380,14 +410,14 @@ pub async fn create_reminder( | ||||
|  | ||||
|     if !channel_matches_guild || !channel_exists { | ||||
|         warn!( | ||||
|             "Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})", | ||||
|             "Error in `create_reminder`: channel {:?} not found for guild {} (channel exists: {})", | ||||
|             reminder.channel, guild_id, channel_exists | ||||
|         ); | ||||
|  | ||||
|         return Err(json!({"error": "Channel not found"})); | ||||
|     } | ||||
|  | ||||
|     let channel = create_database_channel(&ctx, ChannelId(reminder.channel), pool).await; | ||||
|     let channel = create_database_channel(&ctx, ChannelId(reminder.channel.unwrap()), pool).await; | ||||
|  | ||||
|     if let Err(e) = channel { | ||||
|         warn!("`create_database_channel` returned an error code: {:?}", e); | ||||
| @@ -479,6 +509,7 @@ pub async fn create_reminder( | ||||
|          attachment, | ||||
|          attachment_name, | ||||
|          channel_id, | ||||
|          guild_id, | ||||
|          avatar, | ||||
|          content, | ||||
|          embed_author, | ||||
| @@ -501,11 +532,12 @@ pub async fn create_reminder( | ||||
|          tts, | ||||
|          username, | ||||
|          `utc_time` | ||||
|         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | ||||
|         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | ||||
|         new_uid, | ||||
|         attachment_data, | ||||
|         reminder.attachment_name, | ||||
|         channel, | ||||
|         guild_id.0, | ||||
|         reminder.avatar, | ||||
|         reminder.content, | ||||
|         reminder.embed_author, | ||||
| @@ -560,7 +592,9 @@ pub async fn create_reminder( | ||||
|              reminders.tts, | ||||
|              reminders.uid, | ||||
|              reminders.username, | ||||
|              reminders.utc_time | ||||
|              reminders.utc_time, | ||||
|              reminders.status, | ||||
|              reminders.status_change_time | ||||
|             FROM reminders | ||||
|             LEFT JOIN channels ON channels.id = reminders.channel_id | ||||
|             WHERE uid = ?", | ||||
| @@ -662,7 +696,17 @@ pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirec | ||||
| } | ||||
|  | ||||
| #[get("/<_>")] | ||||
| pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | ||||
| pub async fn dashboard_1(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | ||||
|     if cookies.get_private("userid").is_some() { | ||||
|         let map: HashMap<&str, String> = HashMap::new(); | ||||
|         Ok(Template::render("dashboard", &map)) | ||||
|     } else { | ||||
|         Err(Redirect::to("/login/discord")) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/<_>/<_>")] | ||||
| pub async fn dashboard_2(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | ||||
|     if cookies.get_private("userid").is_some() { | ||||
|         let map: HashMap<&str, String> = HashMap::new(); | ||||
|         Ok(Template::render("dashboard", &map)) | ||||
|   | ||||
| @@ -291,10 +291,19 @@ div.dashboard-sidebar:not(.mobile-sidebar) { | ||||
|     flex-direction: column; | ||||
| } | ||||
|  | ||||
| ul.guildList { | ||||
|     flex-grow: 1; | ||||
|     flex-shrink: 1; | ||||
|     overflow: scroll; | ||||
| } | ||||
|  | ||||
| div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | ||||
|     position: fixed; | ||||
|     bottom: 0; | ||||
|     width: 226px; | ||||
|     flex-shrink: 0; | ||||
|     flex-grow: 0; | ||||
| } | ||||
|  | ||||
| div.dashboard-sidebar svg { | ||||
|     flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| div.mobile-sidebar { | ||||
| @@ -679,6 +688,76 @@ li.highlight { | ||||
|  | ||||
| /* END */ | ||||
|  | ||||
| div.reminderError { | ||||
|     margin: 10px; | ||||
|     padding: 14px; | ||||
|     background-color: #f5f5f5; | ||||
|     border-radius: 8px; | ||||
| } | ||||
|  | ||||
| div.reminderError .errorHead { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
| } | ||||
|  | ||||
| div.reminderError .errorIcon { | ||||
|     padding: 8px; | ||||
|     border-radius: 4px; | ||||
|     margin-right: 12px; | ||||
| } | ||||
|  | ||||
| div.reminderError .errorIcon .fas { | ||||
|     display: none | ||||
| } | ||||
|  | ||||
| div.reminderError[data-case="deleted"] .errorIcon { | ||||
|     background-color: #e7e5e4; | ||||
| } | ||||
|  | ||||
| div.reminderError[data-case="failed"] .errorIcon { | ||||
|     background-color: #fecaca; | ||||
| } | ||||
|  | ||||
| div.reminderError[data-case="sent"] .errorIcon { | ||||
|     background-color: #d9f99d; | ||||
| } | ||||
|  | ||||
| div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| div.reminderError[data-case="sent"] .errorIcon .fas.fa-check { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| div.reminderError .errorHead .reminderName { | ||||
|     font-size: 1rem; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     justify-content: center; | ||||
|     color: rgb(54, 54, 54); | ||||
|     flex-grow: 1; | ||||
| } | ||||
|  | ||||
| div.reminderError .errorHead .reminderTime { | ||||
|     font-size: 1rem; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     flex-shrink: 1; | ||||
|     justify-content: center; | ||||
|     color: rgb(54, 54, 54); | ||||
|     background-color: #ffffff; | ||||
|     padding: 8px; | ||||
|     border-radius: 4px; | ||||
|     border-color: #e5e5e5; | ||||
|     border-width: 1px; | ||||
|     border-style: solid; | ||||
| } | ||||
|  | ||||
| /* other stuff */ | ||||
|  | ||||
| .half-rem { | ||||
| @@ -716,6 +795,18 @@ a.switch-pane { | ||||
|     text-overflow: ellipsis; | ||||
| } | ||||
|  | ||||
| .guild-submenu { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| .guild-submenu li { | ||||
|     font-size: 0.8rem; | ||||
| } | ||||
|  | ||||
| a.switch-pane.is-active ~ .guild-submenu { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| .feedback { | ||||
|     background-color: #5865F2; | ||||
| } | ||||
|   | ||||
| @@ -18,6 +18,7 @@ const $downloader = document.querySelector("a#downloader"); | ||||
| const $uploader = document.querySelector("input#uploader"); | ||||
|  | ||||
| let channels = []; | ||||
| let reminderErrors = []; | ||||
| let guildNames = {}; | ||||
| let roles = []; | ||||
| let templates = {}; | ||||
| @@ -33,7 +34,11 @@ let globalPatreon = false; | ||||
| let guildPatreon = false; | ||||
|  | ||||
| function guildId() { | ||||
|     return document.querySelector(".guildList a.is-active").dataset["guild"]; | ||||
|     return document.querySelector("li > a.is-active").parentElement.dataset["guild"]; | ||||
| } | ||||
|  | ||||
| function guildName() { | ||||
|     return guildNames[guildId()]; | ||||
| } | ||||
|  | ||||
| function colorToInt(r, g, b) { | ||||
| @@ -52,7 +57,7 @@ function switch_pane(selector) { | ||||
|         el.classList.add("is-hidden"); | ||||
|     }); | ||||
|  | ||||
|     document.getElementById(selector).classList.remove("is-hidden"); | ||||
|     document.querySelector(`*[data-name=${selector}]`).classList.remove("is-hidden"); | ||||
| } | ||||
|  | ||||
| function update_select(sel) { | ||||
| @@ -449,21 +454,19 @@ document.addEventListener("guildSwitched", async (e) => { | ||||
|         .querySelectorAll(".patreon-only") | ||||
|         .forEach((el) => el.classList.add("is-locked")); | ||||
|  | ||||
|     let $anchor = document.querySelector( | ||||
|         `.switch-pane[data-guild="${e.detail.guild_id}"]` | ||||
|     ); | ||||
|     let $li = document.querySelector(`li[data-guild="${e.detail.guild_id}"]`); | ||||
|  | ||||
|     let hasError = false; | ||||
|  | ||||
|     if ($anchor === null) { | ||||
|     if ($li === null) { | ||||
|         switch_pane("user-error"); | ||||
|         hasError = true; | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     switch_pane($anchor.dataset["pane"]); | ||||
|     switch_pane(e.detail.pane); | ||||
|     reset_guild_pane(); | ||||
|     $anchor.classList.add("is-active"); | ||||
|     $li.querySelector("li > a").classList.add("is-active"); | ||||
|     $li.querySelectorAll(`*[data-pane="${e.detail.pane}"]`).forEach((el) => { | ||||
|         el.classList.add("is-active"); | ||||
|     }); | ||||
|  | ||||
|     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { | ||||
|         document | ||||
| @@ -471,15 +474,26 @@ document.addEventListener("guildSwitched", async (e) => { | ||||
|             .forEach((el) => el.classList.remove("is-locked")); | ||||
|     } | ||||
|  | ||||
|     hasError = await fetch_channels(e.detail.guild_id); | ||||
|     const event = new CustomEvent("paneLoad", { | ||||
|         detail: { | ||||
|             guild_id: e.detail.guild_id, | ||||
|             pane: e.detail.pane, | ||||
|         }, | ||||
|     }); | ||||
|     document.dispatchEvent(event); | ||||
| }); | ||||
|  | ||||
| document.addEventListener("paneLoad", async (ev) => { | ||||
|     const hasError = await fetch_channels(ev.detail.guild_id); | ||||
|     if (!hasError) { | ||||
|         fetch_roles(e.detail.guild_id); | ||||
|         fetch_templates(e.detail.guild_id); | ||||
|         fetch_reminders(e.detail.guild_id); | ||||
|         fetch_roles(ev.detail.guild_id); | ||||
|         fetch_templates(ev.detail.guild_id); | ||||
|         fetch_reminders(ev.detail.guild_id); | ||||
|  | ||||
|         document.querySelectorAll("p.pageTitle").forEach((el) => { | ||||
|             el.textContent = `${e.detail.guild_name} Reminders`; | ||||
|             el.textContent = `${guildName()} Reminders`; | ||||
|         }); | ||||
|  | ||||
|         document.querySelectorAll("select.channel-selector").forEach((el) => { | ||||
|             el.addEventListener("change", (e) => { | ||||
|                 update_select(e.target); | ||||
| @@ -684,36 +698,56 @@ document.addEventListener("DOMContentLoaded", async () => { | ||||
|                             "%guildname%", | ||||
|                             guild.name | ||||
|                         ); | ||||
|                         $anchor.dataset["guild"] = guild.id; | ||||
|                         $anchor.dataset["name"] = guild.name; | ||||
|                         $anchor.href = `/dashboard/${guild.id}?name=${guild.name}`; | ||||
|                         $anchor.href = `/dashboard/${guild.id}/reminders`; | ||||
|  | ||||
|                         $anchor.addEventListener("click", async (e) => { | ||||
|                         const $li = $anchor.parentElement; | ||||
|                         $li.dataset["guild"] = guild.id; | ||||
|  | ||||
|                         $li.querySelectorAll("a").forEach((el) => { | ||||
|                             el.addEventListener("click", (e) => { | ||||
|                                 const pane = el.dataset["pane"]; | ||||
|                                 const slug = el.dataset["slug"]; | ||||
|  | ||||
|                                 if (pane !== undefined && slug !== undefined) { | ||||
|                                     e.preventDefault(); | ||||
|                             window.history.pushState({}, "", `/dashboard/${guild.id}`); | ||||
|  | ||||
|                                     switch_pane(pane); | ||||
|  | ||||
|                                     window.history.pushState( | ||||
|                                         {}, | ||||
|                                         "", | ||||
|                                         `/dashboard/${guild.id}/${slug}` | ||||
|                                     ); | ||||
|                                     const event = new CustomEvent("guildSwitched", { | ||||
|                                         detail: { | ||||
|                                     guild_name: guild.name, | ||||
|                                             guild_id: guild.id, | ||||
|                                             pane, | ||||
|                                         }, | ||||
|                                     }); | ||||
|  | ||||
|                                     document.dispatchEvent(event); | ||||
|                                 } | ||||
|                             }); | ||||
|                         }); | ||||
|  | ||||
|                         element.append($clone); | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 const matches = window.location.href.match(/dashboard\/(\d+)/); | ||||
|                 const matches = window.location.href.match( | ||||
|                     /dashboard\/(\d+)(\/)?([a-zA-Z\-]+)?/ | ||||
|                 ); | ||||
|                 if (matches) { | ||||
|                     let id = matches[1]; | ||||
|                     let kind = matches[3]; | ||||
|                     let name = guildNames[id]; | ||||
|  | ||||
|                     const event = new CustomEvent("guildSwitched", { | ||||
|                         detail: { | ||||
|                             guild_name: name, | ||||
|                             guild_id: id, | ||||
|                             pane: kind, | ||||
|                         }, | ||||
|                     }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										43
									
								
								web/static/js/reminder_errors.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								web/static/js/reminder_errors.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| function loadErrors() { | ||||
|     return fetch( | ||||
|         `/dashboard/api/guild/${guildId()}/reminders?status=deleted,sent,failed` | ||||
|     ).then((response) => response.json()); | ||||
| } | ||||
|  | ||||
| document.addEventListener("paneLoad", (ev) => { | ||||
|     if (ev.detail.pane !== "errors") { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     document.querySelectorAll(".reminderError").forEach((el) => el.remove()); | ||||
|  | ||||
|     const template = document.getElementById("reminderError"); | ||||
|     const container = document.getElementById("reminderLog"); | ||||
|  | ||||
|     loadErrors() | ||||
|         .then((res) => { | ||||
|             res = res | ||||
|                 .filter((r) => r.status_change_time !== null) | ||||
|                 .sort((a, b) => a.status_change_time < b.status_change_time); | ||||
|  | ||||
|             for (const reminder of res) { | ||||
|                 const newRow = template.content.cloneNode(true); | ||||
|  | ||||
|                 newRow.querySelector(".reminderError").dataset["case"] = reminder.status; | ||||
|  | ||||
|                 const statusTime = new luxon.DateTime.fromISO( | ||||
|                     reminder.status_change_time, | ||||
|                     { zone: "UTC" } | ||||
|                 ); | ||||
|                 newRow.querySelector(".reminderName").textContent = reminder.name; | ||||
|                 newRow.querySelector(".reminderTime").textContent = statusTime | ||||
|                     .toLocal() | ||||
|                     .toLocaleString(luxon.DateTime.DATETIME_MED); | ||||
|  | ||||
|                 container.appendChild(newRow); | ||||
|             } | ||||
|         }) | ||||
|         .finally(() => { | ||||
|             $loader.classList.add("is-hidden"); | ||||
|         }); | ||||
| }); | ||||
| @@ -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', () => { | ||||
|  | ||||
| }) | ||||
| @@ -332,16 +332,16 @@ | ||||
|                 <p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p> | ||||
|             </div> | ||||
|         </section> | ||||
|         <section id="guild" class="is-hidden"> | ||||
|         <section data-name="reminders" class="is-hidden"> | ||||
|             {% include "reminder_dashboard/reminder_dashboard" %} | ||||
|         </section> | ||||
|         <section id="reminder-errors" class="is-hidden"> | ||||
|         <section data-name="errors" class="is-hidden"> | ||||
|             {% include "reminder_dashboard/reminder_errors" %} | ||||
|         </section> | ||||
|         <section id="guild-error" class="is-hidden"> | ||||
|         <section data-name="guild-error" class="is-hidden"> | ||||
|             {% include "reminder_dashboard/guild_error" %} | ||||
|         </section> | ||||
|         <section id="user-error" class="is-hidden"> | ||||
|         <section data-name="user-error" class="is-hidden"> | ||||
|             {% include "reminder_dashboard/user_error" %} | ||||
|         </section> | ||||
|     </div> | ||||
| @@ -375,14 +375,28 @@ | ||||
|  | ||||
| <template id="guildListEntry"> | ||||
|     <li> | ||||
|         <a class="switch-pane" data-pane="guild"> | ||||
|         <a class="switch-pane" data-pane="reminders" data-slug="reminders"> | ||||
|             <span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span> | ||||
|         </a> | ||||
|         <ul class="guild-submenu"> | ||||
|             <li> | ||||
|                 <a class="switch-pane" data-pane="reminders" data-slug="reminders"> | ||||
|                     <span class="icon"><i class="fas fa-calendar-alt"></i></span> Reminders | ||||
|                 </a> | ||||
|                 <a class="switch-pane" data-pane="errors" data-slug="errors"> | ||||
|                     <span class="icon"><i class="fas fa-file-alt"></i></span> Logs | ||||
|                 </a> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </li> | ||||
| </template> | ||||
|  | ||||
| <template id="guildReminder"> | ||||
|     {% include "reminder_dashboard/guild_reminder" %} | ||||
|     {% include "reminder_dashboard/templates/guild_reminder" %} | ||||
| </template> | ||||
|  | ||||
| <template id="reminderError"> | ||||
|     {% include "reminder_dashboard/templates/reminder_error" %} | ||||
| </template> | ||||
|  | ||||
| <script src="/static/js/iro.js"></script> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|     <strong>Create Reminder</strong> | ||||
|     <div id="reminderCreator"> | ||||
|         {% set creating = true %} | ||||
|         {% include "reminder_dashboard/guild_reminder" %} | ||||
|         {% include "reminder_dashboard/templates/guild_reminder" %} | ||||
|         {% set creating = false %} | ||||
|     </div> | ||||
|     <br> | ||||
| @@ -46,6 +46,10 @@ | ||||
|     <div id="guildReminders"> | ||||
|  | ||||
|     </div> | ||||
|  | ||||
|     <div id="guildErrors"> | ||||
|  | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <script src="/static/js/sort.js"></script> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <div> | ||||
| <div id="reminderLog"> | ||||
|  | ||||
| </div> | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,17 @@ | ||||
| <div class="reminderError" data-case="success"> | ||||
|     <div class="errorHead"> | ||||
|         <div class="errorIcon"> | ||||
|             <span class="icon"> | ||||
|                 <i class="fas fa-trash"></i> | ||||
|                 <i class="fas fa-check"></i> | ||||
|                 <i class="fas fa-exclamation-triangle"></i> | ||||
|             </span> | ||||
|         </div> | ||||
|         <div class="reminderName"> | ||||
|             Reminder | ||||
|         </div> | ||||
|         <div class="reminderTime"> | ||||
|  | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
		Reference in New Issue
	
	Block a user