1use nostr_sdk::prelude::*;
11
12use super::derive::channel_pseudonym;
13use super::envelope::{open_message_multi, seal_message_with_ephemeral, seal_with_signed_inner, OpenedMessage};
14#[cfg(test)]
15use super::envelope::open_message;
16use super::transport::{Query, Transport};
17use super::{Channel, Community};
18use crate::stored_event::event_kind;
19
20pub async fn publish_message<T: Transport + ?Sized>(
28 transport: &T,
29 community: &Community,
30 channel: &Channel,
31 author: &Keys,
32 content: &str,
33 ms: u64,
34) -> Result<(Event, Keys), String> {
35 let ephemeral = Keys::generate();
36 let outer = seal_message_with_ephemeral(
37 &ephemeral, author, &channel.key, &channel.id, channel.epoch, content, ms,
38 )
39 .map_err(|e| e.to_string())?;
40 transport.publish(&outer, &community.relays).await?;
41 Ok((outer, ephemeral))
42}
43
44pub async fn publish_signed_message<T: Transport + ?Sized>(
52 transport: &T,
53 community: &Community,
54 channel: &Channel,
55 inner: &Event,
56 durable: bool,
57) -> Result<(Event, Keys), String> {
58 let ephemeral = Keys::generate();
59 let outer = seal_with_signed_inner(&ephemeral, inner, &channel.key, &channel.id, channel.epoch)
60 .map_err(|e| e.to_string())?;
61 if durable {
62 transport.publish_durable(&outer, &community.relays).await?;
63 } else {
64 transport.publish(&outer, &community.relays).await?;
65 }
66 Ok((outer, ephemeral))
67}
68
69pub async fn delete_own_message<T: Transport + ?Sized>(
73 transport: &T,
74 relays: &[String],
75 ephemeral: &Keys,
76 outer_event_id: EventId,
77) -> Result<(), String> {
78 let deletion = EventBuilder::delete(EventDeletionRequest::new().ids([outer_event_id]))
79 .sign_with_keys(ephemeral)
80 .map_err(|e| e.to_string())?;
81 transport.publish_durable(&deletion, relays).await
82}
83
84fn channel_read_pseudonyms(channel: &Channel) -> Vec<String> {
88 channel
89 .read_epoch_keys()
90 .iter()
91 .map(|(epoch, key)| channel_pseudonym(key, &channel.id, *epoch).to_hex())
92 .collect()
93}
94
95pub async fn fetch_channel_messages<T: Transport + ?Sized>(
98 transport: &T,
99 community: &Community,
100 channel: &Channel,
101) -> Result<Vec<OpenedMessage>, String> {
102 let query = Query {
103 kinds: vec![event_kind::COMMUNITY_MESSAGE],
104 z_tags: channel_read_pseudonyms(channel),
105 since: None,
106 ..Default::default()
107 };
108 let events = transport.fetch(&query, &community.relays).await?;
109 let epoch_keys = channel.read_epoch_keys();
114 let mut opened: Vec<OpenedMessage> = Vec::new();
115 let mut dropped = 0usize;
116 for ev in &events {
117 match open_message_multi(ev, &channel.id, &epoch_keys) {
118 Ok(msg) => opened.push(msg),
119 Err(e) => {
120 dropped += 1;
121 crate::log_debug!("[community] dropped event {}: {}", ev.id.to_hex(), e);
122 }
123 }
124 }
125 if dropped > 0 {
126 crate::log_debug!(
127 "[community] channel {} fetch: {} opened, {} dropped",
128 channel.id.to_hex(),
129 opened.len(),
130 dropped
131 );
132 }
133 {
138 let mut seen = std::collections::HashSet::new();
139 opened.retain(|m| seen.insert(m.message_id));
140 }
141 opened.sort_by(|a, b| {
143 a.ms.unwrap_or(0)
144 .cmp(&b.ms.unwrap_or(0))
145 .then_with(|| a.message_id.to_hex().cmp(&b.message_id.to_hex()))
146 });
147 Ok(opened)
148}
149
150pub async fn fetch_channel_events<T: Transport + ?Sized>(
156 transport: &T,
157 community: &Community,
158 channel: &Channel,
159) -> Result<Vec<Event>, String> {
160 let query = Query {
161 kinds: vec![
162 event_kind::COMMUNITY_MESSAGE,
163 event_kind::COMMUNITY_REACTION,
164 event_kind::COMMUNITY_EDIT,
165 event_kind::COMMUNITY_DELETE,
166 event_kind::COMMUNITY_PRESENCE,
167 event_kind::COMMUNITY_KICK,
168 event_kind::COMMUNITY_WEBXDC,
169 ],
170 z_tags: channel_read_pseudonyms(channel),
171 since: None,
172 ..Default::default()
173 };
174 transport.fetch(&query, &community.relays).await
175}
176
177pub async fn fetch_channel_page<T: Transport + ?Sized>(
184 transport: &T,
185 community: &Community,
186 channel: &Channel,
187 until: Option<u64>,
188 since: Option<u64>,
189 limit: usize,
190) -> Result<Vec<Event>, String> {
191 let query = Query {
192 kinds: vec![
193 event_kind::COMMUNITY_MESSAGE,
194 event_kind::COMMUNITY_REACTION,
195 event_kind::COMMUNITY_EDIT,
196 event_kind::COMMUNITY_DELETE,
197 event_kind::COMMUNITY_PRESENCE,
198 event_kind::COMMUNITY_KICK,
199 event_kind::COMMUNITY_WEBXDC,
200 ],
201 z_tags: channel_read_pseudonyms(channel),
205 until,
206 since,
211 limit: Some(limit),
212 ..Default::default()
213 };
214 transport.fetch(&query, &community.relays).await
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::community::transport::memory::MemoryRelay;
221 use crate::community::{Channel, ChannelKey, Epoch};
222
223 fn community() -> Community {
225 Community::create("HQ", "general", vec!["r1".into(), "r2".into(), "r3".into()])
226 }
227
228 fn member_view(of: &Community) -> Community {
231 Community {
232 id: of.id,
233 server_root_key: of.server_root_key.clone(),
234 server_root_epoch: of.server_root_epoch,
235 name: of.name.clone(),
236 description: of.description.clone(),
237 icon: of.icon.clone(),
238 banner: of.banner.clone(),
239 relays: of.relays.clone(),
240 channels: of.channels.clone(),
241 owner_attestation: of.owner_attestation.clone(),
242 dissolved: of.dissolved,
243 }
244 }
245
246 #[tokio::test]
247 async fn two_clients_exchange_via_relay() {
248 let relay = MemoryRelay::new();
249 let community = community();
250 let channel = community.channels[0].clone();
251
252 let alice = Keys::generate();
253 publish_message(&relay, &community, &channel, &alice, "gm from alice", 100)
254 .await
255 .unwrap();
256
257 let bob_view = member_view(&community);
259 let msgs = fetch_channel_messages(&relay, &bob_view, &bob_view.channels[0])
260 .await
261 .unwrap();
262 assert_eq!(msgs.len(), 1);
263 assert_eq!(msgs[0].content, "gm from alice");
264 assert_eq!(msgs[0].author, alice.public_key());
265 }
266
267 fn epoch_view(base: &Channel, key: ChannelKey, epoch: u64) -> Channel {
269 Channel {
270 id: base.id, key, epoch: Epoch(epoch), name: base.name.clone(),
271 banned: Vec::new(), protected: Vec::new(), roster: Default::default(), epoch_keys: Vec::new(),
272 dissolved: false,
273 }
274 }
275
276 #[tokio::test]
277 async fn fetch_spans_held_epochs_after_rekeys() {
278 let relay = MemoryRelay::new();
281 let community = community();
282 let base = community.channels[0].clone();
283 let alice = Keys::generate();
284 let k0 = base.key.clone();
285 let k1 = ChannelKey([0x11u8; 32]);
286 let k2 = ChannelKey([0x22u8; 32]);
287
288 publish_message(&relay, &community, &epoch_view(&base, k0.clone(), 0), &alice, "epoch0", 100).await.unwrap();
289 publish_message(&relay, &community, &epoch_view(&base, k1.clone(), 1), &alice, "epoch1", 200).await.unwrap();
290 publish_message(&relay, &community, &epoch_view(&base, k2.clone(), 2), &alice, "epoch2", 300).await.unwrap();
291
292 let mut reader = member_view(&community);
294 reader.channels[0] = Channel {
295 id: base.id, key: k2.clone(), epoch: Epoch(2), name: base.name.clone(),
296 banned: Vec::new(), protected: Vec::new(), roster: Default::default(),
297 epoch_keys: vec![(Epoch(0), k0), (Epoch(1), k1), (Epoch(2), k2.clone())],
298 dissolved: false,
299 };
300 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
301 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
302 assert_eq!(contents, vec!["epoch0", "epoch1", "epoch2"], "every held epoch's messages returned, none stranded");
303
304 let mut head_only = member_view(&community);
307 head_only.channels[0] = epoch_view(&base, k2, 2);
308 let only = fetch_channel_messages(&relay, &head_only, &head_only.channels[0]).await.unwrap();
309 assert_eq!(only.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), vec!["epoch2"],
310 "head-only reader sees only the current epoch (single-epoch fallback)");
311 }
312
313 #[tokio::test]
314 async fn page_spans_the_epoch_boundary() {
315 let relay = MemoryRelay::new();
319 let community = community();
320 let base = community.channels[0].clone();
321 let alice = Keys::generate();
322 let k0 = base.key.clone();
323 let k1 = ChannelKey([0x33u8; 32]);
324 publish_message(&relay, &community, &epoch_view(&base, k0.clone(), 0), &alice, "old-a", 100).await.unwrap();
325 publish_message(&relay, &community, &epoch_view(&base, k0.clone(), 0), &alice, "old-b", 200).await.unwrap();
326 publish_message(&relay, &community, &epoch_view(&base, k1.clone(), 1), &alice, "new-c", 300).await.unwrap();
327
328 let mut reader = member_view(&community);
329 reader.channels[0] = Channel {
330 id: base.id, key: k1.clone(), epoch: Epoch(1), name: base.name.clone(),
331 banned: Vec::new(), protected: Vec::new(), roster: Default::default(),
332 epoch_keys: vec![(Epoch(0), k0), (Epoch(1), k1)],
333 dissolved: false,
334 };
335 let page = fetch_channel_page(&relay, &reader, &reader.channels[0], None, None, 20).await.unwrap();
336 let opened: Vec<String> = page.iter()
337 .filter_map(|e| open_message_multi(e, &reader.channels[0].id, &reader.channels[0].read_epoch_keys()).ok())
338 .map(|m| m.content)
339 .collect();
340 assert!(opened.contains(&"new-c".to_string()), "head-epoch message in the page");
341 assert!(opened.contains(&"old-a".to_string()) && opened.contains(&"old-b".to_string()),
342 "older-epoch messages in the SAME page (across the epoch boundary)");
343 }
344
345 #[tokio::test]
346 async fn three_clients_one_broadcast() {
347 let relay = MemoryRelay::new();
349 let community = community();
350 let channel = community.channels[0].clone();
351 let alice = Keys::generate();
352 publish_message(&relay, &community, &channel, &alice, "hello all", 1)
353 .await
354 .unwrap();
355
356 for reader in [member_view(&community), member_view(&community)] {
357 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0])
358 .await
359 .unwrap();
360 assert_eq!(msgs.len(), 1);
361 assert_eq!(msgs[0].content, "hello all");
362 }
363 }
364
365 #[tokio::test]
366 async fn non_member_with_wrong_key_cannot_read() {
367 let relay = MemoryRelay::new();
368 let community = community();
369 let channel = community.channels[0].clone();
370 let alice = Keys::generate();
371 publish_message(&relay, &community, &channel, &alice, "secret", 1)
372 .await
373 .unwrap();
374
375 let mut outsider = member_view(&community);
378 outsider.channels[0].key = ChannelKey([0xeeu8; 32]);
379 let msgs = fetch_channel_messages(&relay, &outsider, &outsider.channels[0])
380 .await
381 .unwrap();
382 assert!(msgs.is_empty(), "wrong key derives a different pseudonym → no hits");
383
384 let raw = relay
386 .fetch(
387 &Query { kinds: vec![event_kind::COMMUNITY_MESSAGE], ..Default::default() },
388 &community.relays,
389 )
390 .await
391 .unwrap();
392 assert_eq!(raw.len(), 1);
393 assert!(open_message(&raw[0], &outsider.channels[0].key, &channel.id, channel.epoch).is_err());
394 }
395
396 #[tokio::test]
397 async fn messages_return_in_ms_order() {
398 let relay = MemoryRelay::new();
399 let community = community();
400 let channel = community.channels[0].clone();
401 let alice = Keys::generate();
402 publish_message(&relay, &community, &channel, &alice, "third", 300).await.unwrap();
404 publish_message(&relay, &community, &channel, &alice, "first", 100).await.unwrap();
405 publish_message(&relay, &community, &channel, &alice, "second", 200).await.unwrap();
406
407 let reader = member_view(&community);
408 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
409 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
410 assert_eq!(contents, vec!["first", "second", "third"]);
411 }
412
413 #[tokio::test]
414 async fn other_channel_traffic_is_not_returned() {
415 let relay = MemoryRelay::new();
418 let community = community();
419 let chan_a = community.channels[0].clone();
420 let chan_b = Channel {
421 id: super::super::ChannelId([0x77u8; 32]),
422 key: ChannelKey([0x88u8; 32]),
423 epoch: Epoch(0),
424 name: "other".into(),
425 banned: Vec::new(),
426 protected: Vec::new(), roster: Default::default(),
427 epoch_keys: Vec::new(),
428 dissolved: false,
429 };
430 let alice = Keys::generate();
431 publish_message(&relay, &community, &chan_a, &alice, "in A", 1).await.unwrap();
432 publish_message(&relay, &community, &chan_b, &alice, "in B", 1).await.unwrap();
433
434 let reader = member_view(&community);
435 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
436 assert_eq!(msgs.len(), 1);
437 assert_eq!(msgs[0].content, "in A");
438 }
439
440 #[tokio::test]
441 async fn two_distinct_messages_are_not_collapsed() {
442 let relay = MemoryRelay::new();
446 let community = community();
447 let channel = community.channels[0].clone();
448 let alice = Keys::generate();
449 publish_message(&relay, &community, &channel, &alice, "one", 1).await.unwrap();
450 publish_message(&relay, &community, &channel, &alice, "two", 2).await.unwrap();
451
452 let reader = member_view(&community);
453 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
454 assert_eq!(msgs.len(), 2);
455 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
456 assert!(contents.contains(&"one") && contents.contains(&"two"));
457 }
458
459 #[tokio::test]
460 async fn backfill_fetches_and_applies_messages_then_reactions() {
461 use super::super::inbound::{process_channel_batch, IncomingEvent};
466 let relay = MemoryRelay::new();
467 let community = community();
468 let channel = community.channels[0].clone();
469 let alice = Keys::generate();
470 let bob = Keys::generate();
471
472 let (m1_outer, _) = publish_message(&relay, &community, &channel, &alice, "hello", 1).await.unwrap();
473 publish_message(&relay, &community, &channel, &alice, "world", 2).await.unwrap();
474 let m1_inner = open_message(&m1_outer, &channel.key, &channel.id, channel.epoch).unwrap().message_id.to_hex();
476 let react_inner = super::super::envelope::build_inner_typed(
477 bob.public_key(), &channel.id, channel.epoch,
478 event_kind::COMMUNITY_REACTION, "🔥", 3, Some(&m1_inner), &[],
479 ).sign_with_keys(&bob).unwrap();
480 let react_outer = seal_with_signed_inner(&Keys::generate(), &react_inner, &channel.key, &channel.id, channel.epoch).unwrap();
481 relay.publish(&react_outer, &community.relays).await.unwrap();
482
483 let events = fetch_channel_events(&relay, &community, &channel).await.unwrap();
484 assert_eq!(events.len(), 3, "two messages + one reaction fetched");
485 let mut state = crate::state::ChatState::new();
486 let applied = process_channel_batch(&mut state, &events, &channel, &bob.public_key());
487
488 let new_msgs = applied.iter().filter(|e| matches!(e, IncomingEvent::NewMessage(_))).count();
489 let updates: Vec<&String> = applied.iter().filter_map(|e| match e {
490 IncomingEvent::Updated { target_id, .. } => Some(target_id),
491 _ => None,
492 }).collect();
493 assert_eq!(new_msgs, 2, "both messages backfilled");
494 assert_eq!(updates.len(), 1, "reaction applied during backfill");
495 assert_eq!(updates[0], &m1_inner, "reaction landed on its target message");
496 }
497
498 #[tokio::test]
499 async fn same_inner_message_in_two_wrappers_collapses() {
500 let relay = MemoryRelay::new();
504 let community = community();
505 let channel = community.channels[0].clone();
506 let alice = Keys::generate();
507
508 let inner = super::super::envelope::build_inner_event(alice.public_key(), &channel.id, channel.epoch, "dup me", 1, None)
510 .sign_with_keys(&alice)
511 .unwrap();
512 let outer_a = seal_with_signed_inner(&Keys::generate(), &inner, &channel.key, &channel.id, channel.epoch).unwrap();
514 let outer_b = seal_with_signed_inner(&Keys::generate(), &inner, &channel.key, &channel.id, channel.epoch).unwrap();
515 assert_ne!(outer_a.id, outer_b.id, "distinct outer wrappers");
516 relay.publish(&outer_a, &community.relays).await.unwrap();
517 relay.publish(&outer_b, &community.relays).await.unwrap();
518
519 let reader = member_view(&community);
520 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
521 assert_eq!(msgs.len(), 1, "same inner id collapses across wrappers");
522 assert_eq!(msgs[0].content, "dup me");
523 }
524
525 #[tokio::test]
526 async fn bad_event_dropped_good_event_kept_in_same_batch() {
527 let relay = MemoryRelay::new();
530 let community = community();
531 let channel = community.channels[0].clone();
532 let alice = Keys::generate();
533 publish_message(&relay, &community, &channel, &alice, "valid", 1).await.unwrap();
535 let pseudonym =
537 super::super::derive::channel_pseudonym(&channel.key, &channel.id, channel.epoch);
538 let garbage = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_MESSAGE), "not-base64-or-cipher!!")
539 .tags([
540 Tag::custom(
541 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
542 [pseudonym.to_hex()],
543 ),
544 Tag::custom(TagKind::Custom("v".into()), ["1".to_string()]),
545 ])
546 .sign_with_keys(&Keys::generate())
547 .unwrap();
548 relay.publish(&garbage, &community.relays).await.unwrap();
549
550 let reader = member_view(&community);
551 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
552 assert_eq!(msgs.len(), 1);
553 assert_eq!(msgs[0].content, "valid");
554 }
555
556 #[tokio::test]
557 async fn publish_retains_key_and_owner_can_delete() {
558 let relay = MemoryRelay::new();
559 let community = community();
560 let channel = community.channels[0].clone();
561 let alice = Keys::generate();
562
563 let (outer, ephemeral) =
564 publish_message(&relay, &community, &channel, &alice, "deletable", 1).await.unwrap();
565 assert_eq!(ephemeral.public_key(), outer.pubkey);
567
568 let before = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
569 assert_eq!(before.len(), 1);
570
571 delete_own_message(&relay, &community.relays, &ephemeral, outer.id).await.unwrap();
573 let after = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
574 assert!(after.is_empty(), "owner's deletion should remove the message");
575 }
576
577 #[tokio::test]
578 async fn deletion_by_a_different_key_is_ignored() {
579 let relay = MemoryRelay::new();
581 let community = community();
582 let channel = community.channels[0].clone();
583 let alice = Keys::generate();
584 let (outer, ephemeral) =
585 publish_message(&relay, &community, &channel, &alice, "mine", 1).await.unwrap();
586
587 let attacker = Keys::generate();
588 delete_own_message(&relay, &community.relays, &attacker, outer.id).await.unwrap();
589 let after = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
590 assert_eq!(after.len(), 1, "a foreign key must not delete someone else's message");
591
592 delete_own_message(&relay, &community.relays, &ephemeral, outer.id).await.unwrap();
595 let gone = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
596 assert!(gone.is_empty(), "the original signer's key must delete it");
597 }
598
599 #[tokio::test]
600 async fn deletion_must_reach_all_relays_to_take_effect() {
601 let relay = MemoryRelay::new();
605 let community = community(); let channel = community.channels[0].clone();
607 let alice = Keys::generate();
608 let (outer, ephemeral) =
609 publish_message(&relay, &community, &channel, &alice, "sticky", 1).await.unwrap();
610
611 delete_own_message(&relay, &["r1".to_string()], &ephemeral, outer.id).await.unwrap();
613 let still = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
615 assert_eq!(still.len(), 1, "deletion on a subset must not remove it everywhere");
616
617 delete_own_message(&relay, &community.relays, &ephemeral, outer.id).await.unwrap();
619 let gone = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
620 assert!(gone.is_empty());
621 }
622
623 #[tokio::test]
629 #[ignore]
630 async fn live_relay_roundtrip_and_cleanup() {
631 use super::super::transport::LiveTransport;
632 use crate::community::derive::channel_pseudonym;
633
634 let _ = rustls::crypto::ring::default_provider().install_default();
635
636 let relays = vec!["wss://jskitty.com/nostr".to_string()];
637 let community = Community::create("LiveTest", "general", relays.clone());
638 let channel = community.channels[0].clone();
639 let alice = Keys::generate();
640 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
641
642 let pseudonym = channel_pseudonym(&channel.key, &channel.id, channel.epoch).to_hex();
643 eprintln!("[live] channel pseudonym (z tag) = {pseudonym}");
644
645 let now_ms = std::time::SystemTime::now()
649 .duration_since(std::time::UNIX_EPOCH)
650 .unwrap()
651 .as_millis() as u64;
652 let mut published: Vec<(Keys, Event)> = Vec::new();
653 let mut publish_errs: Vec<String> = Vec::new();
654 for (i, body) in ["live hello one", "live hello two"].iter().enumerate() {
655 let ephemeral = Keys::generate();
656 let outer = seal_message_with_ephemeral(
657 &ephemeral, &alice, &channel.key, &channel.id, channel.epoch, body, now_ms + i as u64,
658 )
659 .expect("seal");
660 match transport.publish(&outer, &relays).await {
661 Ok(()) => {
662 eprintln!("[live] published event {}", outer.id.to_hex());
663 published.push((ephemeral, outer));
664 }
665 Err(e) => publish_errs.push(e),
666 }
667 }
668
669 let fetched = fetch_channel_messages(&transport, &community, &channel).await;
671
672 let mut cleanup_ok = true;
675 for (ephemeral, outer) in &published {
676 let del = EventBuilder::delete(EventDeletionRequest::new().ids([outer.id]))
677 .sign_with_keys(ephemeral)
678 .expect("build deletion");
679 if let Err(e) = transport.publish(&del, &relays).await {
680 cleanup_ok = false;
681 eprintln!("[live] CLEANUP FAILED for {}: {e} — manual delete may be needed", outer.id.to_hex());
682 } else {
683 eprintln!("[live] deleted event {}", outer.id.to_hex());
684 }
685 }
686
687 assert!(publish_errs.is_empty(), "publish errors: {publish_errs:?}");
689 let msgs = fetched.expect("fetch failed");
690 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
691 assert!(contents.contains(&"live hello one"), "got {contents:?}");
692 assert!(contents.contains(&"live hello two"), "got {contents:?}");
693 for m in &msgs {
694 assert_eq!(m.author, alice.public_key(), "author recovered over the wire");
695 }
696 assert!(cleanup_ok, "cleanup deletion failed — check relay for leftover events");
697 eprintln!("[live] OK: {} messages round-tripped and cleaned up", msgs.len());
698 }
699
700 #[tokio::test]
701 async fn redundancy_a_dropped_relay_still_delivers() {
702 let relay = MemoryRelay::new();
705 let community = community();
706 let channel = community.channels[0].clone();
707 let alice = Keys::generate();
708
709 let outer = seal_message_with_ephemeral(
710 &Keys::generate(), &alice, &channel.key, &channel.id, channel.epoch, "survives", 1,
711 )
712 .unwrap();
713 relay.inject(&outer, &["r2".to_string()]); let reader = member_view(&community);
716 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
717 assert_eq!(msgs.len(), 1);
718 assert_eq!(msgs[0].content, "survives");
719 }
720}