fields are now json and work. fix for intervals. moved some code together

This commit is contained in:
jude 2022-04-07 17:13:02 +01:00
parent d946ef1dca
commit 85d27c5bba
13 changed files with 270 additions and 539 deletions

6
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -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<EmbedField>,
}
struct EmbedInner {
title: String,
description: String,
image_url: Option<String>,
@ -108,8 +107,10 @@ struct EmbedInner {
author: String,
author_url: Option<String>,
color: u32,
fields: Json<Vec<EmbedField>>,
}
#[derive(Deserialize)]
struct EmbedField {
title: String,
value: String,
@ -121,9 +122,9 @@ impl Embed {
pool: impl Executor<'_, Database = Database> + Copy,
id: u32,
) -> Option<Self> {
let mut inner = sqlx::query_as_unchecked!(
EmbedInner,
"
let mut embed = sqlx::query_as!(
Self,
r#"
SELECT
`embed_title` AS title,
`embed_description` AS description,
@ -133,64 +134,42 @@ SELECT
`embed_footer_url` AS footer_url,
`embed_author` AS author,
`embed_author_url` AS author_url,
`embed_color` AS color
FROM
reminders
WHERE
`id` = ?
",
`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<CreateEmbed> 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);
}

View File

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

View File

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

View File

@ -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<String, String> = 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,

View File

@ -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<Conte
#[post("/api/guild/<id>/reminders", data = "<reminder>")]
pub async fn create_reminder(
id: u64,
reminder: Json<JsonReminder>,
reminder: Json<Reminder>,
cookies: &CookieJar<'_>,
serenity_context: &State<Context>,
pool: &State<Pool<MySql>>,
@ -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<Context>, pool: &State<Pool<MySq
reminders.embed_image_url,
reminders.embed_thumbnail_url,
reminders.embed_title,
reminders.embed_fields,
reminders.enabled,
reminders.expires,
reminders.interval_seconds,
@ -421,6 +433,7 @@ pub async fn edit_reminder(
embed_image_url,
embed_thumbnail_url,
embed_title,
embed_fields,
enabled,
expires,
interval_seconds,
@ -507,6 +520,7 @@ pub async fn edit_reminder(
reminders.embed_image_url,
reminders.embed_thumbnail_url,
reminders.embed_title,
reminders.embed_fields,
reminders.enabled,
reminders.expires,
reminders.interval_seconds,

View File

@ -6,7 +6,7 @@ use rocket::{http::CookieJar, response::Redirect};
use rocket_dyn_templates::Template;
use serde::{Deserialize, Serialize};
use serenity::{http::Http, model::id::ChannelId};
use sqlx::Executor;
use sqlx::{types::Json, Executor};
use crate::{
consts::{CHARACTERS, DEFAULT_AVATAR},
@ -30,39 +30,7 @@ fn channel_default() -> u64 {
pub struct EmbedField {
title: String,
value: String,
}
#[derive(Serialize, Deserialize)]
pub struct JsonReminder {
attachment: Option<String>,
attachment_name: Option<String>,
avatar: Option<String>,
#[serde(with = "string")]
channel: 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: Vec<EmbedField>,
enabled: bool,
expires: Option<NaiveDateTime>,
interval_seconds: Option<u32>,
interval_months: Option<u32>,
#[serde(default = "name_default")]
name: String,
pin: bool,
restartable: bool,
tts: bool,
#[serde(default)]
uid: String,
username: Option<String>,
utc_time: NaiveDateTime,
inline: bool,
}
#[derive(Serialize, Deserialize)]
@ -82,6 +50,7 @@ pub struct Reminder {
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>,
@ -130,7 +99,7 @@ pub struct PatchReminder {
#[serde(default)]
embed_title: Unset<String>,
#[serde(default)]
embed_fields: Unset<EmbedField>,
embed_fields: Unset<Json<Vec<EmbedField>>>,
#[serde(default)]
enabled: Unset<bool>,
#[serde(default)]

View File

@ -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<Cli
json!({"error": "Not authorized"})
}
}
#[post("/api/user/reminders", data = "<reminder>")]
pub async fn create_reminder(
reminder: Json<Reminder>,
_ctx: &State<Context>,
pool: &State<Pool<MySql>>,
) -> 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<Pool<MySql>>,
cookies: &CookieJar<'_>,
ctx: &State<Context>,
) -> JsonValue {
if let Some(user_id) =
cookies.get_private("userid").map(|c| c.value().parse::<u64>().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 = "<reminder>")]
pub async fn overwrite_reminder(reminder: Json<Reminder>, pool: &State<Pool<MySql>>) -> 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 = "<reminder>")]
pub async fn delete_reminder(
reminder: Json<DeleteReminder>,
pool: &State<Pool<MySql>>,
) -> 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"})
}
}

View File

@ -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) => {
$intervalGroup.addEventListener(
"blur",
(ev) => {
if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
});
},
true
);
$intervalGroup.querySelector("button.clear").addEventListener("click", () => {
$intervalGroup.querySelectorAll("input").forEach((el) => {

View File

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

View File

@ -140,6 +140,23 @@
<button class="modal-close is-large close-modal" aria-label="close"></button>
</div>
<div class="modal" id="dataManagerModal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<label class="modal-card-title" for="urlInput">Import/Export Manager <a href="/help/iemanager"><span><i class="fa fa-question-circle"></i></span></a></label>
<button class="delete close-modal" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="has-text-centered">
<button class="button is-success is-outlined" id="import-data">Import Data</button>
<button class="button is-success" id="export-data">Export Data</button>
</div>
</section>
</div>
<button class="modal-close is-large close-modal" aria-label="close"></button>
</div>
<div class="modal" id="deleteReminderModal">
<div class="modal-background"></div>
<div class="modal-card">
@ -182,12 +199,15 @@
<ul class="menu-list guildList">
</ul>
<div class="aside-footer" style="position: fixed; bottom: 0;">
<div class="aside-footer" style="position: fixed; bottom: 0; width: 226px;">
<p class="menu-label">
Settings
</p>
<ul class="menu-list">
<li>
<a class="show-modal" data-modal="dataManagerModal">
<span class="icon"><i class="fas fa-exchange"></i></span> Import/Export
</a>
<a class="show-modal" data-modal="chooseTimezoneModal">
<span class="icon"><i class="fas fa-map-marked"></i></span> Timezone
</a>
@ -211,16 +231,6 @@
</g>
</svg>
<aside class="menu" style="display: flex; flex-direction: column; flex-grow: 1;">
<p class="menu-label">
Personal
</p>
<ul class="menu-list">
<li>
<a class="switch-pane" data-pane="personal">
<span class="icon"><i class="fas fa-map-pin"></i></span> @%username%
</a>
</li>
</ul>
<p class="menu-label">
Servers
</p>
@ -233,6 +243,9 @@
</p>
<ul class="menu-list">
<li>
<a class="show-modal" data-modal="dataManagerModal">
<span class="icon"><i class="fas fa-exchange"></i></span> Import/Export
</a>
<a class="show-modal" data-modal="chooseTimezoneModal">
<span class="icon"><i class="fas fa-map-marked"></i></span> Timezone
</a>