Files
reminder-bot/src/web/routes/dashboard/api/user/models.rs
2025-10-27 17:52:31 +00:00

241 lines
7.4 KiB
Rust

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<Attachment>,
pub attachment_name: Option<String>,
pub content: String,
pub embed_author: String,
pub embed_author_url: Option<String>,
pub embed_color: u32,
pub embed_description: String,
pub embed_footer: String,
pub embed_footer_url: Option<String>,
pub embed_image_url: Option<String>,
pub embed_thumbnail_url: Option<String>,
pub embed_title: String,
pub embed_fields: Option<Json<Vec<EmbedField>>>,
pub enabled: bool,
pub expires: Option<NaiveDateTime>,
pub interval_seconds: Option<u32>,
pub interval_days: Option<u32>,
pub interval_months: Option<u32>,
#[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"}))
}
}
}