3 Commits

Author SHA1 Message Date
d2d9b8d6d0 Store guild when creating reminders 2023-08-19 21:21:56 +01:00
a050b13127 Correct migration script.
Stub code for routes. Update existing routes to set the reminder's guild
ID.
2023-08-19 19:24:06 +01:00
7e627fee9e In progress 2023-08-12 12:27:15 +01:00
26 changed files with 1043 additions and 1590 deletions

1389
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,18 @@
[package] [package]
name = "reminder-rs" name = "reminder-rs"
version = "1.6.40" version = "1.6.36"
authors = ["Jude Southworth <judesouthworth@pm.me>"] authors = ["Jude Southworth <judesouthworth@pm.me>"]
edition = "2021" edition = "2021"
license = "AGPL-3.0 only" license = "AGPL-3.0 only"
description = "Reminder Bot for Discord, now in Rust" description = "Reminder Bot for Discord, now in Rust"
[dependencies] [dependencies]
poise = "0.5" poise = "0.5.5"
dotenv = "0.15" dotenv = "0.15"
tokio = { version = "1", features = ["process", "full"] } tokio = { version = "1", features = ["process", "full"] }
reqwest = "0.11" reqwest = "0.11"
lazy-regex = "3.0" lazy-regex = "2.3.0"
regex = "1.9" regex = "1.6"
log = "0.4" log = "0.4"
env_logger = "0.10" env_logger = "0.10"
chrono = "0.4" chrono = "0.4"
@ -25,7 +25,7 @@ serde_repr = "0.1"
rmp-serde = "1.1" rmp-serde = "1.1"
rand = "0.8" rand = "0.8"
levenshtein = "1.0" levenshtein = "1.0"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]} sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]}
base64 = "0.21.0" base64 = "0.21.0"
[dependencies.postman] [dependencies.postman]

View File

@ -1,4 +0,0 @@
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` WHERE `status` != 'pending';

View File

@ -5,12 +5,12 @@ edition = "2021"
[dependencies] [dependencies]
tokio = { version = "1", features = ["process", "full"] } tokio = { version = "1", features = ["process", "full"] }
regex = "1.9" regex = "1.4"
log = "0.4" log = "0.4"
chrono = "0.4" chrono = "0.4"
chrono-tz = { version = "0.8", features = ["serde"] } chrono-tz = { version = "0.5", features = ["serde"] }
lazy_static = "1.4" lazy_static = "1.4"
num-integer = "0.1" num-integer = "0.1"
serde = "1.0" serde = "1.0"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]} sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }

View File

@ -237,11 +237,11 @@ impl Into<CreateEmbed> for Embed {
pub struct Reminder { pub struct Reminder {
id: u32, id: u32,
channel_id: Option<u64>, channel_id: u64,
webhook_id: Option<u64>, webhook_id: Option<u64>,
webhook_token: Option<String>, webhook_token: Option<String>,
channel_paused: Option<bool>, channel_paused: bool,
channel_paused_until: Option<NaiveDateTime>, channel_paused_until: Option<NaiveDateTime>,
enabled: bool, enabled: bool,
@ -297,7 +297,7 @@ SELECT
reminders.`username` AS username reminders.`username` AS username
FROM FROM
reminders reminders
LEFT JOIN INNER JOIN
channels channels
ON ON
reminders.channel_id = channels.id reminders.channel_id = channels.id
@ -343,10 +343,7 @@ WHERE
async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) { async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) {
let _ = sqlx::query!( let _ = sqlx::query!(
" "UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?",
UPDATE channels SET webhook_id = NULL, webhook_token = NULL
WHERE channel = ?
",
self.channel_id self.channel_id
) )
.execute(pool) .execute(pool)
@ -418,9 +415,7 @@ WHERE
self.set_sent(pool).await; self.set_sent(pool).await;
} else { } else {
sqlx::query!( sqlx::query!(
" "UPDATE reminders SET `utc_time` = ? WHERE `id` = ?",
UPDATE reminders SET `utc_time` = ? WHERE `id` = ?
",
updated_reminder_time.with_timezone(&Utc), updated_reminder_time.with_timezone(&Utc),
self.id self.id
) )
@ -453,10 +448,7 @@ WHERE
if *LOG_TO_DATABASE { if *LOG_TO_DATABASE {
sqlx::query!( sqlx::query!(
" "INSERT INTO stat (type, reminder_id, message) VALUES ('reminder_failed', ?, ?)",
INSERT INTO stat (type, reminder_id, message)
VALUES ('reminder_failed', ?, ?)
",
self.id, self.id,
message, message,
) )
@ -469,10 +461,7 @@ WHERE
async fn log_success(&self, pool: impl Executor<'_, Database = Database> + Copy) { async fn log_success(&self, pool: impl Executor<'_, Database = Database> + Copy) {
if *LOG_TO_DATABASE { if *LOG_TO_DATABASE {
sqlx::query!( sqlx::query!(
" "INSERT INTO stat (type, reminder_id) VALUES ('reminder_sent', ?)",
INSERT INTO stat (type, reminder_id)
VALUES ('reminder_sent', ?)
",
self.id, self.id,
) )
.execute(pool) .execute(pool)
@ -482,14 +471,7 @@ 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!( sqlx::query!("UPDATE reminders SET `status` = 'sent' WHERE `id` = ?", self.id)
"
UPDATE reminders
SET `status` = 'sent', `status_change_time` = NOW()
WHERE `id` = ?
",
self.id
)
.execute(pool) .execute(pool)
.await .await
.expect(&format!("Could not delete Reminder {}", self.id)); .expect(&format!("Could not delete Reminder {}", self.id));
@ -501,11 +483,7 @@ WHERE
message: &'static str, message: &'static str,
) { ) {
sqlx::query!( sqlx::query!(
" "UPDATE reminders SET `status` = 'failed', `status_message` = ? WHERE `id` = ?",
UPDATE reminders
SET `status` = 'failed', `status_message` = ?, `status_change_time` = NOW()
WHERE `id` = ?
",
message, message,
self.id self.id
) )
@ -515,9 +493,7 @@ WHERE
} }
async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) { async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) {
if let Some(channel_id) = self.channel_id { let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await;
let _ = http.as_ref().pin_message(channel_id, message_id.into(), None).await;
}
} }
pub async fn send( pub async fn send(
@ -527,11 +503,10 @@ WHERE
) { ) {
async fn send_to_channel( async fn send_to_channel(
cache_http: impl CacheHttp, cache_http: impl CacheHttp,
channel_id: u64,
reminder: &Reminder, reminder: &Reminder,
embed: Option<CreateEmbed>, embed: Option<CreateEmbed>,
) -> Result<()> { ) -> Result<()> {
let channel = ChannelId(channel_id).to_channel(&cache_http).await; let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await;
match channel { match channel {
Ok(Channel::Guild(channel)) => { Ok(Channel::Guild(channel)) => {
@ -563,7 +538,6 @@ WHERE
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
Ok(Channel::Private(channel)) => { Ok(Channel::Private(channel)) => {
match channel match channel
.send_message(&cache_http.http(), |m| { .send_message(&cache_http.http(), |m| {
@ -593,9 +567,7 @@ WHERE
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
Err(e) => Err(e), Err(e) => Err(e),
_ => Err(Error::Other("Channel not of valid type")), _ => Err(Error::Other("Channel not of valid type")),
} }
} }
@ -650,20 +622,14 @@ WHERE
} }
} }
match self.channel_id {
Some(channel_id) => {
if self.enabled if self.enabled
&& !(self.channel_paused.unwrap_or(false) && !(self.channel_paused
&& self && self
.channel_paused_until .channel_paused_until
.map_or(true, |inner| inner >= Utc::now().naive_local())) .map_or(true, |inner| inner >= Utc::now().naive_local()))
{ {
let _ = sqlx::query!( let _ = sqlx::query!(
" "UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?",
UPDATE `channels`
SET paused = 0, paused_until = NULL
WHERE `channel` = ?
",
self.channel_id self.channel_id
) )
.execute(pool) .execute(pool)
@ -674,10 +640,8 @@ WHERE
let result = if let (Some(webhook_id), Some(webhook_token)) = let result = if let (Some(webhook_id), Some(webhook_token)) =
(self.webhook_id, &self.webhook_token) (self.webhook_id, &self.webhook_token)
{ {
let webhook_res = cache_http let webhook_res =
.http() cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await;
.get_webhook_with_token(webhook_id, webhook_token)
.await;
if let Ok(webhook) = webhook_res { if let Ok(webhook) = webhook_res {
send_to_webhook(cache_http, &self, webhook, embed).await send_to_webhook(cache_http, &self, webhook, embed).await
@ -685,10 +649,10 @@ WHERE
warn!("Webhook vanished for reminder {}: {:?}", self.id, webhook_res); warn!("Webhook vanished for reminder {}: {:?}", self.id, webhook_res);
self.reset_webhook(pool).await; self.reset_webhook(pool).await;
send_to_channel(cache_http, channel_id, &self, embed).await send_to_channel(cache_http, &self, embed).await
} }
} else { } else {
send_to_channel(cache_http, channel_id, &self, embed).await send_to_channel(cache_http, &self, embed).await
}; };
if let Err(e) = result { if let Err(e) = result {
@ -715,10 +679,7 @@ WHERE
None::<&'static str>, None::<&'static str>,
) )
.await; .await;
self.set_failed( self.set_failed(pool, "Could not be sent as guild does not exist")
pool,
"Could not be sent as guild does not exist",
)
.await; .await;
} }
50001 => { 50001 => {
@ -728,11 +689,7 @@ WHERE
None::<&'static str>, None::<&'static str>,
) )
.await; .await;
self.set_failed( self.set_failed(pool, "Could not be sent as missing access").await;
pool,
"Could not be sent as missing access",
)
.await;
} }
50007 => { 50007 => {
self.log_error( self.log_error(
@ -741,10 +698,7 @@ WHERE
None::<&'static str>, None::<&'static str>,
) )
.await; .await;
self.set_failed( self.set_failed(pool, "Could not be sent as user has DMs disabled")
pool,
"Could not be sent as user has DMs disabled",
)
.await; .await;
} }
50013 => { 50013 => {
@ -788,13 +742,4 @@ WHERE
self.refresh(pool).await; self.refresh(pool).await;
} }
} }
None => {
info!("Reminder {} is orphaned", self.id);
self.log_error(pool, "Orphaned", Option::<u8>::None).await;
self.set_failed(pool, "Could not be sent as channel was deleted").await;
}
}
}
} }

View File

@ -27,7 +27,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
"SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", "SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
guild_id.0 guild_id.0
) )
.fetch_all(&mut *transaction) .fetch_all(&mut transaction)
.await?; .await?;
let mut added_aliases = 0; let mut added_aliases = 0;
@ -42,7 +42,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
cmd_macro.description, cmd_macro.description,
cmd_macro.commands cmd_macro.commands
) )
.execute(&mut *transaction) .execute(&mut transaction)
.await?; .await?;
added_aliases += 1; added_aliases += 1;

View File

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

View File

@ -23,11 +23,7 @@ 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, guild_id AS db_guild_id 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)
@ -35,18 +31,12 @@ impl ChannelData {
{ {
Ok(c) Ok(c)
} else { } else {
let props = let props = channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name));
channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name));
let (guild_id, channel_name) = let (guild_id, channel_name) = if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
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
@ -57,10 +47,7 @@ impl ChannelData {
Ok(sqlx::query_as_unchecked!( Ok(sqlx::query_as_unchecked!(
Self, Self,
" "
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until, guild_id AS db_guild_id FROM channels WHERE channel = ?
paused_until, guild_id AS db_guild_id
FROM channels
WHERE channel = ?
", ",
channel_id channel_id
) )
@ -72,10 +59,8 @@ impl ChannelData {
pub async fn commit_changes(&self, pool: &MySqlPool) { pub async fn commit_changes(&self, pool: &MySqlPool) {
sqlx::query!( sqlx::query!(
" "
UPDATE channels UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until \
SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, = ? WHERE id = ?
paused = ?, paused_until = ?
WHERE id = ?
", ",
self.name, self.name,
self.nudge, self.nudge,

View File

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

View File

@ -7,14 +7,14 @@ edition = "2018"
[dependencies] [dependencies]
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] } rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] }
rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] } rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] }
serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
oauth2 = "4" oauth2 = "4"
log = "0.4" log = "0.4"
reqwest = "0.11" reqwest = "0.11"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] } sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] }
chrono = "0.4" chrono = "0.4"
chrono-tz = "0.8" chrono-tz = "0.5"
lazy_static = "1.4.0" lazy_static = "1.4.0"
rand = "0.7" rand = "0.7"
base64 = "0.13" base64 = "0.13"

View File

@ -72,14 +72,10 @@ pub async fn initialize(
db_pool: Pool<Database>, db_pool: Pool<Database>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
info!("Checking environment variables..."); info!("Checking environment variables...");
if env::var("OFFLINE").map_or(true, |v| v != "1") {
env::var("OAUTH2_CLIENT_ID").expect("`OAUTH2_CLIENT_ID' not supplied"); env::var("OAUTH2_CLIENT_ID").expect("`OAUTH2_CLIENT_ID' not supplied");
env::var("OAUTH2_CLIENT_SECRET").expect("`OAUTH2_CLIENT_SECRET' not supplied"); env::var("OAUTH2_CLIENT_SECRET").expect("`OAUTH2_CLIENT_SECRET' not supplied");
env::var("OAUTH2_DISCORD_CALLBACK").expect("`OAUTH2_DISCORD_CALLBACK' not supplied"); env::var("OAUTH2_DISCORD_CALLBACK").expect("`OAUTH2_DISCORD_CALLBACK' not supplied");
env::var("PATREON_GUILD_ID").expect("`PATREON_GUILD_ID' not supplied"); env::var("PATREON_GUILD_ID").expect("`PATREON_GUILD_ID' not supplied");
}
info!("Done!"); info!("Done!");
let oauth2_client = BasicClient::new( let oauth2_client = BasicClient::new(
@ -150,8 +146,7 @@ pub async fn initialize(
.mount( .mount(
"/dashboard", "/dashboard",
routes![ routes![
routes::dashboard::dashboard_1, routes::dashboard::dashboard,
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,
@ -166,6 +161,7 @@ pub async fn initialize(
routes::dashboard::guild::get_reminders, routes::dashboard::guild::get_reminders,
routes::dashboard::guild::edit_reminder, routes::dashboard::guild::edit_reminder,
routes::dashboard::guild::delete_reminder, routes::dashboard::guild::delete_reminder,
routes::dashboard::guild::get_reminder_errors,
routes::dashboard::export::export_reminders, routes::dashboard::export::export_reminders,
routes::dashboard::export::export_reminder_templates, routes::dashboard::export::export_reminder_templates,
routes::dashboard::export::export_todos, routes::dashboard::export::export_todos,
@ -190,8 +186,6 @@ pub async fn initialize(
} }
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool { pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
offline!(true);
if let Some(subscription_guild) = *CNC_GUILD { if let Some(subscription_guild) = *CNC_GUILD {
let guild_member = GuildId(subscription_guild).member(cache_http, user_id).await; let guild_member = GuildId(subscription_guild).member(cache_http, user_id).await;
@ -213,8 +207,6 @@ pub async fn check_guild_subscription(
cache_http: impl CacheHttp, cache_http: impl CacheHttp,
guild_id: impl Into<GuildId>, guild_id: impl Into<GuildId>,
) -> bool { ) -> bool {
offline!(true);
if let Some(guild) = cache_http.cache().unwrap().guild(guild_id) { if let Some(guild) = cache_http.cache().unwrap().guild(guild_id) {
let owner = guild.owner_id; let owner = guild.owner_id;

View File

@ -1,11 +1,3 @@
macro_rules! offline {
($field:expr) => {
if std::env::var("OFFLINE").map_or(false, |v| v == "1") {
return $field;
}
};
}
macro_rules! check_length { macro_rules! check_length {
($max:ident, $field:expr) => { ($max:ident, $field:expr) => {
if $field.len() > $max { if $field.len() > $max {
@ -60,7 +52,6 @@ macro_rules! check_authorization {
let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten(); let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten();
if std::env::var("OFFLINE").map_or(true, |v| v != "1") {
match user_id { match user_id {
Some(user_id) => { Some(user_id) => {
match GuildId($guild).to_guild_cached($ctx) { match GuildId($guild).to_guild_cached($ctx) {
@ -101,7 +92,6 @@ macro_rules! check_authorization {
} }
} }
} }
}
} }
macro_rules! update_field { macro_rules! update_field {

View File

@ -12,7 +12,8 @@ use sqlx::{MySql, Pool};
use crate::routes::{ use crate::routes::{
dashboard::{ dashboard::{
create_reminder, ImportBody, ReminderCreate, ReminderCsv, ReminderTemplateCsv, TodoCsv, create_reminder, generate_uid, ImportBody, Reminder, ReminderCsv, ReminderTemplateCsv,
TodoCsv,
}, },
JsonResult, JsonResult,
}; };
@ -140,7 +141,7 @@ pub async fn import_reminders(
match channel_id.parse::<u64>() { match channel_id.parse::<u64>() {
Ok(channel_id) => { Ok(channel_id) => {
let reminder = ReminderCreate { let reminder = Reminder {
attachment: record.attachment, attachment: record.attachment,
attachment_name: record.attachment_name, attachment_name: record.attachment_name,
avatar: record.avatar, avatar: record.avatar,
@ -167,6 +168,7 @@ pub async fn import_reminders(
name: record.name, name: record.name,
restartable: record.restartable, restartable: record.restartable,
tts: record.tts, tts: record.tts,
uid: generate_uid(),
username: record.username, username: record.username,
utc_time: record.utc_time, utc_time: record.utc_time,
}; };

View File

@ -26,7 +26,7 @@ use crate::{
routes::{ routes::{
dashboard::{ dashboard::{
create_database_channel, create_reminder, template_name_default, DeleteReminder, create_database_channel, create_reminder, template_name_default, DeleteReminder,
DeleteReminderTemplate, PatchReminder, Reminder, ReminderCreate, ReminderTemplate, DeleteReminderTemplate, PatchReminder, Reminder, ReminderError, ReminderTemplate,
}, },
JsonResult, JsonResult,
}, },
@ -46,7 +46,6 @@ pub async fn get_guild_patreon(
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
) -> JsonResult { ) -> JsonResult {
offline!(Ok(json!({ "patreon": true })));
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
match GuildId(id).to_guild_cached(ctx.inner()) { match GuildId(id).to_guild_cached(ctx.inner()) {
@ -74,12 +73,6 @@ pub async fn get_guild_channels(
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
) -> JsonResult { ) -> JsonResult {
offline!(Ok(json!(vec![ChannelInfo {
name: "general".to_string(),
id: "1".to_string(),
webhook_avatar: None,
webhook_name: None,
}])));
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
match GuildId(id).to_guild_cached(ctx.inner()) { match GuildId(id).to_guild_cached(ctx.inner()) {
@ -118,7 +111,6 @@ struct RoleInfo {
#[get("/api/guild/<id>/roles")] #[get("/api/guild/<id>/roles")]
pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }])));
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
let roles_res = ctx.cache.guild_roles(id); let roles_res = ctx.cache.guild_roles(id);
@ -298,7 +290,7 @@ pub async fn delete_reminder_template(
#[post("/api/guild/<id>/reminders", data = "<reminder>")] #[post("/api/guild/<id>/reminders", data = "<reminder>")]
pub async fn create_guild_reminder( pub async fn create_guild_reminder(
id: u64, id: u64,
reminder: Json<ReminderCreate>, reminder: Json<Reminder>,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
serenity_context: &State<Context>, serenity_context: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
@ -318,22 +310,18 @@ pub async fn create_guild_reminder(
.await .await
} }
#[get("/api/guild/<id>/reminders?<status>")] #[get("/api/guild/<id>/reminders")]
pub async fn get_reminders( pub async fn get_reminders(
id: u64, id: u64,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
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 status = status.unwrap_or("pending".to_string());
sqlx::query_as_unchecked!( sqlx::query_as_unchecked!(
Reminder, Reminder,
" "SELECT
SELECT
reminders.attachment, reminders.attachment,
reminders.attachment_name, reminders.attachment_name,
reminders.avatar, reminders.avatar,
@ -359,14 +347,10 @@ pub async fn get_reminders(
reminders.tts, reminders.tts,
reminders.uid, reminders.uid,
reminders.username, reminders.username,
reminders.utc_time, reminders.utc_time
reminders.status,
reminders.status_change_time,
reminders.status_message
FROM reminders FROM reminders
LEFT JOIN channels ON channels.id = reminders.channel_id LEFT JOIN channels ON channels.id = reminders.channel_id
WHERE FIND_IN_SET(`status`, ?) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", WHERE `status` = 'pending' AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)",
status,
id id
) )
.fetch_all(pool.inner()) .fetch_all(pool.inner())
@ -550,8 +534,7 @@ pub async fn edit_reminder(
match sqlx::query_as_unchecked!( match sqlx::query_as_unchecked!(
Reminder, Reminder,
" "SELECT reminders.attachment,
SELECT reminders.attachment,
reminders.attachment_name, reminders.attachment_name,
reminders.avatar, reminders.avatar,
channels.channel, channels.channel,
@ -576,10 +559,7 @@ 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,
reminders.status_message
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 = ?",
@ -603,10 +583,7 @@ pub async fn delete_reminder(
reminder: Json<DeleteReminder>, reminder: Json<DeleteReminder>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
match sqlx::query!( match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid)
"UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?",
reminder.uid
)
.execute(pool.inner()) .execute(pool.inner())
.await .await
{ {
@ -619,3 +596,35 @@ pub async fn delete_reminder(
} }
} }
} }
#[get("/api/guild/<id>/errors")]
pub async fn get_reminder_errors(
id: u64,
cookies: &CookieJar<'_>,
serenity_context: &State<Context>,
pool: &State<Pool<MySql>>,
) -> JsonResult {
check_authorization!(cookies, serenity_context.inner(), id);
sqlx::query_as_unchecked!(
ReminderError,
"SELECT
reminders.status,
reminders.utc_time,
reminders.name,
reminders.uid,
reminders.channel_id AS channel
FROM reminders
LEFT JOIN channels ON channels.id = reminders.channel_id
WHERE (`status` != 'pending' OR reminders.channel_id IS NULL) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)",
id
)
.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")
})
}

View File

@ -118,8 +118,8 @@ pub struct EmbedField {
inline: bool, inline: bool,
} }
#[derive(Deserialize)] #[derive(Serialize, Deserialize)]
pub struct ReminderCreate { pub struct Reminder {
#[serde(with = "base64s")] #[serde(with = "base64s")]
attachment: Option<Vec<u8>>, attachment: Option<Vec<u8>>,
attachment_name: Option<String>, attachment_name: Option<String>,
@ -146,45 +146,22 @@ pub struct ReminderCreate {
name: String, name: String,
restartable: bool, restartable: bool,
tts: bool, tts: bool,
username: Option<String>,
utc_time: NaiveDateTime,
}
#[derive(Serialize, Deserialize)]
pub struct Reminder {
#[serde(with = "base64s")]
attachment: Option<Vec<u8>>,
attachment_name: Option<String>,
avatar: Option<String>,
#[serde(with = "string_opt")]
channel: Option<u64>,
content: String,
embed_author: String,
embed_author_url: Option<String>,
embed_color: u32,
embed_description: String,
embed_footer: String,
embed_footer_url: Option<String>,
embed_image_url: Option<String>,
embed_thumbnail_url: Option<String>,
embed_title: String,
embed_fields: Option<Json<Vec<EmbedField>>>,
enabled: bool,
expires: Option<NaiveDateTime>,
interval_seconds: Option<u32>,
interval_days: Option<u32>,
interval_months: Option<u32>,
#[serde(default = "name_default")]
name: String,
restartable: bool,
tts: bool,
#[serde(default)] #[serde(default)]
uid: String, uid: String,
username: Option<String>, username: Option<String>,
utc_time: NaiveDateTime, utc_time: NaiveDateTime,
}
#[derive(Serialize)]
pub struct ReminderError {
#[serde(with = "string")]
channel: u64,
status: String, status: String,
status_message: Option<String>, #[serde(default = "name_default")]
status_change_time: Option<NaiveDateTime>, name: String,
#[serde(default)]
uid: String,
utc_time: NaiveDateTime,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -323,7 +300,15 @@ pub fn generate_uid() -> String {
mod string { mod string {
use std::{fmt::Display, str::FromStr}; use std::{fmt::Display, str::FromStr};
use serde::{de, Deserialize, Deserializer}; use serde::{de, Deserialize, Deserializer, Serializer};
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: Display,
S: Serializer,
{
serializer.collect_str(value)
}
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error> pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where where
@ -335,34 +320,6 @@ 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};
@ -407,7 +364,7 @@ pub async fn create_reminder(
pool: impl sqlx::Executor<'_, Database = Database> + Copy, pool: impl sqlx::Executor<'_, Database = Database> + Copy,
guild_id: GuildId, guild_id: GuildId,
user_id: UserId, user_id: UserId,
reminder: ReminderCreate, reminder: Reminder,
) -> JsonResult { ) -> JsonResult {
// check guild in db // check guild in db
match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0) match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0)
@ -435,7 +392,7 @@ 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
); );
@ -529,8 +486,7 @@ pub async fn create_reminder(
// write to db // write to db
match sqlx::query!( match sqlx::query!(
" "INSERT INTO reminders (
INSERT INTO reminders (
uid, uid,
attachment, attachment,
attachment_name, attachment_name,
@ -558,9 +514,7 @@ pub async fn create_reminder(
tts, tts,
username, username,
`utc_time` `utc_time`
) VALUES (?, ?, ?, ?, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?)",
new_uid, new_uid,
attachment_data, attachment_data,
reminder.attachment_name, reminder.attachment_name,
@ -620,10 +574,7 @@ 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,
reminders.status_message
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 = ?",
@ -725,17 +676,7 @@ pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirec
} }
#[get("/<_>")] #[get("/<_>")]
pub async fn dashboard_1(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { pub async fn dashboard(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

@ -54,8 +54,6 @@ pub async fn get_user_info(
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonValue {
offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None }));
if let Some(user_id) = if let Some(user_id) =
cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
{ {
@ -118,8 +116,6 @@ pub async fn update_user_info(
#[get("/api/user/guilds")] #[get("/api/user/guilds")]
pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue { pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue {
offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }]));
if let Some(access_token) = cookies.get_private("access_token") { if let Some(access_token) = cookies.get_private("access_token") {
let request_res = reqwest_client let request_res = reqwest_client
.get(format!("{}/users/@me/guilds", DISCORD_API)) .get(format!("{}/users/@me/guilds", DISCORD_API))

View File

@ -15,18 +15,6 @@ div.reminderContent.is-collapsed .column.settings {
display: none; display: none;
} }
div.reminderContent.is-collapsed .button-row {
display: none;
}
div.reminderContent.is-collapsed .button-row-edit {
display: none;
}
div.reminderContent.is-collapsed .reminder-topbar {
padding-bottom: 0;
}
div.reminderContent.is-collapsed .invert-collapses { div.reminderContent.is-collapsed .invert-collapses {
display: inline-flex; display: inline-flex;
} }
@ -141,12 +129,6 @@ div.split-controls {
margin-top: 0 !important; margin-top: 0 !important;
} }
.reminder-settings > .column {
flex-grow: 0;
flex-shrink: 0;
flex-basis: 50%;
}
div.reminderContent { div.reminderContent {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
@ -267,11 +249,11 @@ div#pageNavbar a {
.navbar-item.pageTitle { .navbar-item.pageTitle {
flex-shrink: 1; flex-shrink: 1;
white-space: nowrap; text-wrap: nowrap;
overflow: hidden; overflow: hidden;
} }
.dashboard-burger, .dashboard-burger:active, .dashboard-burger.is-active { .navbar-burger, .navbar-burger:active, .navbar-burger.is-active {
background-color: #adc99c !important; background-color: #adc99c !important;
border-radius: 14px; border-radius: 14px;
padding: 6px; padding: 6px;
@ -309,19 +291,10 @@ div.dashboard-sidebar:not(.mobile-sidebar) {
flex-direction: column; flex-direction: column;
} }
ul.guildList {
flex-grow: 1;
flex-shrink: 1;
overflow: auto;
}
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
flex-shrink: 0; position: fixed;
flex-grow: 0; bottom: 0;
} width: 226px;
div.dashboard-sidebar svg {
flex-shrink: 0;
} }
div.mobile-sidebar { div.mobile-sidebar {
@ -471,7 +444,8 @@ input.default-width {
.customizable.is-400x300 img { .customizable.is-400x300 img {
margin-top: 10px; margin-top: 10px;
width: 100%; width: 100%;
height: 100px; min-height: 100px;
max-height: 400px;
} }
.customizable.is-32x32 img { .customizable.is-32x32 img {
@ -615,14 +589,6 @@ input.default-width {
border-bottom: 1px solid #fff; border-bottom: 1px solid #fff;
} }
.channel-selector {
width: 100%;
}
.select {
width: 100%;
}
li.highlight { li.highlight {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
@ -646,22 +612,7 @@ li.highlight {
padding: 2px; padding: 2px;
} }
@media only screen and (max-width: 1023px) { @media only screen and (max-width: 1408px) {
p.title.pageTitle {
display: none;
}
.dashboard-frame {
margin-top: 4rem !important;
}
.customizable.thumbnail img {
width: 60px;
height: 60px;
}
}
@media only screen and (max-width: 768px) {
.button-row { .button-row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -679,20 +630,41 @@ li.highlight {
.button-row button { .button-row button {
width: 100%; width: 100%;
} }
}
.reminder-settings { @media only screen and (max-width: 768px) {
margin-bottom: 0 !important; .button-row-edit {
display: flex;
flex-direction: column;
} }
.tts-row { .button-row-edit > button {
padding-bottom: 0; width: 100%;
margin: 4px;
}
p.title.pageTitle {
visibility: hidden;
text-wrap: nowrap;
overflow: hidden;
}
}
@media only screen and (max-width: 768px) {
.customizable.thumbnail img {
width: 60px;
height: 60px;
}
.customizable.is-24x24 img {
width: 16px;
height: 16px;
} }
} }
/* loader */ /* loader */
#loader { #loader {
position: fixed; position: fixed;
top: 0;
background-color: rgba(255, 255, 255, 0.8); background-color: rgba(255, 255, 255, 0.8);
width: 100vw; width: 100vw;
z-index: 999; z-index: 999;
@ -704,86 +676,6 @@ li.highlight {
/* END */ /* END */
div.reminderError {
margin: 10px;
padding: 14px;
background-color: #f5f5f5;
border-radius: 8px;
}
div.reminderError .errorHead {
display: flex;
flex-direction: row;
}
div.reminderError .errorIcon {
padding: 8px;
border-radius: 4px;
margin-right: 12px;
}
div.reminderError .errorIcon .fas {
display: none
}
div.reminderError[data-case="deleted"] .errorIcon {
background-color: #e7e5e4;
}
div.reminderError[data-case="failed"] .errorIcon {
background-color: #fecaca;
}
div.reminderError[data-case="sent"] .errorIcon {
background-color: #d9f99d;
}
div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash {
display: block;
}
div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle {
display: block;
}
div.reminderError[data-case="sent"] .errorIcon .fas.fa-check {
display: block;
}
div.reminderError .errorHead .reminderName {
font-size: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
color: rgb(54, 54, 54);
flex-grow: 1;
}
div.reminderError .errorHead .reminderTime {
font-size: 1rem;
display: flex;
flex-direction: column;
flex-shrink: 1;
justify-content: center;
color: rgb(54, 54, 54);
background-color: #ffffff;
padding: 8px;
border-radius: 4px;
border-color: #e5e5e5;
border-width: 1px;
border-style: solid;
}
div.reminderError .reminderMessage {
font-size: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
color: rgb(54, 54, 54);
flex-grow: 1;
font-style: italic;
}
/* other stuff */ /* other stuff */
.half-rem { .half-rem {
@ -821,38 +713,15 @@ 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;
} }
.is-locked { .is-locked {
pointer-events: none; pointer-events: none;
}
.is-locked > :not(.patreon-invert) {
opacity: 0.4; opacity: 0.4;
} }
.is-locked .patreon-invert {
display: block;
}
.patreon-invert {
display: none;
}
.is-locked .foreground { .is-locked .foreground {
pointer-events: auto; pointer-events: auto;
} }
@ -882,5 +751,5 @@ a.switch-pane.is-active ~ .guild-submenu {
} }
.figure-num { .figure-num {
font-size: 2rem; font-size: 2em;
} }

View File

@ -18,7 +18,6 @@ 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 = {};
@ -34,11 +33,7 @@ let globalPatreon = false;
let guildPatreon = false; let guildPatreon = false;
function guildId() { function guildId() {
return document.querySelector("li > a.is-active").parentElement.dataset["guild"]; return document.querySelector(".guildList a.is-active").dataset["guild"];
}
function guildName() {
return guildNames[guildId()];
} }
function colorToInt(r, g, b) { function colorToInt(r, g, b) {
@ -57,7 +52,7 @@ function switch_pane(selector) {
el.classList.add("is-hidden"); el.classList.add("is-hidden");
}); });
document.querySelector(`*[data-name=${selector}]`).classList.remove("is-hidden"); document.getElementById(selector).classList.remove("is-hidden");
} }
function update_select(sel) { function update_select(sel) {
@ -454,27 +449,21 @@ 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 $li = document.querySelectorAll(`li[data-guild="${e.detail.guild_id}"]`); let $anchor = document.querySelector(
`.switch-pane[data-guild="${e.detail.guild_id}"]`
);
if ($li.length === 0) { let hasError = false;
if ($anchor === null) {
switch_pane("user-error"); switch_pane("user-error");
hasError = true;
return; return;
} }
switch_pane(e.detail.pane); switch_pane($anchor.dataset["pane"]);
reset_guild_pane(); reset_guild_pane();
document $anchor.classList.add("is-active");
.querySelectorAll(`li[data-guild="${e.detail.guild_id}"] > a`)
.forEach((el) => {
el.classList.add("is-active");
});
document
.querySelectorAll(
`li[data-guild="${e.detail.guild_id}"] *[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
@ -482,26 +471,15 @@ document.addEventListener("guildSwitched", async (e) => {
.forEach((el) => el.classList.remove("is-locked")); .forEach((el) => el.classList.remove("is-locked"));
} }
const event = new CustomEvent("paneLoad", { hasError = await fetch_channels(e.detail.guild_id);
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(ev.detail.guild_id); fetch_roles(e.detail.guild_id);
fetch_templates(ev.detail.guild_id); fetch_templates(e.detail.guild_id);
fetch_reminders(ev.detail.guild_id); fetch_reminders(e.detail.guild_id);
document.querySelectorAll("p.pageTitle").forEach((el) => { document.querySelectorAll("p.pageTitle").forEach((el) => {
el.textContent = `${guildName()} Reminders`; el.textContent = `${e.detail.guild_name} 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);
@ -706,56 +684,36 @@ 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}/reminders`; $anchor.href = `/dashboard/${guild.id}?name=${guild.name}`;
const $li = $anchor.parentElement; $anchor.addEventListener("click", async (e) => {
$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(); e.preventDefault();
window.history.pushState({}, "", `/dashboard/${guild.id}`);
switch_pane(pane);
window.history.pushState(
{},
"",
`/dashboard/${guild.id}/${slug}`
);
const event = new CustomEvent("guildSwitched", { const event = new CustomEvent("guildSwitched", {
detail: { detail: {
guild_name: guild.name,
guild_id: guild.id, guild_id: guild.id,
pane,
}, },
}); });
document.dispatchEvent(event); document.dispatchEvent(event);
}
});
}); });
element.append($clone); element.append($clone);
}); });
} }
const matches = window.location.href.match( const matches = window.location.href.match(/dashboard\/(\d+)/);
/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

@ -1,45 +1,19 @@
function loadErrors() { let _reminderErrors = [];
return fetch(
`/dashboard/api/guild/${guildId()}/reminders?status=deleted,sent,failed` const reminderErrors = () => {
).then((response) => response.json()); return _reminderErrors;
} }
document.addEventListener("paneLoad", (ev) => { const guildId = () => {
if (ev.detail.pane !== "errors") { let selected = document.querySelector(".guildList a.is-active");
return; return selected.dataset["guild"];
} }
document.querySelectorAll(".reminderError").forEach((el) => el.remove());
const template = document.getElementById("reminderError"); function loadErrors() {
const container = document.getElementById("reminderLog"); fetch(`/dashboard/api/guild/${guildId()}/errors`).then(response => response.json())
}
loadErrors() document.addEventListener('DOMContentLoaded', () => {
.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(".reminderMessage").textContent =
reminder.status_message;
newRow.querySelector(".reminderTime").textContent = statusTime
.toLocal()
.toLocaleString(luxon.DateTime.DATETIME_MED);
container.appendChild(newRow);
}
})
.finally(() => {
$loader.classList.add("is-hidden");
});
});

View File

@ -40,14 +40,14 @@
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="/"> <a class="navbar-item" href="/">
<figure class="image"> <figure class="image">
<img width="28px" height="28px" src="/static/img/logo_nobg.webp" alt="Reminder Bot Logo"> <img src="/static/img/logo_nobg.webp" alt="Reminder Bot Logo">
</figure> </figure>
</a> </a>
<p class="navbar-item pageTitle"> <p class="navbar-item pageTitle">
</p> </p>
<a role="button" class="dashboard-burger navbar-burger is-right" aria-label="menu" aria-expanded="false" <a role="button" class="navbar-burger is-right" aria-label="menu" aria-expanded="false"
data-target="mobileSidebar"> data-target="mobileSidebar">
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
@ -234,7 +234,6 @@
<a href="/"> <a href="/">
<div class="brand"> <div class="brand">
<img src="/static/img/logo_nobg.webp" alt="Reminder bot logo" <img src="/static/img/logo_nobg.webp" alt="Reminder bot logo"
width="52px" height="52px"
class="dashboard-brand"> class="dashboard-brand">
</div> </div>
</a> </a>
@ -333,17 +332,37 @@
<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 data-name="reminders" class="is-hidden"> <section id="guild" class="is-hidden">
{% include "reminder_dashboard/reminder_dashboard" %} {% include "reminder_dashboard/reminder_dashboard" %}
</section> </section>
<section data-name="errors" class="is-hidden"> <section id="guild-error" class="is-hidden hero is-fullheight">
{% include "reminder_dashboard/reminder_errors" %} <div class="hero-body">
<div class="container has-text-centered">
<p class="title">
We couldn't get this server's data
</p>
<p class="subtitle">
Please check Reminder Bot is in the server, and has correct permissions.
</p>
<a class="button is-size-4 is-rounded is-success" href="https://invite.reminder-bot.com">
<p class="is-size-4">
<span>Add to Server</span> <span class="icon"><i class="fas fa-chevron-right"></i></span>
</p>
</a>
</div>
</div>
</section> </section>
<section data-name="guild-error" class="is-hidden"> <section id="user-error" class="is-hidden hero is-fullheight">
{% include "reminder_dashboard/guild_error" %} <div class="hero-body">
</section> <div class="container has-text-centered">
<section data-name="user-error" class="is-hidden"> <p class="title">
{% include "reminder_dashboard/user_error" %} You do not have permissions for this server
</p>
<p class="subtitle">
Ask an admin to grant you the "Manage Messages" permission.
</p>
</div>
</div>
</section> </section>
</div> </div>
<!-- /main content --> <!-- /main content -->
@ -376,28 +395,14 @@
<template id="guildListEntry"> <template id="guildListEntry">
<li> <li>
<a class="switch-pane" data-pane="reminders" data-slug="reminders"> <a class="switch-pane" data-pane="guild">
<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/templates/guild_reminder" %} {% include "reminder_dashboard/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

@ -1,17 +0,0 @@
<div class="hero is-fullheight">
<div class="hero-body">
<div class="container has-text-centered">
<p class="title">
We couldn't get this server's data
</p>
<p class="subtitle">
Please check Reminder Bot is in the server, and has correct permissions.
</p>
<a class="button is-size-4 is-rounded is-success" href="https://invite.reminder-bot.com">
<p class="is-size-4">
<span>Add to Server</span> <span class="icon"><i class="fas fa-chevron-right"></i></span>
</p>
</a>
</div>
</div>
</div>

View File

@ -133,6 +133,8 @@
</article> </article>
</div> </div>
<div class="column settings"> <div class="column settings">
<div class="columns">
<div class="column">
<div class="field channel-field"> <div class="field channel-field">
<div class="collapses"> <div class="collapses">
<label class="label" for="channelOption">Channel*</label> <label class="label" for="channelOption">Channel*</label>
@ -147,7 +149,8 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="column">
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<label class="label collapses"> <label class="label collapses">
@ -156,13 +159,12 @@
</label> </label>
</div> </div>
</div> </div>
</div>
</div>
<div class="collapses split-controls"> <div class="collapses split-controls">
<div> <div>
<div class="patreon-only"> <div class="patreon-only">
<div class="patreon-invert foreground">
Intervals available on <a href="https://patreon.com/jellywx">Patreon</a> or <a href="https://gitea.jellypro.xyz/jude/reminder-bot">self-hosting</a>
</div>
<div class="field"> <div class="field">
<label class="label">Interval <a class="foreground" href="/help/intervals"><i class="fas fa-question-circle"></i></a></label> <label class="label">Interval <a class="foreground" href="/help/intervals"><i class="fas fa-question-circle"></i></a></label>
<div class="control intervalSelector"> <div class="control intervalSelector">
@ -231,9 +233,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</div>
{% if creating %} {% if creating %}
<div class="button-row"> <div class="button-row">
<div class="button-row-reminder"> <div class="button-row-reminder">
@ -266,4 +266,7 @@
</button> </button>
</div> </div>
{% endif %} {% endif %}
</div>
</div>
</div>
</div> </div>

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/templates/guild_reminder" %} {% include "reminder_dashboard/guild_reminder" %}
{% set creating = false %} {% set creating = false %}
</div> </div>
<br> <br>
@ -46,10 +46,6 @@
<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,5 +0,0 @@
<div id="reminderLog">
</div>
<script src="/static/js/reminder_errors.js"></script>

View File

@ -1,20 +0,0 @@
<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="reminderMessage">
</div>
<div class="reminderTime">
</div>
</div>
</div>

View File

@ -1,12 +0,0 @@
<div class="hero is-fullheight">
<div class="hero-body">
<div class="container has-text-centered">
<p class="title">
You do not have permissions for this server
</p>
<p class="subtitle">
Ask an admin to grant you the "Manage Messages" permission.
</p>
</div>
</div>
</div>