diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs index f2ff0f55..92055cde 100644 --- a/crates/core/database/src/models/messages/model.rs +++ b/crates/core/database/src/models/messages/model.rs @@ -8,7 +8,7 @@ use revolt_models::v0::{ MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION, }; use revolt_permissions::{ChannelPermission, PermissionValue}; -use revolt_result::Result; +use revolt_result::{ErrorType, Result}; use ulid::Ulid; use validator::Validate; @@ -329,14 +329,31 @@ impl Message { })); } - for ReplyIntent { id, mention } in entries { - let message = db.fetch_message(&id).await?; + for ReplyIntent { + id, + mention, + fail_if_not_exists, + } in entries + { + match db.fetch_message(&id).await { + // Referenced message exists + Ok(message) => { + if mention && allow_mentions { + mentions.insert(message.author.to_owned()); + } - if mention && allow_mentions { - mentions.insert(message.author.to_owned()); + replies.insert(message.id); + } + // If the referenced message doesn't exist and fail_if_not_exists + // is set to false, send the message without the reply. + Err(e) => { + if !matches!(e.error_type, ErrorType::NotFound) + || fail_if_not_exists.unwrap_or(true) + { + return Err(e); + } + } } - - replies.insert(message.id); } } diff --git a/crates/core/models/src/v0/messages.rs b/crates/core/models/src/v0/messages.rs index 37ed526e..2897b447 100644 --- a/crates/core/models/src/v0/messages.rs +++ b/crates/core/models/src/v0/messages.rs @@ -241,6 +241,10 @@ auto_derived!( pub id: String, /// Whether this reply should mention the message's author pub mention: bool, + /// Whether to error if the referenced message doesn't exist. + /// Otherwise, send a message without this reply. + /// Default is true. + pub fail_if_not_exists: Option, } /// Message to send diff --git a/crates/delta/src/routes/channels/message_send.rs b/crates/delta/src/routes/channels/message_send.rs index d55bd8bd..83f7a272 100644 --- a/crates/delta/src/routes/channels/message_send.rs +++ b/crates/delta/src/routes/channels/message_send.rs @@ -327,4 +327,163 @@ mod test { "Mention was scrubbed when the user can see the channel" ); } + + #[rocket::async_test] + async fn message_reply() { + let harness = TestHarness::new().await; + let (_, _, user) = harness.new_user().await; + let (server, channels) = harness.new_server(&user).await; + let channel = harness.new_channel(&server).await; + let (_, member, message) = harness.new_message(&user, &server, channels).await; + + // Send a message with a reply + // Should succeed + let message_with_reply = Message::create_from_api( + &harness.db, + Some(&harness.amqp), + channel.clone(), + v0::DataMessageSend { + content: Some("Message with reply".to_string()), + nonce: None, + attachments: None, + replies: Some(vec![v0::ReplyIntent { + id: message.id.clone(), + mention: false, + fail_if_not_exists: Some(true), + }]), + embeds: None, + masquerade: None, + interactions: None, + flags: None, + }, + v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await), + Some(user.clone().into(&harness.db, Some(&user)).await), + Some(member.clone().into()), + user.limits().await, + IdempotencyKey::unchecked_from_string("1".to_string()), + false, + false, + ) + .await + .expect("Failed to create message with reply"); + + assert!( + message_with_reply.replies.is_some(), + "Message replies is None", + ); + + let replies = message_with_reply.replies.unwrap(); + + assert!(!replies.is_empty(), "Message replies is empty",); + + assert_eq!(replies[0], message.id, "Message reply ID does not match",); + + // Delete the message + message + .clone() + .delete(&harness.db) + .await + .expect("Failed to delete message"); + + // Attempt to create messages with a reply to a deleted message + + // fail_if_not_exists is set to false + // Should send the message without a reply + let message_with_missing_reply = Message::create_from_api( + &harness.db, + Some(&harness.amqp), + channel.clone(), + v0::DataMessageSend { + content: Some("Message with missing reply".to_string()), + nonce: None, + attachments: None, + replies: Some(vec![v0::ReplyIntent { + id: message.id.clone(), + mention: false, + fail_if_not_exists: Some(false), + }]), + embeds: None, + masquerade: None, + interactions: None, + flags: None, + }, + v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await), + Some(user.clone().into(&harness.db, Some(&user)).await), + Some(member.clone().into()), + user.limits().await, + IdempotencyKey::unchecked_from_string("3".to_string()), + false, + false, + ) + .await + .expect("Failed to create message with missing reply"); + + assert!( + message_with_missing_reply.replies.is_none() + || message_with_missing_reply.replies.unwrap().is_empty(), + "Message replies exist when they shouldn't", + ); + + // fail_if_not_exists is set to true + // Should fail to send the message + Message::create_from_api( + &harness.db, + Some(&harness.amqp), + channel.clone(), + v0::DataMessageSend { + content: Some("Message with missing reply".to_string()), + nonce: None, + attachments: None, + replies: Some(vec![v0::ReplyIntent { + id: message.id.clone(), + mention: false, + fail_if_not_exists: Some(true), + }]), + embeds: None, + masquerade: None, + interactions: None, + flags: None, + }, + v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await), + Some(user.clone().into(&harness.db, Some(&user)).await), + Some(member.clone().into()), + user.limits().await, + IdempotencyKey::unchecked_from_string("4".to_string()), + false, + false, + ) + .await + .expect_err("Created message with missing reply and true fail"); + + // fail_if_not_exists is not set + // Should fail to send the message + Message::create_from_api( + &harness.db, + Some(&harness.amqp), + channel.clone(), + v0::DataMessageSend { + content: Some("Message with missing reply".to_string()), + nonce: None, + attachments: None, + replies: Some(vec![v0::ReplyIntent { + id: message.id.clone(), + mention: false, + fail_if_not_exists: None, + }]), + embeds: None, + masquerade: None, + interactions: None, + flags: None, + }, + v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await), + Some(user.clone().into(&harness.db, Some(&user)).await), + Some(member.clone().into()), + user.limits().await, + IdempotencyKey::unchecked_from_string("4".to_string()), + false, + false, + ) + .await + .expect_err("Created message with missing reply and none fail"); + } } diff --git a/crates/delta/src/util/test.rs b/crates/delta/src/util/test.rs index 3de14281..88dcdd1a 100644 --- a/crates/delta/src/util/test.rs +++ b/crates/delta/src/util/test.rs @@ -122,6 +122,22 @@ impl TestHarness { .expect("Failed to create test server") } + pub async fn new_channel(&self, server: &Server) -> Channel { + Channel::create_server_channel( + &self.db, + &mut server.clone(), + v0::DataCreateServerChannel { + channel_type: v0::LegacyServerChannelType::Text, + name: "Test Channel".to_string(), + description: None, + nsfw: Some(false), + }, + true, + ) + .await + .expect("Failed to make test channel") + } + pub async fn new_message( &self, user: &User,