Compare commits

...

10 Commits

Author SHA1 Message Date
jude
5f703e8538 Add status update time to sender 2023-09-16 17:59:03 +01:00
jude
2993505a47 Add times to the log 2023-09-09 15:34:43 +01:00
jude
b225ad7e45 Render log rows 2023-09-03 16:00:49 +01:00
jude
ee89cb40c5 Move errors route into get_reminders route. Add database migration. 2023-09-03 15:01:42 +01:00
jude
b6b5e6d2b2 Add error pane 2023-08-27 17:41:23 +01:00
jude
adf29dca5d Start to think about how to display errors 2023-08-19 22:37:48 +01:00
jude
ea3fe3f543 Ensure postman doesn't try to send reminders with no channel 2023-08-19 21:28:05 +01:00
jude
109cf16dbb Store guild when creating reminders 2023-08-19 21:24:02 +01:00
jude
6726ca0c2d Correct migration script.
Stub code for routes. Update existing routes to set the reminder's guild
ID.
2023-08-19 21:24:02 +01:00
38133be15d In progress 2023-08-19 21:24:02 +01:00
20 changed files with 446 additions and 162 deletions

View 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);

View 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`;

View File

@ -310,6 +310,7 @@ WHERE
reminders reminders
WHERE WHERE
reminders.`utc_time` <= NOW() AND reminders.`utc_time` <= NOW() AND
reminders.`channel_id` IS NOT NULL AND
`status` = 'pending' AND `status` = 'pending' AND
( (
reminders.`interval_seconds` IS NOT NULL reminders.`interval_seconds` IS NOT NULL
@ -471,10 +472,17 @@ WHERE
} }
async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) { async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) {
sqlx::query!("UPDATE reminders SET `status` = 'sent' WHERE `id` = ?", self.id) sqlx::query!(
.execute(pool) "
.await UPDATE reminders
.expect(&format!("Could not delete Reminder {}", self.id)); SET `status` = 'sent', `status_change_time` = NOW()
WHERE `id` = ?
",
self.id
)
.execute(pool)
.await
.expect(&format!("Could not delete Reminder {}", self.id));
} }
async fn set_failed( async fn set_failed(

View File

@ -166,15 +166,21 @@ impl ComponentDataModel {
.await; .await;
} }
ComponentDataModel::DelSelector(selector) => { 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!( Err(e) => {
"UPDATE reminders SET `status` = 'deleted' WHERE FIND_IN_SET(id, ?)", warn!("Error casting ID to integer: {:?}.", e);
selected_id }
) }
.execute(&data.database) }
.await
.unwrap();
let reminders = Reminder::from_guild( let reminders = Reminder::from_guild(
&ctx, &ctx,

View File

@ -10,6 +10,7 @@ pub struct ChannelData {
pub webhook_id: Option<u64>, pub webhook_id: Option<u64>,
pub webhook_token: Option<String>, pub webhook_token: Option<String>,
pub paused: bool, pub paused: bool,
pub db_guild_id: Option<u32>,
pub paused_until: Option<NaiveDateTime>, pub paused_until: Option<NaiveDateTime>,
} }
@ -22,7 +23,11 @@ impl ChannelData {
if let Ok(c) = sqlx::query_as_unchecked!( if let Ok(c) = sqlx::query_as_unchecked!(
Self, 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 channel_id
) )
.fetch_one(pool) .fetch_one(pool)
@ -30,12 +35,18 @@ impl ChannelData {
{ {
Ok(c) Ok(c)
} else { } 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!( 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_id,
channel_name, channel_name,
guild_id guild_id
@ -46,7 +57,10 @@ impl ChannelData {
Ok(sqlx::query_as_unchecked!( Ok(sqlx::query_as_unchecked!(
Self, 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 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) { pub async fn commit_changes(&self, pool: &MySqlPool) {
sqlx::query!( sqlx::query!(
" "
UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until \ UPDATE channels
= ? WHERE id = ? SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?,
", paused = ?, paused_until = ?
WHERE id = ?",
self.name, self.name,
self.nudge, self.nudge,
self.blacklisted, self.blacklisted,

View File

@ -51,6 +51,7 @@ pub struct ReminderBuilder {
pool: MySqlPool, pool: MySqlPool,
uid: String, uid: String,
channel: u32, channel: u32,
guild: Option<u32>,
thread_id: Option<u64>, thread_id: Option<u64>,
utc_time: NaiveDateTime, utc_time: NaiveDateTime,
timezone: String, timezone: String,
@ -86,6 +87,7 @@ impl ReminderBuilder {
INSERT INTO reminders ( INSERT INTO reminders (
`uid`, `uid`,
`channel_id`, `channel_id`,
`guild_id`,
`utc_time`, `utc_time`,
`timezone`, `timezone`,
`interval_seconds`, `interval_seconds`,
@ -110,11 +112,13 @@ INSERT INTO reminders (
?, ?,
?, ?,
?, ?,
?,
? ?
) )
", ",
self.uid, self.uid,
self.channel, self.channel,
self.guild,
utc_time, utc_time,
self.timezone, self.timezone,
self.interval_seconds, self.interval_seconds,
@ -247,10 +251,10 @@ impl<'a> MultiReminderBuilder<'a> {
{ {
Err(ReminderError::UserBlockedDm) Err(ReminderError::UserBlockedDm)
} else { } else {
Ok(user_data.dm_channel) Ok((user_data.dm_channel, None))
} }
} else { } else {
Ok(user_data.dm_channel) Ok((user_data.dm_channel, None))
} }
} else { } else {
Err(ReminderError::InvalidTag) Err(ReminderError::InvalidTag)
@ -297,13 +301,13 @@ impl<'a> MultiReminderBuilder<'a> {
.commit_changes(&self.ctx.data().database) .commit_changes(&self.ctx.data().database)
.await; .await;
Ok(channel_data.id) Ok((channel_data.id, channel_data.db_guild_id))
} }
Err(e) => Err(ReminderError::DiscordError(e.to_string())), Err(e) => Err(ReminderError::DiscordError(e.to_string())),
} }
} else { } else {
Ok(channel_data.id) Ok((channel_data.id, channel_data.db_guild_id))
} }
} }
} else { } else {
@ -317,7 +321,8 @@ impl<'a> MultiReminderBuilder<'a> {
let builder = ReminderBuilder { let builder = ReminderBuilder {
pool: self.ctx.data().database.clone(), pool: self.ctx.data().database.clone(),
uid: generate_uid(), uid: generate_uid(),
channel: c, channel: c.0,
guild: c.1,
thread_id, thread_id,
utc_time: self.utc_time, utc_time: self.utc_time,
timezone: self.timezone.to_string(), timezone: self.timezone.to_string(),

View File

@ -304,10 +304,13 @@ WHERE
&self, &self,
db: impl Executor<'_, Database = Database>, db: impl Executor<'_, Database = Database>,
) -> Result<(), sqlx::Error> { ) -> Result<(), sqlx::Error> {
sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", self.uid) sqlx::query!(
.execute(db) "UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?",
.await self.uid
.map(|_| ()) )
.execute(db)
.await
.map(|_| ())
} }
pub fn display_content(&self) -> &str { pub fn display_content(&self) -> &str {

View File

@ -150,7 +150,8 @@ pub async fn initialize(
.mount( .mount(
"/dashboard", "/dashboard",
routes![ routes![
routes::dashboard::dashboard, routes::dashboard::dashboard_1,
routes::dashboard::dashboard_2,
routes::dashboard::dashboard_home, routes::dashboard::dashboard_home,
routes::dashboard::user::get_user_info, routes::dashboard::user::get_user_info,
routes::dashboard::user::update_user_info, routes::dashboard::user::update_user_info,

View File

@ -145,7 +145,7 @@ pub async fn import_reminders(
attachment: record.attachment, attachment: record.attachment,
attachment_name: record.attachment_name, attachment_name: record.attachment_name,
avatar: record.avatar, avatar: record.avatar,
channel: channel_id, channel: Some(channel_id),
content: record.content, content: record.content,
embed_author: record.embed_author, embed_author: record.embed_author,
embed_author_url: record.embed_author_url, embed_author_url: record.embed_author_url,
@ -171,6 +171,8 @@ pub async fn import_reminders(
uid: generate_uid(), uid: generate_uid(),
username: record.username, username: record.username,
utc_time: record.utc_time, utc_time: record.utc_time,
status: "pending".to_string(),
status_change_time: None,
}; };
create_reminder( create_reminder(

View File

@ -318,76 +318,64 @@ pub async fn create_guild_reminder(
.await .await
} }
#[get("/api/guild/<id>/reminders")] #[get("/api/guild/<id>/reminders?<status>")]
pub async fn get_reminders( pub async fn get_reminders(
id: u64, id: u64,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>,
serenity_context: &State<Context>, serenity_context: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
status: Option<String>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, serenity_context.inner(), id); check_authorization!(cookies, serenity_context.inner(), id);
let channels_res = GuildId(id).channels(&ctx.inner()).await; let status = status.unwrap_or("pending".to_string());
match channels_res { sqlx::query_as_unchecked!(
Ok(channels) => { Reminder,
let channels = channels "
.keys() SELECT
.into_iter() reminders.attachment,
.map(|k| k.as_u64().to_string()) reminders.attachment_name,
.collect::<Vec<String>>() reminders.avatar,
.join(","); channels.channel,
reminders.content,
reminders.embed_author,
reminders.embed_author_url,
reminders.embed_color,
reminders.embed_description,
reminders.embed_footer,
reminders.embed_footer_url,
reminders.embed_image_url,
reminders.embed_thumbnail_url,
reminders.embed_title,
IFNULL(reminders.embed_fields, '[]') AS embed_fields,
reminders.enabled,
reminders.expires,
reminders.interval_seconds,
reminders.interval_days,
reminders.interval_months,
reminders.name,
reminders.restartable,
reminders.tts,
reminders.uid,
reminders.username,
reminders.utc_time,
reminders.status,
reminders.status_change_time
FROM reminders
LEFT JOIN channels ON channels.id = reminders.channel_id
WHERE FIND_IN_SET(`status`, ?) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)",
status,
id
)
.fetch_all(pool.inner())
.await
.map(|r| Ok(json!(r)))
.unwrap_or_else(|e| {
warn!("Failed to complete SQL query: {:?}", e);
sqlx::query_as_unchecked!( json_err!("Could not load reminders")
Reminder, })
"SELECT
reminders.attachment,
reminders.attachment_name,
reminders.avatar,
channels.channel,
reminders.content,
reminders.embed_author,
reminders.embed_author_url,
reminders.embed_color,
reminders.embed_description,
reminders.embed_footer,
reminders.embed_footer_url,
reminders.embed_image_url,
reminders.embed_thumbnail_url,
reminders.embed_title,
IFNULL(reminders.embed_fields, '[]') AS embed_fields,
reminders.enabled,
reminders.expires,
reminders.interval_seconds,
reminders.interval_days,
reminders.interval_months,
reminders.name,
reminders.restartable,
reminders.tts,
reminders.uid,
reminders.username,
reminders.utc_time
FROM reminders
LEFT JOIN channels ON channels.id = reminders.channel_id
WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)",
channels
)
.fetch_all(pool.inner())
.await
.map(|r| Ok(json!(r)))
.unwrap_or_else(|e| {
warn!("Failed to complete SQL query: {:?}", e);
json_err!("Could not load reminders")
})
}
Err(e) => {
warn!("Could not fetch channels from {}: {:?}", id, e);
Ok(json!([]))
}
}
} }
#[patch("/api/guild/<id>/reminders", data = "<reminder>")] #[patch("/api/guild/<id>/reminders", data = "<reminder>")]
@ -586,7 +574,9 @@ pub async fn edit_reminder(
reminders.tts, reminders.tts,
reminders.uid, reminders.uid,
reminders.username, reminders.username,
reminders.utc_time reminders.utc_time,
reminders.status,
reminders.status_change_time
FROM reminders FROM reminders
LEFT JOIN channels ON channels.id = reminders.channel_id LEFT JOIN channels ON channels.id = reminders.channel_id
WHERE uid = ?", WHERE uid = ?",
@ -610,9 +600,12 @@ pub async fn delete_reminder(
reminder: Json<DeleteReminder>, reminder: Json<DeleteReminder>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) match sqlx::query!(
.execute(pool.inner()) "UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?",
.await reminder.uid
)
.execute(pool.inner())
.await
{ {
Ok(_) => Ok(json!({})), Ok(_) => Ok(json!({})),

View File

@ -124,8 +124,8 @@ pub struct Reminder {
attachment: Option<Vec<u8>>, attachment: Option<Vec<u8>>,
attachment_name: Option<String>, attachment_name: Option<String>,
avatar: Option<String>, avatar: Option<String>,
#[serde(with = "string")] #[serde(with = "string_opt")]
channel: u64, channel: Option<u64>,
content: String, content: String,
embed_author: String, embed_author: String,
embed_author_url: Option<String>, embed_author_url: Option<String>,
@ -150,6 +150,8 @@ pub struct Reminder {
uid: String, uid: String,
username: Option<String>, username: Option<String>,
utc_time: NaiveDateTime, utc_time: NaiveDateTime,
status: String,
status_change_time: Option<NaiveDateTime>,
} }
#[derive(Serialize, Deserialize)] #[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 { mod base64s {
use serde::{de, Deserialize, Deserializer, Serializer}; use serde::{de, Deserialize, Deserializer, Serializer};
@ -372,7 +402,7 @@ pub async fn create_reminder(
} }
// validate channel // 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_exists = channel.is_some();
let channel_matches_guild = let channel_matches_guild =
@ -380,14 +410,14 @@ pub async fn create_reminder(
if !channel_matches_guild || !channel_exists { if !channel_matches_guild || !channel_exists {
warn!( 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 reminder.channel, guild_id, channel_exists
); );
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.unwrap()), pool).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);
@ -479,6 +509,7 @@ pub async fn create_reminder(
attachment, attachment,
attachment_name, attachment_name,
channel_id, channel_id,
guild_id,
avatar, avatar,
content, content,
embed_author, embed_author,
@ -501,11 +532,12 @@ pub async fn create_reminder(
tts, tts,
username, username,
`utc_time` `utc_time`
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
new_uid, new_uid,
attachment_data, attachment_data,
reminder.attachment_name, reminder.attachment_name,
channel, channel,
guild_id.0,
reminder.avatar, reminder.avatar,
reminder.content, reminder.content,
reminder.embed_author, reminder.embed_author,
@ -560,7 +592,9 @@ pub async fn create_reminder(
reminders.tts, reminders.tts,
reminders.uid, reminders.uid,
reminders.username, reminders.username,
reminders.utc_time reminders.utc_time,
reminders.status,
reminders.status_change_time
FROM reminders FROM reminders
LEFT JOIN channels ON channels.id = reminders.channel_id LEFT JOIN channels ON channels.id = reminders.channel_id
WHERE uid = ?", WHERE uid = ?",
@ -662,7 +696,17 @@ pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirec
} }
#[get("/<_>")] #[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() { if cookies.get_private("userid").is_some() {
let map: HashMap<&str, String> = HashMap::new(); let map: HashMap<&str, String> = HashMap::new();
Ok(Template::render("dashboard", &map)) Ok(Template::render("dashboard", &map))

View File

@ -291,10 +291,19 @@ div.dashboard-sidebar:not(.mobile-sidebar) {
flex-direction: column; flex-direction: column;
} }
ul.guildList {
flex-grow: 1;
flex-shrink: 1;
overflow: scroll;
}
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
position: fixed; flex-shrink: 0;
bottom: 0; flex-grow: 0;
width: 226px; }
div.dashboard-sidebar svg {
flex-shrink: 0;
} }
div.mobile-sidebar { div.mobile-sidebar {
@ -679,6 +688,76 @@ 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;
}
/* other stuff */ /* other stuff */
.half-rem { .half-rem {
@ -716,6 +795,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;
} }

View File

@ -18,6 +18,7 @@ const $downloader = document.querySelector("a#downloader");
const $uploader = document.querySelector("input#uploader"); const $uploader = document.querySelector("input#uploader");
let channels = []; let channels = [];
let reminderErrors = [];
let guildNames = {}; let guildNames = {};
let roles = []; let roles = [];
let templates = {}; let templates = {};
@ -33,7 +34,11 @@ let globalPatreon = false;
let guildPatreon = false; let guildPatreon = false;
function guildId() { 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) { function colorToInt(r, g, b) {
@ -52,7 +57,7 @@ function switch_pane(selector) {
el.classList.add("is-hidden"); 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) { function update_select(sel) {
@ -449,21 +454,19 @@ document.addEventListener("guildSwitched", async (e) => {
.querySelectorAll(".patreon-only") .querySelectorAll(".patreon-only")
.forEach((el) => el.classList.add("is-locked")); .forEach((el) => el.classList.add("is-locked"));
let $anchor = document.querySelector( let $li = document.querySelector(`li[data-guild="${e.detail.guild_id}"]`);
`.switch-pane[data-guild="${e.detail.guild_id}"]`
);
let hasError = false; if ($li === null) {
if ($anchor === null) {
switch_pane("user-error"); switch_pane("user-error");
hasError = true;
return; return;
} }
switch_pane($anchor.dataset["pane"]); switch_pane(e.detail.pane);
reset_guild_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))) { if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) {
document document
@ -471,15 +474,26 @@ document.addEventListener("guildSwitched", async (e) => {
.forEach((el) => el.classList.remove("is-locked")); .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) { if (!hasError) {
fetch_roles(e.detail.guild_id); fetch_roles(ev.detail.guild_id);
fetch_templates(e.detail.guild_id); fetch_templates(ev.detail.guild_id);
fetch_reminders(e.detail.guild_id); fetch_reminders(ev.detail.guild_id);
document.querySelectorAll("p.pageTitle").forEach((el) => { document.querySelectorAll("p.pageTitle").forEach((el) => {
el.textContent = `${e.detail.guild_name} Reminders`; el.textContent = `${guildName()} Reminders`;
}); });
document.querySelectorAll("select.channel-selector").forEach((el) => { document.querySelectorAll("select.channel-selector").forEach((el) => {
el.addEventListener("change", (e) => { el.addEventListener("change", (e) => {
update_select(e.target); update_select(e.target);
@ -684,36 +698,56 @@ document.addEventListener("DOMContentLoaded", async () => {
"%guildname%", "%guildname%",
guild.name guild.name
); );
$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) => { const $li = $anchor.parentElement;
e.preventDefault(); $li.dataset["guild"] = guild.id;
window.history.pushState({}, "", `/dashboard/${guild.id}`);
const event = new CustomEvent("guildSwitched", { $li.querySelectorAll("a").forEach((el) => {
detail: { el.addEventListener("click", (e) => {
guild_name: guild.name, const pane = el.dataset["pane"];
guild_id: guild.id, const slug = el.dataset["slug"];
},
if (pane !== undefined && slug !== undefined) {
e.preventDefault();
switch_pane(pane);
window.history.pushState(
{},
"",
`/dashboard/${guild.id}/${slug}`
);
const event = new CustomEvent("guildSwitched", {
detail: {
guild_id: guild.id,
pane,
},
});
document.dispatchEvent(event);
}
}); });
document.dispatchEvent(event);
}); });
element.append($clone); element.append($clone);
}); });
} }
const matches = window.location.href.match(/dashboard\/(\d+)/); const matches = window.location.href.match(
/dashboard\/(\d+)(\/)?([a-zA-Z\-]+)?/
);
if (matches) { if (matches) {
let id = matches[1]; let id = matches[1];
let kind = matches[3];
let name = guildNames[id]; let name = guildNames[id];
const event = new CustomEvent("guildSwitched", { const event = new CustomEvent("guildSwitched", {
detail: { detail: {
guild_name: name, guild_name: name,
guild_id: id, guild_id: id,
pane: kind,
}, },
}); });

View 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");
});
});

View File

@ -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', () => {
})

View File

@ -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> <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 data-name="reminders" class="is-hidden">
{% include "reminder_dashboard/reminder_dashboard" %} {% include "reminder_dashboard/reminder_dashboard" %}
</section> </section>
<section id="reminder-errors" class="is-hidden"> <section data-name="errors" class="is-hidden">
{% include "reminder_dashboard/reminder_errors" %} {% include "reminder_dashboard/reminder_errors" %}
</section> </section>
<section id="guild-error" class="is-hidden"> <section data-name="guild-error" class="is-hidden">
{% include "reminder_dashboard/guild_error" %} {% include "reminder_dashboard/guild_error" %}
</section> </section>
<section id="user-error" class="is-hidden"> <section data-name="user-error" class="is-hidden">
{% include "reminder_dashboard/user_error" %} {% include "reminder_dashboard/user_error" %}
</section> </section>
</div> </div>
@ -375,14 +375,28 @@
<template id="guildListEntry"> <template id="guildListEntry">
<li> <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> <span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span>
</a> </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> </li>
</template> </template>
<template id="guildReminder"> <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> </template>
<script src="/static/js/iro.js"></script> <script src="/static/js/iro.js"></script>

View File

@ -2,7 +2,7 @@
<strong>Create Reminder</strong> <strong>Create Reminder</strong>
<div id="reminderCreator"> <div id="reminderCreator">
{% set creating = true %} {% set creating = true %}
{% include "reminder_dashboard/guild_reminder" %} {% include "reminder_dashboard/templates/guild_reminder" %}
{% set creating = false %} {% set creating = false %}
</div> </div>
<br> <br>
@ -46,6 +46,10 @@
<div id="guildReminders"> <div id="guildReminders">
</div> </div>
<div id="guildErrors">
</div>
</div> </div>
<script src="/static/js/sort.js"></script> <script src="/static/js/sort.js"></script>

View File

@ -1,4 +1,4 @@
<div> <div id="reminderLog">
</div> </div>

View File

@ -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>