Compare commits
	
		
			10 Commits
		
	
	
		
			1.7.14
			...
			jude/orpha
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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
 | 
			
		||||
@@ -382,13 +377,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>")]
 | 
			
		||||
pub async fn edit_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