Skip to content

Commit 51aa523

Browse files
authored
Feat/chat media in aggregator (#392)
* refactor(message_aggregator/processor): extract several private methods from process_message * refactor(message_aggregator/processor): remove pointless retry pass * perf(message_aggregator): replace 3 full passes by single full pass + orphans * feat(message_aggregator): add media file information on messages * test(media message_aggregator): add send message with media test
1 parent 5c79a9b commit 51aa523

File tree

14 files changed

+521
-230
lines changed

14 files changed

+521
-230
lines changed

src/integration_tests/core/context.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::whitenoise::media_files::MediaFile;
12
use crate::{Account, Whitenoise, WhitenoiseError};
23
use mdk_core::prelude::group_types::Group;
34
use std::collections::HashMap;
@@ -9,6 +10,7 @@ pub struct ScenarioContext {
910
pub accounts: HashMap<String, Account>,
1011
pub groups: HashMap<String, Group>,
1112
pub messages_ids: HashMap<String, String>,
13+
pub media_files: HashMap<String, MediaFile>,
1214
pub tests_count: u32,
1315
pub tests_passed: u32,
1416
}
@@ -30,6 +32,7 @@ impl ScenarioContext {
3032
accounts: HashMap::new(),
3133
groups: HashMap::new(),
3234
messages_ids: HashMap::new(),
35+
media_files: HashMap::new(),
3336
tests_count: 0,
3437
tests_passed: 0,
3538
}
@@ -53,6 +56,16 @@ impl ScenarioContext {
5356
self.groups.get(name).ok_or(WhitenoiseError::GroupNotFound)
5457
}
5558

59+
pub fn add_media_file(&mut self, name: &str, media_file: MediaFile) {
60+
self.media_files.insert(name.to_string(), media_file);
61+
}
62+
63+
pub fn get_media_file(&self, name: &str) -> Result<&MediaFile, WhitenoiseError> {
64+
self.media_files.get(name).ok_or_else(|| {
65+
WhitenoiseError::Configuration(format!("Media file '{}' not found in context", name))
66+
})
67+
}
68+
5669
pub fn add_message_id(&mut self, name: &str, message_id: String) {
5770
self.messages_ids.insert(name.to_string(), message_id);
5871
}

src/integration_tests/scenarios/chat_media_upload.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,17 @@ impl Scenario for ChatMediaUploadScenario {
4343
.execute(&mut self.context)
4444
.await?;
4545

46+
// Send a message that references the uploaded media and verify aggregation links it
47+
SendMessageWithMediaTestCase::new("media_uploader", "media_upload_test_group")
48+
.execute(&mut self.context)
49+
.await?;
50+
4651
tracing::info!("✓ Chat media upload scenario completed with:");
4752
tracing::info!(" • Image upload with default processing options");
4853
tracing::info!(" • Blurhash generation verification");
4954
tracing::info!(" • Metadata extraction and storage");
55+
tracing::info!(" • Message with media reference sent");
56+
tracing::info!(" • Message aggregation verified media linking");
5057

5158
Ok(())
5259
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
pub mod send_message_with_media;
12
pub mod upload_chat_media;
23

4+
pub use send_message_with_media::*;
35
pub use upload_chat_media::*;
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use crate::WhitenoiseError;
2+
use crate::integration_tests::core::*;
3+
use async_trait::async_trait;
4+
use nostr_sdk::prelude::*;
5+
6+
/// Test case for sending messages with media attachments and verifying aggregation links them correctly
7+
pub struct SendMessageWithMediaTestCase {
8+
sender_account_name: String,
9+
group_name: String,
10+
message_content: String,
11+
}
12+
13+
impl SendMessageWithMediaTestCase {
14+
pub fn new(sender_account_name: &str, group_name: &str) -> Self {
15+
Self {
16+
sender_account_name: sender_account_name.to_string(),
17+
group_name: group_name.to_string(),
18+
message_content: "Check out this image! 📸".to_string(),
19+
}
20+
}
21+
22+
/// Build imeta tag per MIP-04 spec
23+
/// Format: `["imeta", "url <blossom_url>", "x <hash>", "m <mime_type>", ...]`
24+
fn build_imeta_tag(
25+
&self,
26+
hash_hex: &str,
27+
blossom_url: &str,
28+
mime_type: &str,
29+
) -> Result<Tag, WhitenoiseError> {
30+
Tag::parse(vec![
31+
"imeta",
32+
&format!("url {}", blossom_url),
33+
&format!("x {}", hash_hex),
34+
&format!("m {}", mime_type),
35+
])
36+
.map_err(|e| WhitenoiseError::Other(anyhow::anyhow!("Failed to create imeta tag: {}", e)))
37+
}
38+
}
39+
40+
#[async_trait]
41+
impl TestCase for SendMessageWithMediaTestCase {
42+
async fn run(&self, context: &mut ScenarioContext) -> Result<(), WhitenoiseError> {
43+
tracing::info!(
44+
"Sending message with media reference to group {} from account: {}",
45+
self.group_name,
46+
self.sender_account_name
47+
);
48+
49+
let sender_account = context.get_account(&self.sender_account_name)?;
50+
let group = context.get_group(&self.group_name)?;
51+
52+
// Get the uploaded media file from context
53+
let media_file = context.get_media_file("uploaded_chat_media")?;
54+
let media_hash_hex = hex::encode(&media_file.file_hash);
55+
56+
let blossom_url = media_file.blossom_url.as_ref().ok_or_else(|| {
57+
WhitenoiseError::Configuration("Uploaded media has no blossom URL".to_string())
58+
})?;
59+
60+
let imeta_tag =
61+
self.build_imeta_tag(&media_hash_hex, blossom_url, &media_file.mime_type)?;
62+
63+
// Send message with imeta tag
64+
let send_result = context
65+
.whitenoise
66+
.send_message_to_group(
67+
sender_account,
68+
&group.mls_group_id,
69+
self.message_content.clone(),
70+
9, // Regular message
71+
Some(vec![imeta_tag]),
72+
)
73+
.await?;
74+
75+
tracing::info!(
76+
"✓ Message with media reference sent: {}",
77+
send_result.message.id
78+
);
79+
80+
// Wait for message processing
81+
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
82+
83+
// Fetch aggregated messages and verify media is linked
84+
let aggregated_messages = retry(
85+
15,
86+
std::time::Duration::from_millis(100),
87+
|| async {
88+
let messages = context
89+
.whitenoise
90+
.fetch_aggregated_messages_for_group(
91+
&sender_account.pubkey,
92+
&group.mls_group_id,
93+
)
94+
.await?;
95+
96+
if messages.is_empty() {
97+
return Err(WhitenoiseError::Other(anyhow::anyhow!(
98+
"No messages found yet"
99+
)));
100+
}
101+
102+
Ok(messages)
103+
},
104+
"fetch aggregated messages with media",
105+
)
106+
.await?;
107+
108+
// Find the message we just sent
109+
let sent_message_id = send_result.message.id.to_string();
110+
let message_with_media = aggregated_messages
111+
.iter()
112+
.find(|msg| msg.id == sent_message_id)
113+
.ok_or_else(|| {
114+
WhitenoiseError::Other(anyhow::anyhow!(
115+
"Sent message {} not found in aggregated messages",
116+
sent_message_id
117+
))
118+
})?;
119+
120+
// Verify media is attached
121+
assert!(
122+
!message_with_media.media_attachments.is_empty(),
123+
"Message should have media attachments linked"
124+
);
125+
126+
assert_eq!(
127+
message_with_media.media_attachments.len(),
128+
1,
129+
"Message should have exactly 1 media attachment"
130+
);
131+
132+
let attached_media = &message_with_media.media_attachments[0];
133+
let attached_hash_hex = hex::encode(&attached_media.file_hash);
134+
assert_eq!(
135+
attached_hash_hex, media_hash_hex,
136+
"Attached media hash should match uploaded file hash"
137+
);
138+
139+
assert_eq!(
140+
attached_media.mime_type, media_file.mime_type,
141+
"Attached media MIME type should match uploaded file"
142+
);
143+
144+
tracing::info!(
145+
"✓ Message aggregation correctly linked media: hash={}",
146+
attached_hash_hex
147+
);
148+
149+
// Verify the message content is correct
150+
assert_eq!(
151+
message_with_media.content, self.message_content,
152+
"Message content should match"
153+
);
154+
155+
tracing::info!(
156+
"✓ Message with media verified: {} media attachment(s) linked",
157+
message_with_media.media_attachments.len()
158+
);
159+
160+
Ok(())
161+
}
162+
}

src/integration_tests/test_cases/chat_media_upload/upload_chat_media.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ impl TestCase for UploadChatMediaTestCase {
9292
hex::encode(&media_file.file_hash)
9393
);
9494

95-
// Basic validation
95+
// Validate upload results
9696
assert!(
9797
!media_file.file_hash.is_empty(),
9898
"File hash should not be empty"
@@ -137,6 +137,10 @@ impl TestCase for UploadChatMediaTestCase {
137137
);
138138

139139
tracing::info!("✓ All media file validations passed");
140+
141+
// Store the media file in context for subsequent tests
142+
context.add_media_file("uploaded_chat_media", media_file);
143+
140144
Ok(())
141145
}
142146
}

0 commit comments

Comments
 (0)