diff --git a/postman/src/lib.rs b/postman/src/lib.rs index 1de374b..a378dcf 100644 --- a/postman/src/lib.rs +++ b/postman/src/lib.rs @@ -2,14 +2,31 @@ mod sender; use std::env; -use log::info; +use log::{info, warn}; use serenity::client::Context; use sqlx::{Executor, MySql}; -use tokio::time::{sleep_until, Duration, Instant}; +use tokio::{ + sync::broadcast::Receiver, + time::{sleep_until, Duration, Instant}, +}; type Database = MySql; -pub async fn initialize(ctx: Context, pool: impl Executor<'_, Database = Database> + Copy) { +pub async fn initialize( + mut kill: Receiver<()>, + ctx: Context, + pool: impl Executor<'_, Database = Database> + Copy, +) -> Result<(), &'static str> { + tokio::select! { + output = _initialize(ctx, pool) => Ok(output), + _ = kill.recv() => { + warn!("Received terminate signal. Goodbye"); + Err("Received terminate signal. Goodbye") + } + } +} + +async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database> + Copy) { let remind_interval = env::var("REMIND_INTERVAL") .map(|inner| inner.parse::().ok()) .ok() diff --git a/src/event_handlers.rs b/src/event_handlers.rs index b5c6ba1..f1a786f 100644 --- a/src/event_handlers.rs +++ b/src/event_handlers.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, env, sync::atomic::Ordering}; -use log::{info, warn}; +use log::{error, info, warn}; use poise::{ serenity::{model::interactions::Interaction, utils::shard_id}, serenity_prelude as serenity, @@ -15,10 +15,12 @@ pub async fn listener( ) -> Result<(), Error> { match event { poise::Event::CacheReady { .. } => { - info!("Cache Ready!"); - info!("Preparing to send reminders"); + info!("Cache Ready! Preparing extra processes"); if !data.is_loop_running.load(Ordering::Relaxed) { + let kill_tx = data.broadcast.clone(); + let kill_recv = data.broadcast.subscribe(); + let ctx1 = ctx.clone(); let ctx2 = ctx.clone(); @@ -29,7 +31,12 @@ pub async fn listener( if !run_settings.contains("postman") { tokio::spawn(async move { - postman::initialize(ctx1, &pool1).await; + match postman::initialize(kill_recv, ctx1, &pool1).await { + Ok(_) => {} + Err(e) => { + error!("postman exiting: {}", e); + } + }; }); } else { warn!("Not running postman") @@ -37,7 +44,7 @@ pub async fn listener( if !run_settings.contains("web") { tokio::spawn(async move { - reminder_web::initialize(ctx2, pool2).await.unwrap(); + reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap(); }); } else { warn!("Not running web") diff --git a/src/main.rs b/src/main.rs index ec07695..71bb7a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,13 @@ mod models; mod time_parser; mod utils; -use std::{collections::HashMap, env, fmt::Formatter, sync::atomic::AtomicBool}; +use std::{ + collections::HashMap, + env, + error::Error as StdError, + fmt::{Debug, Display, Formatter}, + sync::atomic::AtomicBool, +}; use chrono_tz::Tz; use dotenv::dotenv; @@ -21,7 +27,7 @@ use poise::serenity::model::{ id::{GuildId, UserId}, }; use sqlx::{MySql, Pool}; -use tokio::sync::RwLock; +use tokio::sync::{broadcast, broadcast::Sender, RwLock}; use crate::{ commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds}, @@ -43,6 +49,7 @@ pub struct Data { recording_macros: RwLock>>, popular_timezones: Vec, is_loop_running: AtomicBool, + broadcast: Sender<()>, } impl std::fmt::Debug for Data { @@ -51,8 +58,33 @@ impl std::fmt::Debug for Data { } } +struct Ended; + +impl Debug for Ended { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("Process ended.") + } +} + +impl Display for Ended { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("Process ended.") + } +} + +impl StdError for Ended {} + #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), Box> { + let (tx, mut rx) = broadcast::channel(16); + + tokio::select! { + output = _main(tx) => output, + _ = rx.recv() => Err(Box::new(Ended) as Box) + } +} + +async fn _main(tx: Sender<()>) -> Result<(), Box> { env_logger::init(); dotenv()?; @@ -157,6 +189,7 @@ async fn main() -> Result<(), Box> { popular_timezones, recording_macros: Default::default(), is_loop_running: AtomicBool::new(false), + broadcast: tx, }) }) }) diff --git a/web/src/lib.rs b/web/src/lib.rs index 2eab0b6..ff93af0 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -9,7 +9,7 @@ mod routes; use std::{collections::HashMap, env}; use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; -use rocket::fs::FileServer; +use rocket::{fs::FileServer, tokio::sync::broadcast::Sender}; use rocket_dyn_templates::Template; use serenity::{ client::Context, @@ -53,6 +53,7 @@ async fn internal_server_error() -> Template { } pub async fn initialize( + kill_channel: Sender<()>, serenity_context: Context, db_pool: Pool, ) -> Result<(), Box> { @@ -119,6 +120,10 @@ pub async fn initialize( .launch() .await?; + warn!("Exiting rocket runtime"); + // distribute kill signal + kill_channel.send(()); + Ok(()) } diff --git a/web/static/css/style.css b/web/static/css/style.css index 20f898a..d81e3d5 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -57,6 +57,14 @@ button.change-color { left: calc(-1rem - 40px); } +button.disable-enable[data-action="enable"]:after { + content: "Enable"; +} + +button.disable-enable[data-action="disable"]:after { + content: "Disable"; +} + .media-content { overflow-x: visible; } diff --git a/web/static/js/main.js b/web/static/js/main.js index 164208e..80e31f7 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -3,10 +3,7 @@ let $discordFrame; const $loader = document.querySelector("#loader"); const $colorPickerModal = document.querySelector("div#pickColorModal"); const $colorPickerInput = $colorPickerModal.querySelector("input"); - -let timezone = luxon.DateTime.now().zone.name; -const browserTimezone = luxon.DateTime.now().zone.name; -let botTimezone = "UTC"; +const $deleteReminderBtn = document.querySelector("#delete-reminder-confirm"); let channels; let roles; @@ -75,6 +72,8 @@ function fetch_roles(guild_id) { } async function fetch_reminders(guild_id) { + document.dispatchEvent(new Event("remindersLoading")); + const $reminderBox = document.querySelector("div#guildReminders"); // reset div contents @@ -113,7 +112,6 @@ async function fetch_reminders(guild_id) { } let $enableBtn = newFrame.querySelector(".disable-enable"); - $enableBtn.textContent = reminder["enabled"] ? "Disable" : "Enable"; $enableBtn.dataset.action = reminder["enabled"] ? "disable" : "enable"; @@ -164,14 +162,36 @@ document.addEventListener("remindersLoaded", (event) => { if (data.error) { show_error(data.error); } else { - enableBtn.textContent = data["enabled"] ? "Disable" : "Enable"; enableBtn.dataset.action = data["enabled"] ? "enable" : "disable"; } }); }); + + reminder.node + .querySelector("button.delete-reminder") + .addEventListener("click", () => { + let uid = reminder.node.closest(".reminderContent").dataset.uid; + + $deleteReminderBtn.dataset["uid"] = uid; + $deleteReminderBtn.closest(".modal").classList.toggle("is-active"); + }); } }); +$deleteReminderBtn.addEventListener("click", () => { + let guild = document.querySelector(".guildList a.is-active").dataset["guild"]; + + fetch(`/dashboard/api/guild/${guild}/reminders`, { + method: "DELETE", + body: JSON.stringify({ + uid: $deleteReminderBtn.dataset["uid"], + }), + }).then(() => { + document.querySelector("#deleteReminderModal").classList.remove("is-active"); + fetch_reminders(guild); + }); +}); + function show_error(error) { document.getElementById("errors").querySelector("span.error-message").textContent = error; @@ -182,60 +202,6 @@ function show_error(error) { }, 5000); } -function update_times() { - document.querySelectorAll("span.set-timezone").forEach((element) => { - element.textContent = timezone; - }); - document.querySelectorAll("span.set-time").forEach((element) => { - element.textContent = luxon.DateTime.now().setZone(timezone).toFormat("HH:mm"); - }); - document.querySelectorAll("span.browser-timezone").forEach((element) => { - element.textContent = browserTimezone; - }); - document.querySelectorAll("span.browser-time").forEach((element) => { - element.textContent = luxon.DateTime.now().toFormat("HH:mm"); - }); - document.querySelectorAll("span.bot-timezone").forEach((element) => { - element.textContent = botTimezone; - }); - document.querySelectorAll("span.bot-time").forEach((element) => { - element.textContent = luxon.DateTime.now().setZone(botTimezone).toFormat("HH:mm"); - }); -} - -window.setInterval(() => { - update_times(); -}, 30000); - -document.getElementById("set-bot-timezone").addEventListener("click", () => { - timezone = botTimezone; - update_times(); -}); -document.getElementById("set-browser-timezone").addEventListener("click", () => { - timezone = browserTimezone; - update_times(); -}); -document.getElementById("update-bot-timezone").addEventListener("click", () => { - timezone = browserTimezone; - fetch("/dashboard/api/user", { - method: "PATCH", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ timezone: timezone }), - }) - .then((response) => response.json()) - .then((data) => { - if (data.error) { - show_error(data.error); - } else { - botTimezone = browserTimezone; - update_times(); - } - }); -}); - $colorPickerInput.value = colorPicker.color.hexString; $colorPickerInput.addEventListener("input", () => { @@ -482,6 +448,7 @@ $createReminder.querySelector("button#createReminder").addEventListener("click", .then((data) => console.log(data)); // process response + fetch_reminders(guild); // reset inputs }); diff --git a/web/static/js/timezone.js b/web/static/js/timezone.js new file mode 100644 index 0000000..9515830 --- /dev/null +++ b/web/static/js/timezone.js @@ -0,0 +1,57 @@ +let timezone = luxon.DateTime.now().zone.name; +const browserTimezone = luxon.DateTime.now().zone.name; +let botTimezone = "UTC"; + +function update_times() { + document.querySelectorAll("span.set-timezone").forEach((element) => { + element.textContent = timezone; + }); + document.querySelectorAll("span.set-time").forEach((element) => { + element.textContent = luxon.DateTime.now().setZone(timezone).toFormat("HH:mm"); + }); + document.querySelectorAll("span.browser-timezone").forEach((element) => { + element.textContent = browserTimezone; + }); + document.querySelectorAll("span.browser-time").forEach((element) => { + element.textContent = luxon.DateTime.now().toFormat("HH:mm"); + }); + document.querySelectorAll("span.bot-timezone").forEach((element) => { + element.textContent = botTimezone; + }); + document.querySelectorAll("span.bot-time").forEach((element) => { + element.textContent = luxon.DateTime.now().setZone(botTimezone).toFormat("HH:mm"); + }); +} + +window.setInterval(() => { + update_times(); +}, 30000); + +document.getElementById("set-bot-timezone").addEventListener("click", () => { + timezone = botTimezone; + update_times(); +}); +document.getElementById("set-browser-timezone").addEventListener("click", () => { + timezone = browserTimezone; + update_times(); +}); +document.getElementById("update-bot-timezone").addEventListener("click", () => { + timezone = browserTimezone; + fetch("/dashboard/api/user", { + method: "PATCH", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ timezone: timezone }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + show_error(data.error); + } else { + botTimezone = browserTimezone; + update_times(); + } + }); +}); diff --git a/web/templates/dashboard.html.tera b/web/templates/dashboard.html.tera index 820313a..3c6999c 100644 --- a/web/templates/dashboard.html.tera +++ b/web/templates/dashboard.html.tera @@ -140,6 +140,27 @@ + +