Move to FKs

This commit is contained in:
jude committed 2026-10-18 20:06:55 +01:00
1 parent 96bfef4575
commit a9cc0b8f0a
32 files changed
+875 -305

No files matched your search

+176 -17
View File
@@ -20,6 +20,7 @@ use serenity::{
use sqlx::types::Json;
use sqlx::FromRow;
use crate::image_upload::ImageReference;
use crate::utils::check_subscription;
use crate::web::{
catchers::internal_server_error,
@@ -39,6 +40,11 @@ pub mod export;
type Unset<T> = Option<T>;
fn image_reference_error(error: sqlx::Error) -> serde_json::Value {
warn!("Could not resolve image upload: {:?}", error);
json!({"error": "Could not resolve image upload"})
}
fn name_default() -> String {
"Reminder".to_string()
}
@@ -92,15 +98,20 @@ pub struct ReminderTemplate {
name: String,
attachment: Option<Attachment>,
attachment_name: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
avatar: Option<String>,
content: String,
embed_author: String,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_author_url: Option<String>,
embed_color: u32,
embed_description: String,
embed_footer: String,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_footer_url: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_image_url: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_thumbnail_url: Option<String>,
embed_title: String,
embed_fields: Option<Json<Vec<EmbedField>>>,
@@ -117,15 +128,20 @@ pub struct ReminderTemplateCsv {
name: String,
attachment: Option<Attachment>,
attachment_name: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
avatar: Option<String>,
content: String,
embed_author: String,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_author_url: Option<String>,
embed_color: u32,
embed_description: String,
embed_footer: String,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_footer_url: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_image_url: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_thumbnail_url: Option<String>,
embed_title: String,
embed_fields: Option<String>,
@@ -182,17 +198,22 @@ pub struct CreateReminder {
#[derive(Serialize, FromRow)]
pub struct GetReminder {
attachment_name: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
avatar: Option<String>,
#[serde(with = "string")]
channel: u64,
content: String,
embed_author: String,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_author_url: Option<String>,
embed_color: u32,
embed_description: String,
embed_footer: String,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_footer_url: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_image_url: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_thumbnail_url: Option<String>,
embed_title: String,
embed_fields: Option<Json<Vec<EmbedField>>>,
@@ -215,16 +236,21 @@ pub struct GetReminder {
pub struct ReminderCsv {
attachment: Option<Attachment>,
attachment_name: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
avatar: Option<String>,
channel: String,
content: String,
embed_author: String,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_author_url: Option<String>,
embed_color: u32,
embed_description: String,
embed_footer: String,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_footer_url: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_image_url: Option<String>,
#[serde(serialize_with = "crate::image_upload::serialize_url")]
embed_thumbnail_url: Option<String>,
embed_title: String,
embed_fields: Option<String>,
@@ -491,6 +517,26 @@ pub(crate) async fn create_reminder(
let new_uid = generate_uid();
let avatar = ImageReference::from_url(transaction.executor(), reminder.avatar.as_deref())
.await
.map_err(image_reference_error)?;
let embed_image_url =
ImageReference::from_url(transaction.executor(), reminder.embed_image_url.as_deref())
.await
.map_err(image_reference_error)?;
let embed_thumbnail_url =
ImageReference::from_url(transaction.executor(), reminder.embed_thumbnail_url.as_deref())
.await
.map_err(image_reference_error)?;
let embed_author_url =
ImageReference::from_url(transaction.executor(), reminder.embed_author_url.as_deref())
.await
.map_err(image_reference_error)?;
let embed_footer_url =
ImageReference::from_url(transaction.executor(), reminder.embed_footer_url.as_deref())
.await
.map_err(image_reference_error)?;
// write to db
match sqlx::query!(
"INSERT INTO reminders (
@@ -499,15 +545,20 @@ pub(crate) async fn create_reminder(
attachment_name,
channel_id,
avatar,
avatar_id,
content,
embed_author,
embed_author_url,
embed_author_id,
embed_color,
embed_description,
embed_footer,
embed_footer_url,
embed_footer_id,
embed_image_url,
embed_image_id,
embed_thumbnail_url,
embed_thumbnail_id,
embed_title,
embed_fields,
enabled,
@@ -520,21 +571,26 @@ pub(crate) async fn create_reminder(
tts,
username,
`utc_time`
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
new_uid,
reminder.attachment,
reminder.attachment_name,
channel,
reminder.avatar,
avatar.url,
avatar.id,
reminder.content,
reminder.embed_author,
reminder.embed_author_url,
embed_author_url.url,
embed_author_url.id,
reminder.embed_color,
reminder.embed_description,
reminder.embed_footer,
reminder.embed_footer_url,
reminder.embed_image_url,
reminder.embed_thumbnail_url,
embed_footer_url.url,
embed_footer_url.id,
embed_image_url.url,
embed_image_url.id,
embed_thumbnail_url.url,
embed_thumbnail_url.id,
reminder.embed_title,
reminder.embed_fields,
reminder.enabled,
@@ -556,17 +612,17 @@ pub(crate) async fn create_reminder(
"
SELECT
reminders.attachment_name,
reminders.avatar,
COALESCE((SELECT path COLLATE utf8mb4_unicode_ci FROM image_uploads WHERE id = reminders.avatar_id), reminders.avatar) AS avatar,
channels.channel,
reminders.content,
reminders.embed_author,
reminders.embed_author_url,
COALESCE((SELECT path COLLATE utf8mb4_unicode_ci FROM image_uploads WHERE id = reminders.embed_author_id), reminders.embed_author_url) AS embed_author_url,
reminders.embed_color,
reminders.embed_description,
reminders.embed_footer,
reminders.embed_footer_url,
reminders.embed_image_url,
reminders.embed_thumbnail_url,
COALESCE((SELECT path COLLATE utf8mb4_unicode_ci FROM image_uploads WHERE id = reminders.embed_footer_id), reminders.embed_footer_url) AS embed_footer_url,
COALESCE((SELECT path COLLATE utf8mb4_unicode_ci FROM image_uploads WHERE id = reminders.embed_image_id), reminders.embed_image_url) AS embed_image_url,
COALESCE((SELECT path COLLATE utf8mb4_unicode_ci FROM image_uploads WHERE id = reminders.embed_thumbnail_id), reminders.embed_thumbnail_url) AS embed_thumbnail_url,
reminders.embed_title,
reminders.embed_fields,
reminders.enabled,
@@ -645,6 +701,35 @@ pub(crate) async fn create_reminder_template(
reminder_template.name.clone()
};
let avatar =
ImageReference::from_url(transaction.executor(), reminder_template.avatar.as_deref())
.await
.map_err(image_reference_error)?;
let embed_image_url = ImageReference::from_url(
transaction.executor(),
reminder_template.embed_image_url.as_deref(),
)
.await
.map_err(image_reference_error)?;
let embed_thumbnail_url = ImageReference::from_url(
transaction.executor(),
reminder_template.embed_thumbnail_url.as_deref(),
)
.await
.map_err(image_reference_error)?;
let embed_author_url = ImageReference::from_url(
transaction.executor(),
reminder_template.embed_author_url.as_deref(),
)
.await
.map_err(image_reference_error)?;
let embed_footer_url = ImageReference::from_url(
transaction.executor(),
reminder_template.embed_footer_url.as_deref(),
)
.await
.map_err(image_reference_error)?;
match sqlx::query!(
"INSERT INTO reminder_template
(guild_id,
@@ -652,15 +737,20 @@ pub(crate) async fn create_reminder_template(
attachment,
attachment_name,
avatar,
avatar_id,
content,
embed_author,
embed_author_url,
embed_author_id,
embed_color,
embed_description,
embed_footer,
embed_footer_url,
embed_footer_id,
embed_image_url,
embed_image_id,
embed_thumbnail_url,
embed_thumbnail_id,
embed_title,
embed_fields,
interval_seconds,
@@ -669,21 +759,26 @@ pub(crate) async fn create_reminder_template(
tts,
username
) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?)",
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
guild_id.get(),
name,
reminder_template.attachment,
reminder_template.attachment_name,
reminder_template.avatar,
avatar.url,
avatar.id,
reminder_template.content,
reminder_template.embed_author,
reminder_template.embed_author_url,
embed_author_url.url,
embed_author_url.id,
reminder_template.embed_color,
reminder_template.embed_description,
reminder_template.embed_footer,
reminder_template.embed_footer_url,
reminder_template.embed_image_url,
reminder_template.embed_thumbnail_url,
embed_footer_url.url,
embed_footer_url.id,
embed_image_url.url,
embed_image_url.id,
embed_thumbnail_url.url,
embed_thumbnail_url.id,
reminder_template.embed_title,
reminder_template.embed_fields,
reminder_template.interval_seconds,
@@ -869,3 +964,67 @@ async fn render_dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
DashboardPage::Unauthorised(Redirect::to("/login/discord"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use sqlx::{MySql, Pool};
#[sqlx::test]
async fn image_edits_distinguish_omission_replacement_and_removal(pool: Pool<MySql>) {
sqlx::query("INSERT INTO channels (id, channel) VALUES (1, 123)")
.execute(&pool)
.await
.unwrap();
let image = sqlx::query("INSERT INTO image_uploads (uploader, path) VALUES (123, ?)")
.bind(format!("uploads/{:064x}.png", 1))
.execute(&pool)
.await
.unwrap()
.last_insert_id();
sqlx::query("INSERT INTO reminders (uid, channel_id, `utc_time`, embed_image_id) VALUES ('image-edit', 1, UTC_TIMESTAMP(), ?)")
.bind(image).execute(&pool).await.unwrap();
let mut transaction = pool.begin().await.unwrap();
let mut errors = vec![];
let reminder: PatchReminder = serde_json::from_value(json!({"uid": "image-edit"})).unwrap();
update_image_field!(&mut *transaction, errors, reminder, embed_image_url, embed_image_id);
let stored: (Option<String>, Option<u64>) = sqlx::query_as(
"SELECT embed_image_url, embed_image_id FROM reminders WHERE uid = 'image-edit'",
)
.fetch_one(&mut *transaction)
.await
.unwrap();
assert_eq!(stored, (None, Some(image)));
let external = "https://external.example/new.png";
let reminder: PatchReminder = serde_json::from_value(json!({
"uid": "image-edit", "embed_image_url": external
}))
.unwrap();
update_image_field!(&mut *transaction, errors, reminder, embed_image_url, embed_image_id);
let stored: (Option<String>, Option<u64>) = sqlx::query_as(
"SELECT embed_image_url, embed_image_id FROM reminders WHERE uid = 'image-edit'",
)
.fetch_one(&mut *transaction)
.await
.unwrap();
assert_eq!(stored, (Some(external.into()), None));
sqlx::query("UPDATE reminders SET embed_image_url = NULL, embed_image_id = ? WHERE uid = 'image-edit'")
.bind(image).execute(&mut *transaction).await.unwrap();
let reminder: PatchReminder = serde_json::from_value(json!({
"uid": "image-edit", "embed_image_url": null
}))
.unwrap();
update_image_field!(&mut *transaction, errors, reminder, embed_image_url, embed_image_id);
let stored: (Option<String>, Option<u64>) = sqlx::query_as(
"SELECT embed_image_url, embed_image_id FROM reminders WHERE uid = 'image-edit'",
)
.fetch_one(&mut *transaction)
.await
.unwrap();
assert_eq!(stored, (None, None));
assert!(errors.is_empty());
transaction.commit().await.unwrap();
}
}