use chrono::{naive::NaiveDateTime, Utc}; use futures::TryFutureExt; use log::warn; use rocket::serde::json::json; use serde::{Deserialize, Serialize}; use serenity::{client::Context, model::id::UserId}; use sqlx::types::Json; use crate::utils::check_subscription; use crate::web::{ consts::{ DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_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_NAME_LENGTH, MAX_URL_LENGTH, MIN_INTERVAL, }, guards::transaction::Transaction, routes::{ dashboard::{create_database_channel, generate_uid, name_default, Attachment, EmbedField}, JsonResult, }, Error, }; #[derive(Serialize, Deserialize)] pub struct Reminder { pub attachment: Option, pub attachment_name: Option, pub content: String, pub embed_author: String, pub embed_author_url: Option, pub embed_color: u32, pub embed_description: String, pub embed_footer: String, pub embed_footer_url: Option, pub embed_image_url: Option, pub embed_thumbnail_url: Option, pub embed_title: String, pub embed_fields: Option>>, pub enabled: bool, pub expires: Option, pub interval_seconds: Option, pub interval_days: Option, pub interval_months: Option, #[serde(default = "name_default")] pub name: String, pub tts: bool, #[serde(default)] pub uid: String, pub utc_time: NaiveDateTime, } pub async fn create_reminder( ctx: &Context, transaction: &mut Transaction<'_>, user_id: UserId, reminder: Reminder, ) -> JsonResult { let channel = user_id .create_dm_channel(&ctx) .map_err(|e| Error::Serenity(e)) .and_then(|dm_channel| create_database_channel(&ctx, dm_channel.id, transaction)) .await; if let Err(e) = channel { warn!("`create_database_channel` returned an error code: {:?}", e); // Provide more specific error messages based on the error type let error_msg = match e { Error::MissingDiscordPermission(permission) => format!( "Please ensure the bot has the \"{}\" permission in the channel", permission ), _ => "Failed to configure channel for reminders.".to_string(), }; return Err(json!({"error": error_msg})); } let channel = channel.unwrap(); // validate lengths check_length!(MAX_NAME_LENGTH, reminder.name); check_length!(MAX_CONTENT_LENGTH, reminder.content); check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder.embed_description); 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_URL_LENGTH, reminder.embed_footer_url, reminder.embed_thumbnail_url, reminder.embed_author_url, reminder.embed_image_url ); // validate urls check_url_opt!( reminder.embed_footer_url, reminder.embed_thumbnail_url, reminder.embed_author_url, reminder.embed_image_url ); // validate time and interval if reminder.utc_time < Utc::now().naive_utc() { return Err(json!({"error": "Time must be in the future"})); } if reminder.interval_seconds.is_some() || reminder.interval_days.is_some() || reminder.interval_months.is_some() { if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32 + reminder.interval_days.unwrap_or(0) * DAY as u32 + reminder.interval_seconds.unwrap_or(0) < *MIN_INTERVAL { return Err(json!({"error": "Interval too short"})); } } // check patreon if necessary if reminder.interval_seconds.is_some() || reminder.interval_days.is_some() || reminder.interval_months.is_some() { if !check_subscription(&ctx, transaction.executor(), user_id, None).await { return Err(json!({"error": "Patreon is required to set intervals"})); } } let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() }; let new_uid = generate_uid(); // write to db match sqlx::query!( "INSERT INTO reminders ( uid, attachment, attachment_name, channel_id, content, embed_author, embed_author_url, embed_color, embed_description, embed_footer, embed_footer_url, embed_image_url, embed_thumbnail_url, embed_title, embed_fields, enabled, expires, interval_seconds, interval_days, interval_months, name, tts, `utc_time` ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new_uid, reminder.attachment, reminder.attachment_name, channel, 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.embed_fields, reminder.enabled, reminder.expires, reminder.interval_seconds, reminder.interval_days, reminder.interval_months, name, reminder.tts, reminder.utc_time, ) .execute(transaction.executor()) .await { Ok(_) => 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 WHERE uid = ?", new_uid ) .fetch_one(transaction.executor()) .await .map(|r| Ok(json!(r))) .unwrap_or_else(|e| { warn!("Failed to complete SQL query: {:?}", e); Err(json!({"error": "Could not load reminder"})) }), Err(e) => { warn!("Error in `create_reminder`: Could not execute query: {:?}", e); Err(json!({"error": "Unknown error"})) } } }