diff --git a/Cargo.lock b/Cargo.lock index cde8216..783f30a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1943,6 +1943,8 @@ dependencies = [ "log", "num-integer", "regex", + "serde", + "serde_json", "serenity", "sqlx", "tokio", @@ -2153,6 +2155,7 @@ dependencies = [ "rocket", "rocket_dyn_templates", "serde", + "serde_json", "serenity", "sqlx", ] @@ -2761,6 +2764,8 @@ dependencies = [ "rand 0.8.5", "rsa", "rustls 0.19.1", + "serde", + "serde_json", "sha-1 0.9.8", "sha2 0.9.9", "smallvec", @@ -2786,6 +2791,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", + "serde_json", "sha2 0.9.9", "sqlx-core", "sqlx-rt", diff --git a/migration/04-reminder_templates.sql b/migration/04-reminder_templates.sql index 987ad59..b5ed77f 100644 --- a/migration/04-reminder_templates.sql +++ b/migration/04-reminder_templates.sql @@ -29,3 +29,5 @@ CREATE TABLE reminder_template ( FOREIGN KEY (`guild_id`) REFERENCES channels (`id`) ON DELETE CASCADE ); + +ALTER TABLE reminders ADD COLUMN embed_fields JSON; diff --git a/postman/Cargo.toml b/postman/Cargo.toml index 5297ae6..da7a5aa 100644 --- a/postman/Cargo.toml +++ b/postman/Cargo.toml @@ -12,7 +12,9 @@ chrono = "0.4" chrono-tz = { version = "0.5", features = ["serde"] } lazy_static = "1.4" num-integer = "0.1" -sqlx = { version = "0.5.10", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]} +serde = "1.0" +serde_json = "1.0" +sqlx = { version = "0.5.10", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]} [dependencies.serenity] git = "https://github.com/serenity-rs/serenity" diff --git a/postman/src/sender.rs b/postman/src/sender.rs index fd17451..2feb704 100644 --- a/postman/src/sender.rs +++ b/postman/src/sender.rs @@ -4,6 +4,7 @@ use lazy_static::lazy_static; use log::{error, info, warn}; use num_integer::Integer; use regex::{Captures, Regex}; +use serde::Deserialize; use serenity::{ builder::CreateEmbed, http::{CacheHttp, Http, StatusCode}, @@ -15,7 +16,10 @@ use serenity::{ Error, Result, }; use sqlx::{ - types::chrono::{NaiveDateTime, Utc}, + types::{ + chrono::{NaiveDateTime, Utc}, + Json, + }, Executor, }; @@ -94,11 +98,6 @@ pub fn substitute(string: &str) -> String { } struct Embed { - inner: EmbedInner, - fields: Vec, -} - -struct EmbedInner { title: String, description: String, image_url: Option, @@ -108,8 +107,10 @@ struct EmbedInner { author: String, author_url: Option, color: u32, + fields: Json>, } +#[derive(Deserialize)] struct EmbedField { title: String, value: String, @@ -121,76 +122,54 @@ impl Embed { pool: impl Executor<'_, Database = Database> + Copy, id: u32, ) -> Option { - let mut inner = sqlx::query_as_unchecked!( - EmbedInner, - " -SELECT - `embed_title` AS title, - `embed_description` AS description, - `embed_image_url` AS image_url, - `embed_thumbnail_url` AS thumbnail_url, - `embed_footer` AS footer, - `embed_footer_url` AS footer_url, - `embed_author` AS author, - `embed_author_url` AS author_url, - `embed_color` AS color -FROM - reminders -WHERE - `id` = ? - ", + let mut embed = sqlx::query_as!( + Self, + r#" + SELECT + `embed_title` AS title, + `embed_description` AS description, + `embed_image_url` AS image_url, + `embed_thumbnail_url` AS thumbnail_url, + `embed_footer` AS footer, + `embed_footer_url` AS footer_url, + `embed_author` AS author, + `embed_author_url` AS author_url, + `embed_color` AS color, + IFNULL(`embed_fields`, '[]') AS "fields:_" + FROM reminders + WHERE `id` = ?"#, id ) .fetch_one(pool) .await .unwrap(); - inner.title = substitute(&inner.title); - inner.description = substitute(&inner.description); - inner.footer = substitute(&inner.footer); + embed.title = substitute(&embed.title); + embed.description = substitute(&embed.description); + embed.footer = substitute(&embed.footer); - let mut fields = sqlx::query_as_unchecked!( - EmbedField, - " -SELECT - title, - value, - inline -FROM - embed_fields -WHERE - reminder_id = ? - ", - id - ) - .fetch_all(pool) - .await - .unwrap(); - - fields.iter_mut().for_each(|mut field| { + embed.fields.iter_mut().for_each(|mut field| { field.title = substitute(&field.title); field.value = substitute(&field.value); }); - let e = Embed { inner, fields }; - - if e.has_content() { - Some(e) + if embed.has_content() { + Some(embed) } else { None } } pub fn has_content(&self) -> bool { - if self.inner.title.is_empty() - && self.inner.description.is_empty() - && self.inner.image_url.is_none() - && self.inner.thumbnail_url.is_none() - && self.inner.footer.is_empty() - && self.inner.footer_url.is_none() - && self.inner.author.is_empty() - && self.inner.author_url.is_none() - && self.fields.is_empty() + if self.title.is_empty() + && self.description.is_empty() + && self.image_url.is_none() + && self.thumbnail_url.is_none() + && self.footer.is_empty() + && self.footer_url.is_none() + && self.author.is_empty() + && self.author_url.is_none() + && self.fields.0.is_empty() { false } else { @@ -203,37 +182,37 @@ impl Into for Embed { fn into(self) -> CreateEmbed { let mut c = CreateEmbed::default(); - c.title(&self.inner.title) - .description(&self.inner.description) - .color(self.inner.color) + c.title(&self.title) + .description(&self.description) + .color(self.color) .author(|a| { - a.name(&self.inner.author); + a.name(&self.author); - if let Some(author_icon) = &self.inner.author_url { + if let Some(author_icon) = &self.author_url { a.icon_url(author_icon); } a }) .footer(|f| { - f.text(&self.inner.footer); + f.text(&self.footer); - if let Some(footer_icon) = &self.inner.footer_url { + if let Some(footer_icon) = &self.footer_url { f.icon_url(footer_icon); } f }); - for field in &self.fields { + for field in &self.fields.0 { c.field(&field.title, &field.value, field.inline); } - if let Some(image_url) = &self.inner.image_url { + if let Some(image_url) = &self.image_url { c.image(image_url); } - if let Some(thumbnail_url) = &self.inner.thumbnail_url { + if let Some(thumbnail_url) = &self.thumbnail_url { c.thumbnail(thumbnail_url); } diff --git a/web/Cargo.toml b/web/Cargo.toml index 89fccfa..10700fe 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -12,7 +12,8 @@ oauth2 = "4" log = "0.4" reqwest = "0.11" serde = { version = "1.0", features = ["derive"] } -sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono"] } +serde_json = "1.0" +sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] } chrono = "0.4" chrono-tz = "0.5" lazy_static = "1.4.0" diff --git a/web/src/consts.rs b/web/src/consts.rs index a94e8e7..937f776 100644 --- a/web/src/consts.rs +++ b/web/src/consts.rs @@ -10,9 +10,9 @@ pub const MAX_EMBED_AUTHOR_LENGTH: usize = 256; pub const MAX_EMBED_FOOTER_LENGTH: usize = 2048; pub const MAX_URL_LENGTH: usize = 512; pub const MAX_USERNAME_LENGTH: usize = 100; +pub const MAX_EMBED_FIELDS: usize = 25; pub const MAX_EMBED_FIELD_TITLE_LENGTH: usize = 256; pub const MAX_EMBED_FIELD_VALUE_LENGTH: usize = 1024; -pub const MAX_EMBED_FIELDS: usize = 25; pub const MINUTE: usize = 60; pub const HOUR: usize = 60 * MINUTE; diff --git a/web/src/lib.rs b/web/src/lib.rs index 274a796..8c54b49 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -9,7 +9,11 @@ mod routes; use std::{collections::HashMap, env}; use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; -use rocket::{fs::FileServer, tokio::sync::broadcast::Sender}; +use rocket::{ + fs::FileServer, + serde::json::{json, Json, Value as JsonValue}, + tokio::sync::broadcast::Sender, +}; use rocket_dyn_templates::Template; use serenity::{ client::Context, @@ -46,6 +50,11 @@ async fn not_found() -> Template { Template::render("errors/404", &map) } +#[catch(422)] +async fn unprocessable_entity() -> JsonValue { + json!({"error": "Invalid request.", "errors": ["Invalid request."]}) +} + #[catch(500)] async fn internal_server_error() -> Template { let map: HashMap = HashMap::new(); @@ -69,7 +78,16 @@ pub async fn initialize( rocket::build() .attach(Template::fairing()) - .register("/", catchers![not_authorized, forbidden, not_found, internal_server_error]) + .register( + "/", + catchers![ + not_authorized, + forbidden, + not_found, + internal_server_error, + unprocessable_entity + ], + ) .manage(oauth2_client) .manage(reqwest_client) .manage(serenity_context) @@ -105,10 +123,6 @@ pub async fn initialize( routes::dashboard::user::get_user_info, routes::dashboard::user::update_user_info, routes::dashboard::user::get_user_guilds, - routes::dashboard::user::create_reminder, - routes::dashboard::user::get_reminders, - routes::dashboard::user::overwrite_reminder, - routes::dashboard::user::delete_reminder, routes::dashboard::guild::get_guild_channels, routes::dashboard::guild::get_guild_roles, routes::dashboard::guild::create_reminder, diff --git a/web/src/routes/dashboard/guild.rs b/web/src/routes/dashboard/guild.rs index b8edd7e..74de489 100644 --- a/web/src/routes/dashboard/guild.rs +++ b/web/src/routes/dashboard/guild.rs @@ -19,12 +19,13 @@ use crate::{ check_guild_subscription, check_subscription, consts::{ DAY, DISCORD_CDN, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, - MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, + MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, + MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL, }, routes::dashboard::{ - create_database_channel, generate_uid, name_default, DeleteReminder, JsonReminder, - PatchReminder, Reminder, + create_database_channel, generate_uid, name_default, DeleteReminder, PatchReminder, + Reminder, }, }; @@ -133,7 +134,7 @@ pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State/reminders", data = "")] pub async fn create_reminder( id: u64, - reminder: Json, + reminder: Json, cookies: &CookieJar<'_>, serenity_context: &State, pool: &State>, @@ -180,6 +181,13 @@ pub async fn create_reminder( check_length!(MAX_EMBED_TITLE_LENGTH, reminder.embed_title); check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder.embed_author); check_length!(MAX_EMBED_FOOTER_LENGTH, reminder.embed_footer); + check_length_opt!(MAX_EMBED_FIELDS, reminder.embed_fields); + if let Some(fields) = &reminder.embed_fields { + for field in &fields.0 { + check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value); + check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title); + } + } check_length_opt!(MAX_USERNAME_LENGTH, reminder.username); check_length_opt!( MAX_URL_LENGTH, @@ -245,6 +253,7 @@ pub async fn create_reminder( embed_image_url, embed_thumbnail_url, embed_title, + embed_fields, enabled, expires, interval_seconds, @@ -255,7 +264,7 @@ pub async fn create_reminder( tts, username, `utc_time` - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new_uid, attachment_data, reminder.attachment_name, @@ -271,6 +280,7 @@ pub async fn create_reminder( reminder.embed_image_url, reminder.embed_thumbnail_url, reminder.embed_title, + reminder.embed_fields, reminder.enabled, reminder.expires, reminder.interval_seconds, @@ -302,6 +312,7 @@ pub async fn create_reminder( reminders.embed_image_url, reminders.embed_thumbnail_url, reminders.embed_title, + reminders.embed_fields, reminders.enabled, reminders.expires, reminders.interval_seconds, @@ -324,7 +335,7 @@ pub async fn create_reminder( .unwrap_or_else(|e| { warn!("Failed to complete SQL query: {:?}", e); - json!({"error": "Could not load reminders"}) + json!({"error": "Could not load reminder"}) }), Err(e) => { @@ -365,6 +376,7 @@ pub async fn get_reminders(id: u64, ctx: &State, pool: &State u64 { pub struct EmbedField { title: String, value: String, -} - -#[derive(Serialize, Deserialize)] -pub struct JsonReminder { - attachment: Option, - attachment_name: Option, - avatar: Option, - #[serde(with = "string")] - channel: u64, - content: String, - embed_author: String, - embed_author_url: Option, - embed_color: u32, - embed_description: String, - embed_footer: String, - embed_footer_url: Option, - embed_image_url: Option, - embed_thumbnail_url: Option, - embed_title: String, - embed_fields: Vec, - enabled: bool, - expires: Option, - interval_seconds: Option, - interval_months: Option, - #[serde(default = "name_default")] - name: String, - pin: bool, - restartable: bool, - tts: bool, - #[serde(default)] - uid: String, - username: Option, - utc_time: NaiveDateTime, + inline: bool, } #[derive(Serialize, Deserialize)] @@ -82,6 +50,7 @@ pub struct Reminder { embed_image_url: Option, embed_thumbnail_url: Option, embed_title: String, + embed_fields: Option>>, enabled: bool, expires: Option, interval_seconds: Option, @@ -130,7 +99,7 @@ pub struct PatchReminder { #[serde(default)] embed_title: Unset, #[serde(default)] - embed_fields: Unset, + embed_fields: Unset>>, #[serde(default)] enabled: Unset, #[serde(default)] diff --git a/web/src/routes/dashboard/user.rs b/web/src/routes/dashboard/user.rs index 72817af..882621a 100644 --- a/web/src/routes/dashboard/user.rs +++ b/web/src/routes/dashboard/user.rs @@ -11,14 +11,13 @@ use serde::{Deserialize, Serialize}; use serenity::{ client::Context, model::{ - id::{GuildId, RoleId, UserId}, + id::{GuildId, RoleId}, permissions::Permissions, }, }; use sqlx::{MySql, Pool}; -use super::Reminder; -use crate::{consts::DISCORD_API, routes::dashboard::DeleteReminder}; +use crate::consts::DISCORD_API; #[derive(Serialize)] struct UserInfo { @@ -164,241 +163,3 @@ pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State, - _ctx: &State, - pool: &State>, -) -> JsonValue { - match sqlx::query!( - "INSERT INTO reminders ( - avatar, - content, - embed_author, - embed_author_url, - embed_color, - embed_description, - embed_footer, - embed_footer_url, - embed_image_url, - embed_thumbnail_url, - embed_title, - enabled, - expires, - interval_seconds, - interval_months, - name, - pin, - restartable, - tts, - username, - `utc_time` - ) VALUES ( - avatar = ?, - content = ?, - embed_author = ?, - embed_author_url = ?, - embed_color = ?, - embed_description = ?, - embed_footer = ?, - embed_footer_url = ?, - embed_image_url = ?, - embed_thumbnail_url = ?, - embed_title = ?, - enabled = ?, - expires = ?, - interval_seconds = ?, - interval_months = ?, - name = ?, - pin = ?, - restartable = ?, - tts = ?, - username = ?, - `utc_time` = ? - )", - reminder.avatar, - reminder.content, - reminder.embed_author, - reminder.embed_author_url, - reminder.embed_color, - reminder.embed_description, - reminder.embed_footer, - reminder.embed_footer_url, - reminder.embed_image_url, - reminder.embed_thumbnail_url, - reminder.embed_title, - reminder.enabled, - reminder.expires, - reminder.interval_seconds, - reminder.interval_months, - reminder.name, - reminder.pin, - reminder.restartable, - reminder.tts, - reminder.username, - reminder.utc_time, - ) - .execute(pool.inner()) - .await - { - Ok(_) => { - json!({}) - } - Err(e) => { - warn!("Error in `create_reminder`: {:?}", e); - - json!({"error": "Could not create reminder"}) - } - } -} - -#[get("/api/user/reminders")] -pub async fn get_reminders( - pool: &State>, - cookies: &CookieJar<'_>, - ctx: &State, -) -> JsonValue { - if let Some(user_id) = - cookies.get_private("userid").map(|c| c.value().parse::().ok()).flatten() - { - let query_res = sqlx::query!( - "SELECT channel FROM channels INNER JOIN users ON users.dm_channel = channels.id WHERE users.user = ?", - user_id - ) - .fetch_one(pool.inner()) - .await; - - let dm_channel = if let Ok(query) = query_res { - Some(query.channel) - } else { - if let Ok(dm_channel) = UserId(user_id).create_dm_channel(&ctx.inner()).await { - Some(dm_channel.id.as_u64().to_owned()) - } else { - None - } - }; - - if let Some(channel_id) = dm_channel { - let reminders = sqlx::query_as!( - Reminder, - r#"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, - reminders.enabled as "enabled:_", - reminders.expires, - reminders.interval_seconds, - reminders.interval_months, - reminders.name, - reminders.pin as "pin:_", - reminders.restartable as "restartable:_", - reminders.tts as "tts:_", - reminders.uid, - reminders.username, - reminders.utc_time - FROM reminders INNER JOIN channels ON channels.id = reminders.channel_id WHERE channels.channel = ?"#, - channel_id - ) - .fetch_all(pool.inner()) - .await - .unwrap_or(vec![]); - - json!(reminders) - } else { - json!({"error": "User's DM channel could not be determined"}) - } - } else { - json!({"error": "Not authorized"}) - } -} - -#[put("/api/user/reminders", data = "")] -pub async fn overwrite_reminder(reminder: Json, pool: &State>) -> JsonValue { - match sqlx::query!( - "UPDATE reminders SET - avatar = ?, - content = ?, - embed_author = ?, - embed_author_url = ?, - embed_color = ?, - embed_description = ?, - embed_footer = ?, - embed_footer_url = ?, - embed_image_url = ?, - embed_thumbnail_url = ?, - embed_title = ?, - enabled = ?, - expires = ?, - interval_seconds = ?, - interval_months = ?, - name = ?, - pin = ?, - restartable = ?, - tts = ?, - username = ?, - `utc_time` = ? - WHERE uid = ?", - reminder.avatar, - reminder.content, - reminder.embed_author, - reminder.embed_author_url, - reminder.embed_color, - reminder.embed_description, - reminder.embed_footer, - reminder.embed_footer_url, - reminder.embed_image_url, - reminder.embed_thumbnail_url, - reminder.embed_title, - reminder.enabled, - reminder.expires, - reminder.interval_seconds, - reminder.interval_months, - reminder.name, - reminder.pin, - reminder.restartable, - reminder.tts, - reminder.username, - reminder.utc_time, - reminder.uid - ) - .execute(pool.inner()) - .await - { - Ok(_) => { - json!({}) - } - Err(e) => { - warn!("Error in `overwrite_reminder`: {:?}", e); - - json!({"error": "Could not modify reminder"}) - } - } -} - -#[delete("/api/user/reminders", data = "")] -pub async fn delete_reminder( - reminder: Json, - pool: &State>, -) -> JsonValue { - if sqlx::query!("DELETE FROM reminders WHERE uid = ?", reminder.uid) - .execute(pool.inner()) - .await - .is_ok() - { - json!({}) - } else { - json!({"error": "Could not delete reminder"}) - } -} diff --git a/web/static/js/interval.js b/web/static/js/interval.js index 6245980..a28d75a 100644 --- a/web/static/js/interval.js +++ b/web/static/js/interval.js @@ -5,8 +5,6 @@ function get_interval(element) { let minutes = element.querySelector('input[name="interval_minutes"]').value; let seconds = element.querySelector('input[name="interval_seconds"]').value; - console.log(minutes); - return { months: parseInt(months) || null, seconds: @@ -53,7 +51,7 @@ function update_interval(element) { } } -let $intervalGroup = document.querySelector(".interval-group"); +const $intervalGroup = document.querySelector(".interval-group"); document.querySelector(".interval-group").addEventListener( "blur", @@ -73,9 +71,13 @@ document.addEventListener("remindersLoaded", (event) => { for (reminder of event.detail) { let $intervalGroup = reminder.node.querySelector(".interval-group"); - $intervalGroup.addEventListener("blur", (ev) => { - if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup); - }); + $intervalGroup.addEventListener( + "blur", + (ev) => { + if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup); + }, + true + ); $intervalGroup.querySelector("button.clear").addEventListener("click", () => { $intervalGroup.querySelectorAll("input").forEach((el) => { diff --git a/web/static/js/main.js b/web/static/js/main.js index 0da0c54..c6a336e 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -5,10 +5,10 @@ const $colorPickerModal = document.querySelector("div#pickColorModal"); const $colorPickerInput = $colorPickerModal.querySelector("input"); const $deleteReminderBtn = document.querySelector("#delete-reminder-confirm"); const $reminderTemplate = document.querySelector("template#guildReminder"); +const $embedFieldTemplate = document.querySelector("template#embedFieldTemplate"); let channels; let roles; -let guild_id; function colorToInt(r, g, b) { return (r << 16) + (g << 8) + b; @@ -118,10 +118,10 @@ async function fetch_reminders(guild_id) { for (let reminder of data) { let newFrame = $reminderTemplate.content.cloneNode(true); - newFrame.querySelector(".reminderContent").dataset.uid = + newFrame.querySelector(".reminderContent").dataset["uid"] = reminder["uid"]; - render_reminder(reminder, newFrame); + deserialize_reminder(reminder, newFrame); $reminderBox.appendChild(newFrame); @@ -137,7 +137,87 @@ async function fetch_reminders(guild_id) { }); } -function render_reminder(reminder, frame) { +async function serialize_reminder(node) { + let interval = get_interval(node); + + let rgb_color = window.getComputedStyle( + node.querySelector("div.discord-embed") + ).borderLeftColor; + let rgb = rgb_color.match(/\d+/g); + let color = colorToInt(parseInt(rgb[0]), parseInt(rgb[1]), parseInt(rgb[2])); + + let utc_time = luxon.DateTime.fromISO( + node.querySelector('input[name="time"]').value + ).setZone("UTC"); + + if (utc_time.invalid) { + return { error: "Time provided invalid." }; + } + + let fields = [ + ...node.querySelectorAll("div.embed-multifield-box div.embed-field-box"), + ] + .map((el) => { + return { + title: el.querySelector("textarea#embedFieldTitle").value, + value: el.querySelector("textarea#embedFieldValue").value, + inline: el.dataset["inlined"] === "1", + }; + }) + .filter(({ title, value, inline }) => title.length + value.length > 0); + + let attachment = null; + let attachment_name = null; + + if (node.querySelector('input[name="attachment"]').files.length > 0) { + let file = node.querySelector('input[name="attachment"]').files[0]; + + attachment = await new Promise((resolve) => { + let fileReader = new FileReader(); + fileReader.onload = (e) => resolve(fileReader.result); + fileReader.readAsDataURL(file); + }); + attachment = attachment.split(",")[1]; + attachment_name = file.name; + } + + const reminderContent = node.closest(".reminderContent"); + + return { + // if we're creating a reminder, ignore this field + uid: reminderContent !== null ? reminderContent.dataset["uid"] : "", + // if we're editing a reminder, ignore this field + enabled: reminderContent !== null ? null : true, + restartable: false, + attachment: attachment, + attachment_name: attachment_name, + avatar: has_source(node.querySelector("img.discord-avatar").src), + channel: node.querySelector("select.channel-selector").value, + content: node.querySelector('textarea[name="content"]').value, + embed_author_url: has_source(node.querySelector("img.embed_author_url").src), + embed_author: node.querySelector('textarea[name="embed_author"]').value, + embed_color: color, + embed_description: node.querySelector('textarea[name="embed_description"]').value, + embed_footer: node.querySelector('textarea[name="embed_footer"]').value, + embed_footer_url: has_source(node.querySelector("img.embed_footer_url").src), + embed_image_url: has_source(node.querySelector("img.embed_image_url").src), + embed_thumbnail_url: has_source( + node.querySelector("img.embed_thumbnail_url").src + ), + embed_title: node.querySelector('textarea[name="embed_title"]').value, + embed_fields: fields, + expires: null, + interval_seconds: interval.seconds, + interval_months: interval.months, + name: node.querySelector('input[name="name"]').value, + pin: node.querySelector('input[name="pin"]').checked, + tts: node.querySelector('input[name="tts"]').checked, + username: node.querySelector('input[name="username"]').value, + utc_time: utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"), + }; +} + +function deserialize_reminder(reminder, frame) { // populate channels set_channels(frame.querySelector("select.channel-selector")); @@ -161,10 +241,25 @@ function render_reminder(reminder, frame) { } } + const lastChild = frame.querySelector("div.embed-multifield-box .embed-field-box"); + + for (let field of reminder["embed_fields"]) { + let embed_field = $embedFieldTemplate.content.cloneNode(true); + embed_field.querySelector("textarea.discord-field-title").value = field["title"]; + embed_field.querySelector("textarea.discord-field-value").value = field["value"]; + embed_field.querySelector(".embed-field-box").dataset["inlined"] = field["inline"] + ? "1" + : "0"; + + frame + .querySelector("div.embed-multifield-box") + .insertBefore(embed_field, lastChild); + } + if (reminder["interval_seconds"] !== null) update_interval(frame); let $enableBtn = frame.querySelector(".disable-enable"); - $enableBtn.dataset.action = reminder["enabled"] ? "disable" : "enable"; + $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable"; let timeInput = frame.querySelector('input[name="time"]'); let localTime = luxon.DateTime.fromISO(reminder["utc_time"], { zone: "UTC" }).setZone( @@ -223,7 +318,7 @@ document.addEventListener("remindersLoaded", (event) => { const enableBtn = node.querySelector(".disable-enable"); enableBtn.addEventListener("click", () => { - let enable = enableBtn.dataset.action === "enable"; + let enable = enableBtn.dataset["action"] === "enable"; fetch(`/dashboard/api/guild/${guild}/reminders`, { method: "PATCH", @@ -237,7 +332,9 @@ document.addEventListener("remindersLoaded", (event) => { if (data.error) { show_error(data.error); } else { - enableBtn.dataset.action = data["enabled"] ? "enable" : "disable"; + enableBtn.dataset["action"] = data["enabled"] + ? "enable" + : "disable"; } }); }); @@ -249,66 +346,17 @@ document.addEventListener("remindersLoaded", (event) => { const $saveBtn = node.querySelector("button.save-btn"); - $saveBtn.addEventListener("click", (event) => { + $saveBtn.addEventListener("click", async (event) => { $saveBtn.querySelector("span.icon > i").classList = [ "fas fa-spinner fa-spin", ]; - let interval = get_interval(node); + let reminder = await serialize_reminder(node); + if (reminder.error) { + show_error(reminder.error); + return; + } - let rgb_color = window.getComputedStyle( - node.querySelector("div.discord-embed") - ).borderLeftColor; - let rgb = rgb_color.match(/\d+/g); - let color = colorToInt(parseInt(rgb[0]), parseInt(rgb[1]), parseInt(rgb[2])); - - let utc_time = luxon.DateTime.fromISO( - node.querySelector('input[name="time"]').value - ).setZone("UTC"); - - let fields = node.querySelectorAll(".embed-field-box", (el) => { - return { - title: el.querySelector('input[name="embed_field_title[]"]').value, - value: el.querySelector('input[name="embed_field_value[]"]').value, - }; - }); - - let reminder = { - uid: node.closest(".reminderContent").dataset["uid"], - avatar: has_source(node.querySelector("img.discord-avatar").src), - channel: node.querySelector("select.channel-selector").value, - content: node.querySelector('textarea[name="content"]').value, - embed_author_url: has_source( - node.querySelector("img.embed_author_url").src - ), - embed_author: node.querySelector('textarea[name="embed_author"]').value, - embed_color: color, - embed_description: node.querySelector( - 'textarea[name="embed_description"]' - ).value, - embed_footer: node.querySelector('textarea[name="embed_footer"]').value, - embed_footer_url: has_source( - node.querySelector("img.embed_footer_url").src - ), - embed_image_url: has_source( - node.querySelector("img.embed_image_url").src - ), - embed_thumbnail_url: has_source( - node.querySelector("img.embed_thumbnail_url").src - ), - embed_title: node.querySelector('textarea[name="embed_title"]').value, - embed_fields: fields, - expires: null, - interval_seconds: interval.seconds, - interval_months: interval.months, - name: node.querySelector('input[name="name"]').value, - pin: node.querySelector('input[name="pin"]').checked, - tts: node.querySelector('input[name="tts"]').checked, - username: node.querySelector('input[name="username"]').value, - utc_time: utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"), - }; - - // send to server let guild = document.querySelector(".guildList a.is-active").dataset["guild"]; fetch(`/dashboard/api/guild/${guild}/reminders`, { @@ -319,7 +367,9 @@ document.addEventListener("remindersLoaded", (event) => { body: JSON.stringify(reminder), }) .then((response) => response.json()) - .then((data) => console.log(data)); + .then((data) => { + for (let error of data.errors) show_error(error); + }); $saveBtn.querySelector("span.icon > i").classList = ["fas fa-check"]; @@ -384,7 +434,7 @@ document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll(".navbar-burger").forEach((el) => { el.addEventListener("click", () => { - const target = el.dataset.target; + const target = el.dataset["target"]; const $target = document.getElementById(target); el.classList.toggle("is-active"); @@ -434,8 +484,6 @@ document.addEventListener("DOMContentLoaded", () => { $anchor.addEventListener("click", async (e) => { e.preventDefault(); - guild_id = guild.id; - const event = new CustomEvent("guildSwitched", { detail: { guild_name: guild.name, @@ -488,35 +536,6 @@ let $createBtn = $createReminder.querySelector("button#createReminder"); $createBtn.addEventListener("click", async () => { $createBtn.querySelector("span.icon > i").classList = ["fas fa-spinner fa-spin"]; - let interval = get_interval($createReminder); - - let rgb_color = window.getComputedStyle( - $createReminder.querySelector("div.discord-embed") - ).borderLeftColor; - let rgb = rgb_color.match(/\d+/g); - let color = colorToInt(parseInt(rgb[0]), parseInt(rgb[1]), parseInt(rgb[2])); - - let utc_time = luxon.DateTime.fromISO( - $createReminder.querySelector('input[name="time"]').value - ).setZone("UTC"); - - if (utc_time.invalid) { - show_error("Time provided invalid."); - $createBtn.querySelector("span.icon > i").classList = ["fas fa-sparkles"]; - return; - } - - let fields = [ - ...$createReminder.querySelectorAll( - "div.embed-multifield-box div.embed-field-box" - ), - ].map((el) => { - return { - title: el.querySelector("textarea#embedFieldTitle").value, - value: el.querySelector("textarea#embedFieldValue").value, - }; - }); - let attachment = null; let attachment_name = null; @@ -532,44 +551,12 @@ $createBtn.addEventListener("click", async () => { attachment_name = file.name; } - let reminder = { - attachment: attachment, - attachment_name: attachment_name, - avatar: has_source($createReminder.querySelector("img.discord-avatar").src), - channel: $createReminder.querySelector("select.channel-selector").value, - content: $createReminder.querySelector("textarea#messageContent").value, - embed_author_url: has_source( - $createReminder.querySelector("img.embed_author_url").src - ), - embed_author: $createReminder.querySelector("textarea#embedAuthor").value, - embed_color: color, - embed_description: $createReminder.querySelector("textarea#embedDescription") - .value, - embed_footer: $createReminder.querySelector("textarea#embedFooter").value, - embed_footer_url: has_source( - $createReminder.querySelector("img.embed_footer_url").src - ), - embed_image_url: has_source( - $createReminder.querySelector("img.embed_image_url").src - ), - embed_thumbnail_url: has_source( - $createReminder.querySelector("img.embed_thumbnail_url").src - ), - embed_title: $createReminder.querySelector("textarea#embedTitle").value, - embed_fields: fields, - enabled: true, - expires: null, - interval_seconds: interval.seconds, - interval_months: interval.months, - name: $createReminder.querySelector('input[name="name"]').value, - pin: $createReminder.querySelector('input[name="pin"]').checked, - restartable: false, - tts: $createReminder.querySelector('input[name="tts"]').checked, - username: $createReminder.querySelector("input#reminderUsername").value, - utc_time: utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"), - }; + let reminder = await serialize_reminder($createReminder); + if (reminder.error) { + show_error(reminder.error); + return; + } - // send to server let guild = document.querySelector(".guildList a.is-active").dataset["guild"]; fetch(`/dashboard/api/guild/${guild}/reminders`, { @@ -589,7 +576,7 @@ $createBtn.addEventListener("click", async () => { newFrame.querySelector(".reminderContent").dataset["uid"] = data["uid"]; - render_reminder(data, newFrame); + deserialize_reminder(data, newFrame); $reminderBox.appendChild(newFrame); @@ -682,19 +669,10 @@ document.addEventListener("remindersLoaded", () => { window.getComputedStyle($discordFrame).borderLeftColor; }); }); - - document.querySelectorAll(".embed-field-box button.inline-btn").forEach((el) => { - el.addEventListener("click", (ev) => { - let inlined = ev.target.closest(".embed-field-box").dataset["inlined"]; - ev.target.closest(".embed-field-box").dataset["inlined"] = - inlined === "1" ? "0" : "1"; - }); - }); }); function check_embed_fields() { document.querySelectorAll(".embed-field-box").forEach((element) => { - const $template = document.querySelector("template#embedFieldTemplate"); const $titleInput = element.querySelector(".discord-field-title"); const $valueInput = element.querySelector(".discord-field-value"); @@ -726,16 +704,7 @@ function check_embed_fields() { $valueInput.value !== "" && element.nextElementSibling === null ) { - const $clone = $template.content.cloneNode(true); - $clone - .querySelector(".embed-field-box button.inline-btn") - .addEventListener("click", (ev) => { - let inlined = - ev.target.closest(".embed-field-box").dataset["inlined"]; - ev.target.closest(".embed-field-box").dataset["inlined"] = - inlined == "1" ? "0" : "1"; - }); - + const $clone = $embedFieldTemplate.content.cloneNode(true); element.parentElement.append($clone); } }); @@ -746,16 +715,7 @@ function check_embed_fields() { $valueInput.value !== "" && element.nextElementSibling === null ) { - const $clone = $template.content.cloneNode(true); - $clone - .querySelector(".embed-field-box button.inline-btn") - .addEventListener("click", (ev) => { - let inlined = - ev.target.closest(".embed-field-box").dataset["inlined"]; - ev.target.closest(".embed-field-box").dataset["inlined"] = - inlined == "1" ? "0" : "1"; - }); - + const $clone = $embedFieldTemplate.content.cloneNode(true); element.parentElement.append($clone); } }); @@ -780,3 +740,11 @@ document.addEventListener("DOMNodeInserted", () => { check_embed_fields(); resize_textareas(); }); + +document.addEventListener("click", (ev) => { + if (ev.target.closest("button.inline-btn") !== null) { + let inlined = ev.target.closest(".embed-field-box").dataset["inlined"]; + ev.target.closest(".embed-field-box").dataset["inlined"] = + inlined == "1" ? "0" : "1"; + } +}); diff --git a/web/templates/dashboard.html.tera b/web/templates/dashboard.html.tera index 93b5926..3326749 100644 --- a/web/templates/dashboard.html.tera +++ b/web/templates/dashboard.html.tera @@ -140,6 +140,23 @@ + +