diff --git a/Cargo.lock b/Cargo.lock index 935fb9b..24e01fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2359,7 +2359,7 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reminder-rs" -version = "1.7.1" +version = "1.7.2" dependencies = [ "base64 0.21.7", "chrono", diff --git a/Cargo.toml b/Cargo.toml index b6f2322..839045f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reminder-rs" -version = "1.7.1" +version = "1.7.2" authors = ["Jude Southworth "] edition = "2021" license = "AGPL-3.0 only" diff --git a/reminder-dashboard/src/api.ts b/reminder-dashboard/src/api.ts index 376a42b..2aad82f 100644 --- a/reminder-dashboard/src/api.ts +++ b/reminder-dashboard/src/api.ts @@ -144,9 +144,9 @@ export const postGuildReminder = (guild: string) => ({ axios.post(`/dashboard/api/guild/${guild}/reminders`, reminder).then((resp) => resp.data), }); -export const deleteGuildReminder = (guild: string) => ({ +export const deleteReminder = () => ({ mutationFn: (reminder: Reminder) => - axios.delete(`/dashboard/api/guild/${guild}/reminders`, { + axios.delete(`/dashboard/api/reminders`, { data: { uid: reminder.uid, }, @@ -187,3 +187,7 @@ export const postUserReminder = () => ({ mutationFn: (reminder: Reminder) => axios.post(`/dashboard/api/user/reminders`, reminder).then((resp) => resp.data), }); + +export const patchUserReminder = () => ({ + mutationFn: (reminder: Reminder) => axios.patch(`/dashboard/api/user/reminders`, reminder), +}); diff --git a/reminder-dashboard/src/components/Reminder/ButtonRow/CreateButtonRow.tsx b/reminder-dashboard/src/components/Reminder/ButtonRow/CreateButtonRow.tsx index b163e93..2a19d0b 100644 --- a/reminder-dashboard/src/components/Reminder/ButtonRow/CreateButtonRow.tsx +++ b/reminder-dashboard/src/components/Reminder/ButtonRow/CreateButtonRow.tsx @@ -2,7 +2,6 @@ import { LoadTemplate } from "../LoadTemplate"; import { useReminder } from "../ReminderContext"; import { useMutation, useQueryClient } from "react-query"; import { postGuildReminder, postGuildTemplate, postUserReminder } from "../../../api"; -import { useParams } from "wouter"; import { useState } from "preact/hooks"; import { ICON_FLASH_TIME } from "../../../consts"; import { useFlash } from "../../App/FlashContext"; @@ -96,34 +95,36 @@ export const CreateButtonRow = () => { )} -
-
- + {guild && ( +
+
+ +
+
+ +
-
- -
-
+ )}
); }; diff --git a/reminder-dashboard/src/components/Reminder/ButtonRow/DeleteButton.tsx b/reminder-dashboard/src/components/Reminder/ButtonRow/DeleteButton.tsx index 1dbf33f..d5ad7e7 100644 --- a/reminder-dashboard/src/components/Reminder/ButtonRow/DeleteButton.tsx +++ b/reminder-dashboard/src/components/Reminder/ButtonRow/DeleteButton.tsx @@ -2,9 +2,10 @@ import { useState } from "preact/hooks"; import { Modal } from "../../Modal"; import { useMutation, useQueryClient } from "react-query"; import { useReminder } from "../ReminderContext"; -import { deleteGuildReminder } from "../../../api"; +import { deleteReminder } from "../../../api"; import { useParams } from "wouter"; import { useFlash } from "../../App/FlashContext"; +import { useGuild } from "../../App/useGuild"; export const DeleteButton = () => { const [modalOpen, setModalOpen] = useState(false); @@ -26,20 +27,26 @@ export const DeleteButton = () => { const DeleteModal = ({ setModalOpen }) => { const [reminder] = useReminder(); - const { guild } = useParams(); + const guild = useGuild(); const flash = useFlash(); const queryClient = useQueryClient(); const mutation = useMutation({ - ...deleteGuildReminder(guild), + ...deleteReminder(), onSuccess: () => { flash({ message: "Reminder deleted", type: "success", }); - queryClient.invalidateQueries({ - queryKey: ["GUILD_REMINDERS", guild], - }); + if (guild) { + queryClient.invalidateQueries({ + queryKey: ["GUILD_REMINDERS", guild], + }); + } else { + queryClient.invalidateQueries({ + queryKey: ["USER_REMINDERS"], + }); + } setModalOpen(false); }, }); diff --git a/reminder-dashboard/src/components/Reminder/ButtonRow/EditButtonRow.tsx b/reminder-dashboard/src/components/Reminder/ButtonRow/EditButtonRow.tsx index a47b74f..5c4a281 100644 --- a/reminder-dashboard/src/components/Reminder/ButtonRow/EditButtonRow.tsx +++ b/reminder-dashboard/src/components/Reminder/ButtonRow/EditButtonRow.tsx @@ -1,14 +1,14 @@ import { useRef, useState } from "preact/hooks"; import { useMutation, useQueryClient } from "react-query"; -import { patchGuildReminder } from "../../../api"; -import { useParams } from "wouter"; +import { patchGuildReminder, patchUserReminder } from "../../../api"; import { ICON_FLASH_TIME } from "../../../consts"; import { DeleteButton } from "./DeleteButton"; import { useReminder } from "../ReminderContext"; import { useFlash } from "../../App/FlashContext"; +import { useGuild } from "../../App/useGuild"; export const EditButtonRow = () => { - const { guild } = useParams(); + const guild = useGuild(); const [reminder, setReminder] = useReminder(); const [recentlySaved, setRecentlySaved] = useState(false); @@ -18,11 +18,17 @@ export const EditButtonRow = () => { const flash = useFlash(); const mutation = useMutation({ - ...patchGuildReminder(guild), + ...(guild ? patchGuildReminder(guild) : patchUserReminder()), onSuccess: (response) => { - queryClient.invalidateQueries({ - queryKey: ["GUILD_REMINDERS", guild], - }); + if (guild) { + queryClient.invalidateQueries({ + queryKey: ["GUILD_REMINDERS", guild], + }); + } else { + queryClient.invalidateQueries({ + queryKey: ["USER_REMINDERS"], + }); + } if (iconFlashTimeout.current !== null) { clearTimeout(iconFlashTimeout.current); diff --git a/web/src/lib.rs b/web/src/lib.rs index ce75f7a..a45ab13 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -131,6 +131,7 @@ pub async fn initialize( routes![ routes::dashboard::dashboard, routes::dashboard::dashboard_home, + routes::dashboard::api::delete_reminder, routes::dashboard::api::user::get_user_info, routes::dashboard::api::user::update_user_info, routes::dashboard::api::user::get_user_guilds, @@ -145,7 +146,6 @@ pub async fn initialize( routes::dashboard::api::guild::create_guild_reminder, routes::dashboard::api::guild::get_reminders, routes::dashboard::api::guild::edit_reminder, - routes::dashboard::api::guild::delete_reminder, routes::dashboard::export::export_reminders, routes::dashboard::export::export_reminder_templates, routes::dashboard::export::export_todos, diff --git a/web/src/routes/dashboard/api/guild/reminders.rs b/web/src/routes/dashboard/api/guild/reminders.rs index 6c94a31..3acd713 100644 --- a/web/src/routes/dashboard/api/guild/reminders.rs +++ b/web/src/routes/dashboard/api/guild/reminders.rs @@ -14,9 +14,7 @@ use crate::{ consts::MIN_INTERVAL, guards::transaction::Transaction, routes::{ - dashboard::{ - create_database_channel, create_reminder, DeleteReminder, PatchReminder, Reminder, - }, + dashboard::{create_database_channel, create_reminder, PatchReminder, Reminder}, JsonResult, }, Database, @@ -364,27 +362,3 @@ pub async fn edit_reminder( } } } - -#[delete("/api/guild//reminders", data = "")] -pub async fn delete_reminder( - cookies: &CookieJar<'_>, - id: u64, - reminder: Json, - ctx: &State, - pool: &State>, -) -> JsonResult { - check_authorization(cookies, ctx.inner(), id).await?; - - match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) - .execute(pool.inner()) - .await - { - Ok(_) => Ok(json!({})), - - Err(e) => { - warn!("Error in `delete_reminder`: {:?}", e); - - Err(json!({"error": "Could not delete reminder"})) - } - } -} diff --git a/web/src/routes/dashboard/api/mod.rs b/web/src/routes/dashboard/api/mod.rs index 56d877f..955d563 100644 --- a/web/src/routes/dashboard/api/mod.rs +++ b/web/src/routes/dashboard/api/mod.rs @@ -1,2 +1,41 @@ pub mod guild; pub mod user; + +use rocket::{ + http::CookieJar, + serde::json::{json, Json}, + State, +}; +use serenity::client::Context; +use sqlx::{MySql, Pool}; + +use crate::routes::{dashboard::DeleteReminder, JsonResult}; + +#[delete("/api/reminders", data = "")] +pub async fn delete_reminder( + cookies: &CookieJar<'_>, + reminder: Json, + pool: &State>, +) -> JsonResult { + match cookies.get_private("userid").map(|c| c.value().parse::().ok()).flatten() { + Some(_) => { + match sqlx::query!( + "UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", + reminder.uid + ) + .execute(pool.inner()) + .await + { + Ok(_) => Ok(json!({})), + + Err(e) => { + warn!("Error in `delete_reminder`: {:?}", e); + + Err(json!({"error": "Could not delete reminder"})) + } + } + } + + None => Err(json!({"error": "User not authorized"})), + } +} diff --git a/web/src/routes/dashboard/api/user/models.rs b/web/src/routes/dashboard/api/user/models.rs index 6b64fc8..d86b8b9 100644 --- a/web/src/routes/dashboard/api/user/models.rs +++ b/web/src/routes/dashboard/api/user/models.rs @@ -15,7 +15,10 @@ use crate::{ }, guards::transaction::Transaction, routes::{ - dashboard::{create_database_channel, generate_uid, name_default, Attachment, EmbedField}, + dashboard::{ + create_database_channel, deserialize_optional_field, generate_uid, interval_default, + name_default, Attachment, EmbedField, Unset, + }, JsonResult, }, Error, @@ -228,3 +231,60 @@ pub async fn create_reminder( } } } + +#[derive(Deserialize)] +pub struct PatchReminder { + pub uid: String, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_field")] + pub attachment: Unset>, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_field")] + pub attachment_name: Unset>, + #[serde(default)] + pub content: Unset, + #[serde(default)] + pub embed_author: Unset, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_field")] + pub embed_author_url: Unset>, + #[serde(default)] + pub embed_color: Unset, + #[serde(default)] + pub embed_description: Unset, + #[serde(default)] + pub embed_footer: Unset, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_field")] + pub embed_footer_url: Unset>, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_field")] + pub embed_image_url: Unset>, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_field")] + pub embed_thumbnail_url: Unset>, + #[serde(default)] + pub embed_title: Unset, + #[serde(default)] + pub embed_fields: Unset>>, + #[serde(default)] + pub enabled: Unset, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_field")] + pub expires: Unset>, + #[serde(default = "interval_default")] + #[serde(deserialize_with = "deserialize_optional_field")] + pub interval_seconds: Unset>, + #[serde(default = "interval_default")] + #[serde(deserialize_with = "deserialize_optional_field")] + pub interval_days: Unset>, + #[serde(default = "interval_default")] + #[serde(deserialize_with = "deserialize_optional_field")] + pub interval_months: Unset>, + #[serde(default)] + pub name: Unset, + #[serde(default)] + pub tts: Unset, + #[serde(default)] + pub utc_time: Unset, +} diff --git a/web/src/routes/dashboard/api/user/reminders.rs b/web/src/routes/dashboard/api/user/reminders.rs index f2da8b8..def9638 100644 --- a/web/src/routes/dashboard/api/user/reminders.rs +++ b/web/src/routes/dashboard/api/user/reminders.rs @@ -7,11 +7,16 @@ use serenity::{client::Context, model::id::UserId}; use sqlx::{MySql, Pool}; use crate::{ + check_subscription, guards::transaction::Transaction, routes::{ - dashboard::api::user::models::{create_reminder, Reminder}, + dashboard::{ + api::user::models::{create_reminder, Reminder}, + PatchReminder, MIN_INTERVAL, + }, JsonResult, }, + Database, }; #[post("/api/user/reminders", data = "")] @@ -103,3 +108,174 @@ pub async fn get_reminders( } } } + +#[patch("/api/user/reminders", data = "")] +pub async fn edit_reminder( + reminder: Json, + ctx: &State, + mut transaction: Transaction<'_>, + pool: &State>, + cookies: &CookieJar<'_>, +) -> JsonResult { + let user_id_cookie = + cookies.get_private("userid").map(|c| c.value().parse::().ok()).flatten(); + + if user_id_cookie.is_none() { + return Err(json!({"error": "User not authorized"})); + } + + let mut error = vec![]; + let user_id = user_id_cookie.unwrap(); + + if reminder.message_ok() { + update_field!(transaction.executor(), error, reminder.[ + content, + embed_author, + embed_description, + embed_footer, + embed_title, + embed_fields + ]); + } else { + error.push("Message exceeds limits.".to_string()); + } + + update_field!(transaction.executor(), error, reminder.[ + attachment, + attachment_name, + embed_author_url, + embed_color, + embed_footer_url, + embed_image_url, + embed_thumbnail_url, + enabled, + expires, + name, + tts, + utc_time + ]); + + if reminder.interval_days.flatten().is_some() + || reminder.interval_months.flatten().is_some() + || reminder.interval_seconds.flatten().is_some() + { + if check_subscription(&ctx.inner(), user_id).await { + let new_interval_length = match reminder.interval_days { + Some(interval) => interval.unwrap_or(0), + None => sqlx::query!( + "SELECT interval_days AS days FROM reminders WHERE uid = ?", + reminder.uid + ) + .fetch_one(transaction.executor()) + .await + .map_err(|e| { + warn!("Error updating reminder interval: {:?}", e); + json!({ "reminder": Option::::None, "errors": vec!["Unknown error"] }) + })? + .days + .unwrap_or(0), + } * 86400 + match reminder.interval_months { + Some(interval) => interval.unwrap_or(0), + None => sqlx::query!( + "SELECT interval_months AS months FROM reminders WHERE uid = ?", + reminder.uid + ) + .fetch_one(transaction.executor()) + .await + .map_err(|e| { + warn!("Error updating reminder interval: {:?}", e); + json!({ "reminder": Option::::None, "errors": vec!["Unknown error"] }) + })? + .months + .unwrap_or(0), + } * 2592000 + match reminder.interval_seconds { + Some(interval) => interval.unwrap_or(0), + None => sqlx::query!( + "SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?", + reminder.uid + ) + .fetch_one(transaction.executor()) + .await + .map_err(|e| { + warn!("Error updating reminder interval: {:?}", e); + json!({ "reminder": Option::::None, "errors": vec!["Unknown error"] }) + })? + .seconds + .unwrap_or(0), + }; + + if new_interval_length < *MIN_INTERVAL { + error.push(String::from("New interval is too short.")); + } else { + update_field!(transaction.executor(), error, reminder.[ + interval_days, + interval_months, + interval_seconds + ]); + } + } + } else { + sqlx::query!( + " + UPDATE reminders + SET interval_seconds = NULL, interval_days = NULL, interval_months = NULL + WHERE uid = ? + ", + reminder.uid + ) + .execute(transaction.executor()) + .await + .map_err(|e| { + warn!("Error updating reminder interval: {:?}", e); + json!({ "reminder": Option::::None, "errors": vec!["Unknown error"] }) + })?; + } + + if let Err(e) = transaction.commit().await { + warn!("Couldn't commit transaction: {:?}", e); + return json_err!("Couldn't commit transaction"); + } + + match sqlx::query_as_unchecked!( + Reminder, + " + SELECT reminders.attachment, + reminders.attachment_name, + 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.embed_fields, + reminders.enabled, + reminders.expires, + reminders.interval_seconds, + reminders.interval_days, + reminders.interval_months, + reminders.name, + reminders.tts, + reminders.uid, + reminders.utc_time + FROM reminders + LEFT JOIN channels ON channels.id = reminders.channel_id + WHERE uid = ? + ", + reminder.uid + ) + .fetch_one(pool.inner()) + .await + { + Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})), + + Err(e) => { + warn!("Error exiting `edit_reminder': {:?}", e); + + Err(json!({"reminder": Option::::None, "errors": vec!["Unknown error"]})) + } + } +}