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::{Evidence, 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 = EventDeletionRequest::new()
79 .ids([outer_event_id])
80 .finalize(ephemeral)
81 .map_err(|e| e.to_string())?;
82 transport.publish_durable(&deletion, relays).await
83}
84
85fn channel_read_pseudonyms(channel: &Channel) -> Vec<String> {
89 channel
90 .read_epoch_keys()
91 .iter()
92 .map(|(epoch, key)| channel_pseudonym(key, &channel.id, *epoch).to_hex())
93 .collect()
94}
95
96pub async fn fetch_channel_messages<T: Transport + ?Sized>(
99 transport: &T,
100 community: &Community,
101 channel: &Channel,
102) -> Result<Vec<OpenedMessage>, String> {
103 let query = Query {
104 kinds: vec![event_kind::COMMUNITY_MESSAGE],
105 z_tags: channel_read_pseudonyms(channel),
106 since: None,
107 evidence: Evidence::Fast,
110 ..Default::default()
111 };
112 let events = transport.fetch(&query, &community.relays).await?;
113 let epoch_keys = channel.read_epoch_keys();
118 let mut opened: Vec<OpenedMessage> = Vec::new();
119 let mut dropped = 0usize;
120 for ev in &events {
121 match open_message_multi(ev, &channel.id, &epoch_keys) {
122 Ok(msg) => opened.push(msg),
123 Err(e) => {
124 dropped += 1;
125 crate::log_debug!("[community] dropped event {}: {}", ev.id.to_hex(), e);
126 }
127 }
128 }
129 if dropped > 0 {
130 crate::log_debug!(
131 "[community] channel {} fetch: {} opened, {} dropped",
132 channel.id.to_hex(),
133 opened.len(),
134 dropped
135 );
136 }
137 {
142 let mut seen = std::collections::HashSet::new();
143 opened.retain(|m| seen.insert(m.message_id));
144 }
145 opened.sort_by(|a, b| {
147 a.ms.unwrap_or(0)
148 .cmp(&b.ms.unwrap_or(0))
149 .then_with(|| a.message_id.to_hex().cmp(&b.message_id.to_hex()))
150 });
151 Ok(opened)
152}
153
154pub async fn fetch_channel_events<T: Transport + ?Sized>(
160 transport: &T,
161 community: &Community,
162 channel: &Channel,
163) -> Result<Vec<Event>, String> {
164 let query = Query {
165 kinds: vec![
166 event_kind::COMMUNITY_MESSAGE,
167 event_kind::COMMUNITY_REACTION,
168 event_kind::COMMUNITY_EDIT,
169 event_kind::COMMUNITY_DELETE,
170 event_kind::COMMUNITY_PRESENCE,
171 event_kind::COMMUNITY_KICK,
172 event_kind::COMMUNITY_WEBXDC,
173 ],
174 z_tags: channel_read_pseudonyms(channel),
175 since: None,
176 evidence: Evidence::Fast,
178 ..Default::default()
179 };
180 transport.fetch(&query, &community.relays).await
181}
182
183pub async fn fetch_channel_page<T: Transport + ?Sized>(
190 transport: &T,
191 community: &Community,
192 channel: &Channel,
193 until: Option<u64>,
194 since: Option<u64>,
195 limit: usize,
196) -> Result<Vec<Event>, String> {
197 let query = Query {
198 kinds: vec![
199 event_kind::COMMUNITY_MESSAGE,
200 event_kind::COMMUNITY_REACTION,
201 event_kind::COMMUNITY_EDIT,
202 event_kind::COMMUNITY_DELETE,
203 event_kind::COMMUNITY_PRESENCE,
204 event_kind::COMMUNITY_KICK,
205 event_kind::COMMUNITY_WEBXDC,
206 ],
207 z_tags: channel_read_pseudonyms(channel),
211 until,
212 since,
217 limit: Some(limit),
218 evidence: if until.is_some() { Evidence::Full } else { Evidence::Fast },
223 ..Default::default()
224 };
225 transport.fetch(&query, &community.relays).await
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::community::transport::memory::MemoryRelay;
232 use crate::community::{Channel, ChannelKey, Epoch};
233
234 fn community() -> Community {
236 Community::create("HQ", "general", vec!["r1".into(), "r2".into(), "r3".into()])
237 }
238
239 fn member_view(of: &Community) -> Community {
242 Community {
243 id: of.id,
244 server_root_key: of.server_root_key.clone(),
245 server_root_epoch: of.server_root_epoch,
246 name: of.name.clone(),
247 description: of.description.clone(),
248 icon: of.icon.clone(),
249 banner: of.banner.clone(),
250 relays: of.relays.clone(),
251 channels: of.channels.clone(),
252 owner_attestation: of.owner_attestation.clone(),
253 dissolved: of.dissolved,
254 }
255 }
256
257 #[tokio::test]
258 async fn two_clients_exchange_via_relay() {
259 let relay = MemoryRelay::new();
260 let community = community();
261 let channel = community.channels[0].clone();
262
263 let alice = Keys::generate();
264 publish_message(&relay, &community, &channel, &alice, "gm from alice", 100)
265 .await
266 .unwrap();
267
268 let bob_view = member_view(&community);
270 let msgs = fetch_channel_messages(&relay, &bob_view, &bob_view.channels[0])
271 .await
272 .unwrap();
273 assert_eq!(msgs.len(), 1);
274 assert_eq!(msgs[0].content, "gm from alice");
275 assert_eq!(msgs[0].author, alice.public_key());
276 }
277
278 fn epoch_view(base: &Channel, key: ChannelKey, epoch: u64) -> Channel {
280 Channel {
281 id: base.id, key, epoch: Epoch(epoch), name: base.name.clone(),
282 banned: Vec::new(), protected: Vec::new(), roster: Default::default(), epoch_keys: Vec::new(),
283 dissolved: false,
284 }
285 }
286
287 #[tokio::test]
288 async fn fetch_spans_held_epochs_after_rekeys() {
289 let relay = MemoryRelay::new();
292 let community = community();
293 let base = community.channels[0].clone();
294 let alice = Keys::generate();
295 let k0 = base.key.clone();
296 let k1 = ChannelKey([0x11u8; 32]);
297 let k2 = ChannelKey([0x22u8; 32]);
298
299 publish_message(&relay, &community, &epoch_view(&base, k0.clone(), 0), &alice, "epoch0", 100).await.unwrap();
300 publish_message(&relay, &community, &epoch_view(&base, k1.clone(), 1), &alice, "epoch1", 200).await.unwrap();
301 publish_message(&relay, &community, &epoch_view(&base, k2.clone(), 2), &alice, "epoch2", 300).await.unwrap();
302
303 let mut reader = member_view(&community);
305 reader.channels[0] = Channel {
306 id: base.id, key: k2.clone(), epoch: Epoch(2), name: base.name.clone(),
307 banned: Vec::new(), protected: Vec::new(), roster: Default::default(),
308 epoch_keys: vec![(Epoch(0), k0), (Epoch(1), k1), (Epoch(2), k2.clone())],
309 dissolved: false,
310 };
311 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
312 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
313 assert_eq!(contents, vec!["epoch0", "epoch1", "epoch2"], "every held epoch's messages returned, none stranded");
314
315 let mut head_only = member_view(&community);
318 head_only.channels[0] = epoch_view(&base, k2, 2);
319 let only = fetch_channel_messages(&relay, &head_only, &head_only.channels[0]).await.unwrap();
320 assert_eq!(only.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), vec!["epoch2"],
321 "head-only reader sees only the current epoch (single-epoch fallback)");
322 }
323
324 #[tokio::test]
325 async fn page_spans_the_epoch_boundary() {
326 let relay = MemoryRelay::new();
330 let community = community();
331 let base = community.channels[0].clone();
332 let alice = Keys::generate();
333 let k0 = base.key.clone();
334 let k1 = ChannelKey([0x33u8; 32]);
335 publish_message(&relay, &community, &epoch_view(&base, k0.clone(), 0), &alice, "old-a", 100).await.unwrap();
336 publish_message(&relay, &community, &epoch_view(&base, k0.clone(), 0), &alice, "old-b", 200).await.unwrap();
337 publish_message(&relay, &community, &epoch_view(&base, k1.clone(), 1), &alice, "new-c", 300).await.unwrap();
338
339 let mut reader = member_view(&community);
340 reader.channels[0] = Channel {
341 id: base.id, key: k1.clone(), epoch: Epoch(1), name: base.name.clone(),
342 banned: Vec::new(), protected: Vec::new(), roster: Default::default(),
343 epoch_keys: vec![(Epoch(0), k0), (Epoch(1), k1)],
344 dissolved: false,
345 };
346 let page = fetch_channel_page(&relay, &reader, &reader.channels[0], None, None, 20).await.unwrap();
347 let opened: Vec<String> = page.iter()
348 .filter_map(|e| open_message_multi(e, &reader.channels[0].id, &reader.channels[0].read_epoch_keys()).ok())
349 .map(|m| m.content)
350 .collect();
351 assert!(opened.contains(&"new-c".to_string()), "head-epoch message in the page");
352 assert!(opened.contains(&"old-a".to_string()) && opened.contains(&"old-b".to_string()),
353 "older-epoch messages in the SAME page (across the epoch boundary)");
354 }
355
356 #[tokio::test]
357 async fn three_clients_one_broadcast() {
358 let relay = MemoryRelay::new();
360 let community = community();
361 let channel = community.channels[0].clone();
362 let alice = Keys::generate();
363 publish_message(&relay, &community, &channel, &alice, "hello all", 1)
364 .await
365 .unwrap();
366
367 for reader in [member_view(&community), member_view(&community)] {
368 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0])
369 .await
370 .unwrap();
371 assert_eq!(msgs.len(), 1);
372 assert_eq!(msgs[0].content, "hello all");
373 }
374 }
375
376 #[tokio::test]
377 async fn non_member_with_wrong_key_cannot_read() {
378 let relay = MemoryRelay::new();
379 let community = community();
380 let channel = community.channels[0].clone();
381 let alice = Keys::generate();
382 publish_message(&relay, &community, &channel, &alice, "secret", 1)
383 .await
384 .unwrap();
385
386 let mut outsider = member_view(&community);
389 outsider.channels[0].key = ChannelKey([0xeeu8; 32]);
390 let msgs = fetch_channel_messages(&relay, &outsider, &outsider.channels[0])
391 .await
392 .unwrap();
393 assert!(msgs.is_empty(), "wrong key derives a different pseudonym → no hits");
394
395 let raw = relay
397 .fetch(
398 &Query { kinds: vec![event_kind::COMMUNITY_MESSAGE], ..Default::default() },
399 &community.relays,
400 )
401 .await
402 .unwrap();
403 assert_eq!(raw.len(), 1);
404 assert!(open_message(&raw[0], &outsider.channels[0].key, &channel.id, channel.epoch).is_err());
405 }
406
407 #[tokio::test]
408 async fn messages_return_in_ms_order() {
409 let relay = MemoryRelay::new();
410 let community = community();
411 let channel = community.channels[0].clone();
412 let alice = Keys::generate();
413 publish_message(&relay, &community, &channel, &alice, "third", 300).await.unwrap();
415 publish_message(&relay, &community, &channel, &alice, "first", 100).await.unwrap();
416 publish_message(&relay, &community, &channel, &alice, "second", 200).await.unwrap();
417
418 let reader = member_view(&community);
419 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
420 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
421 assert_eq!(contents, vec!["first", "second", "third"]);
422 }
423
424 #[tokio::test]
425 async fn other_channel_traffic_is_not_returned() {
426 let relay = MemoryRelay::new();
429 let community = community();
430 let chan_a = community.channels[0].clone();
431 let chan_b = Channel {
432 id: super::super::ChannelId([0x77u8; 32]),
433 key: ChannelKey([0x88u8; 32]),
434 epoch: Epoch(0),
435 name: "other".into(),
436 banned: Vec::new(),
437 protected: Vec::new(), roster: Default::default(),
438 epoch_keys: Vec::new(),
439 dissolved: false,
440 };
441 let alice = Keys::generate();
442 publish_message(&relay, &community, &chan_a, &alice, "in A", 1).await.unwrap();
443 publish_message(&relay, &community, &chan_b, &alice, "in B", 1).await.unwrap();
444
445 let reader = member_view(&community);
446 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
447 assert_eq!(msgs.len(), 1);
448 assert_eq!(msgs[0].content, "in A");
449 }
450
451 #[tokio::test]
452 async fn two_distinct_messages_are_not_collapsed() {
453 let relay = MemoryRelay::new();
457 let community = community();
458 let channel = community.channels[0].clone();
459 let alice = Keys::generate();
460 publish_message(&relay, &community, &channel, &alice, "one", 1).await.unwrap();
461 publish_message(&relay, &community, &channel, &alice, "two", 2).await.unwrap();
462
463 let reader = member_view(&community);
464 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
465 assert_eq!(msgs.len(), 2);
466 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
467 assert!(contents.contains(&"one") && contents.contains(&"two"));
468 }
469
470 #[tokio::test]
471 async fn backfill_fetches_and_applies_messages_then_reactions() {
472 use super::super::inbound::{process_channel_batch, IncomingEvent};
477 let relay = MemoryRelay::new();
478 let community = community();
479 let channel = community.channels[0].clone();
480 let alice = Keys::generate();
481 let bob = Keys::generate();
482
483 let (m1_outer, _) = publish_message(&relay, &community, &channel, &alice, "hello", 1).await.unwrap();
484 publish_message(&relay, &community, &channel, &alice, "world", 2).await.unwrap();
485 let m1_inner = open_message(&m1_outer, &channel.key, &channel.id, channel.epoch).unwrap().message_id.to_hex();
487 let react_inner = super::super::envelope::build_inner_typed(
488 bob.public_key(), &channel.id, channel.epoch,
489 event_kind::COMMUNITY_REACTION, "🔥", 3, Some(&m1_inner), &[],
490 ).finalize(&bob).unwrap();
491 let react_outer = seal_with_signed_inner(&Keys::generate(), &react_inner, &channel.key, &channel.id, channel.epoch).unwrap();
492 relay.publish(&react_outer, &community.relays).await.unwrap();
493
494 let events = fetch_channel_events(&relay, &community, &channel).await.unwrap();
495 assert_eq!(events.len(), 3, "two messages + one reaction fetched");
496 let mut state = crate::state::ChatState::new();
497 let applied = process_channel_batch(&mut state, &events, &channel, &bob.public_key());
498
499 let new_msgs = applied.iter().filter(|e| matches!(e, IncomingEvent::NewMessage(_))).count();
500 let updates: Vec<&String> = applied.iter().filter_map(|e| match e {
501 IncomingEvent::Updated { target_id, .. } => Some(target_id),
502 _ => None,
503 }).collect();
504 assert_eq!(new_msgs, 2, "both messages backfilled");
505 assert_eq!(updates.len(), 1, "reaction applied during backfill");
506 assert_eq!(updates[0], &m1_inner, "reaction landed on its target message");
507 }
508
509 #[tokio::test]
510 async fn same_inner_message_in_two_wrappers_collapses() {
511 let relay = MemoryRelay::new();
515 let community = community();
516 let channel = community.channels[0].clone();
517 let alice = Keys::generate();
518
519 let inner = super::super::envelope::build_inner_event(alice.public_key(), &channel.id, channel.epoch, "dup me", 1, None)
521 .finalize(&alice)
522 .unwrap();
523 let outer_a = seal_with_signed_inner(&Keys::generate(), &inner, &channel.key, &channel.id, channel.epoch).unwrap();
525 let outer_b = seal_with_signed_inner(&Keys::generate(), &inner, &channel.key, &channel.id, channel.epoch).unwrap();
526 assert_ne!(outer_a.id, outer_b.id, "distinct outer wrappers");
527 relay.publish(&outer_a, &community.relays).await.unwrap();
528 relay.publish(&outer_b, &community.relays).await.unwrap();
529
530 let reader = member_view(&community);
531 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
532 assert_eq!(msgs.len(), 1, "same inner id collapses across wrappers");
533 assert_eq!(msgs[0].content, "dup me");
534 }
535
536 #[tokio::test]
537 async fn bad_event_dropped_good_event_kept_in_same_batch() {
538 let relay = MemoryRelay::new();
541 let community = community();
542 let channel = community.channels[0].clone();
543 let alice = Keys::generate();
544 publish_message(&relay, &community, &channel, &alice, "valid", 1).await.unwrap();
546 let pseudonym =
548 super::super::derive::channel_pseudonym(&channel.key, &channel.id, channel.epoch);
549 let garbage = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_MESSAGE), "not-base64-or-cipher!!")
550 .tags([
551 Tag::custom(
552 "z",
553 [pseudonym.to_hex()],
554 ),
555 Tag::custom("v", ["1".to_string()]),
556 ])
557 .finalize(&Keys::generate())
558 .unwrap();
559 relay.publish(&garbage, &community.relays).await.unwrap();
560
561 let reader = member_view(&community);
562 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
563 assert_eq!(msgs.len(), 1);
564 assert_eq!(msgs[0].content, "valid");
565 }
566
567 #[tokio::test]
568 async fn publish_retains_key_and_owner_can_delete() {
569 let relay = MemoryRelay::new();
570 let community = community();
571 let channel = community.channels[0].clone();
572 let alice = Keys::generate();
573
574 let (outer, ephemeral) =
575 publish_message(&relay, &community, &channel, &alice, "deletable", 1).await.unwrap();
576 assert_eq!(ephemeral.public_key(), outer.pubkey);
578
579 let before = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
580 assert_eq!(before.len(), 1);
581
582 delete_own_message(&relay, &community.relays, &ephemeral, outer.id).await.unwrap();
584 let after = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
585 assert!(after.is_empty(), "owner's deletion should remove the message");
586 }
587
588 #[tokio::test]
589 async fn deletion_by_a_different_key_is_ignored() {
590 let relay = MemoryRelay::new();
592 let community = community();
593 let channel = community.channels[0].clone();
594 let alice = Keys::generate();
595 let (outer, ephemeral) =
596 publish_message(&relay, &community, &channel, &alice, "mine", 1).await.unwrap();
597
598 let attacker = Keys::generate();
599 delete_own_message(&relay, &community.relays, &attacker, outer.id).await.unwrap();
600 let after = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
601 assert_eq!(after.len(), 1, "a foreign key must not delete someone else's message");
602
603 delete_own_message(&relay, &community.relays, &ephemeral, outer.id).await.unwrap();
606 let gone = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
607 assert!(gone.is_empty(), "the original signer's key must delete it");
608 }
609
610 #[tokio::test]
611 async fn deletion_must_reach_all_relays_to_take_effect() {
612 let relay = MemoryRelay::new();
616 let community = community(); let channel = community.channels[0].clone();
618 let alice = Keys::generate();
619 let (outer, ephemeral) =
620 publish_message(&relay, &community, &channel, &alice, "sticky", 1).await.unwrap();
621
622 delete_own_message(&relay, &["r1".to_string()], &ephemeral, outer.id).await.unwrap();
624 let still = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
626 assert_eq!(still.len(), 1, "deletion on a subset must not remove it everywhere");
627
628 delete_own_message(&relay, &community.relays, &ephemeral, outer.id).await.unwrap();
630 let gone = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
631 assert!(gone.is_empty());
632 }
633
634 #[tokio::test]
640 #[ignore]
641 async fn live_relay_roundtrip_and_cleanup() {
642 use super::super::transport::LiveTransport;
643 use crate::community::derive::channel_pseudonym;
644
645 let _ = rustls::crypto::ring::default_provider().install_default();
646
647 let relays = vec!["wss://jskitty.com/nostr".to_string()];
648 let community = Community::create("LiveTest", "general", relays.clone());
649 let channel = community.channels[0].clone();
650 let alice = Keys::generate();
651 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
652
653 let pseudonym = channel_pseudonym(&channel.key, &channel.id, channel.epoch).to_hex();
654 eprintln!("[live] channel pseudonym (z tag) = {pseudonym}");
655
656 let now_ms = std::time::SystemTime::now()
660 .duration_since(std::time::UNIX_EPOCH)
661 .unwrap()
662 .as_millis() as u64;
663 let mut published: Vec<(Keys, Event)> = Vec::new();
664 let mut publish_errs: Vec<String> = Vec::new();
665 for (i, body) in ["live hello one", "live hello two"].iter().enumerate() {
666 let ephemeral = Keys::generate();
667 let outer = seal_message_with_ephemeral(
668 &ephemeral, &alice, &channel.key, &channel.id, channel.epoch, body, now_ms + i as u64,
669 )
670 .expect("seal");
671 match transport.publish(&outer, &relays).await {
672 Ok(()) => {
673 eprintln!("[live] published event {}", outer.id.to_hex());
674 published.push((ephemeral, outer));
675 }
676 Err(e) => publish_errs.push(e),
677 }
678 }
679
680 let fetched = fetch_channel_messages(&transport, &community, &channel).await;
682
683 let mut cleanup_ok = true;
686 for (ephemeral, outer) in &published {
687 let del = EventDeletionRequest::new()
688 .ids([outer.id])
689 .finalize(ephemeral)
690 .expect("build deletion");
691 if let Err(e) = transport.publish(&del, &relays).await {
692 cleanup_ok = false;
693 eprintln!("[live] CLEANUP FAILED for {}: {e} — manual delete may be needed", outer.id.to_hex());
694 } else {
695 eprintln!("[live] deleted event {}", outer.id.to_hex());
696 }
697 }
698
699 assert!(publish_errs.is_empty(), "publish errors: {publish_errs:?}");
701 let msgs = fetched.expect("fetch failed");
702 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
703 assert!(contents.contains(&"live hello one"), "got {contents:?}");
704 assert!(contents.contains(&"live hello two"), "got {contents:?}");
705 for m in &msgs {
706 assert_eq!(m.author, alice.public_key(), "author recovered over the wire");
707 }
708 assert!(cleanup_ok, "cleanup deletion failed — check relay for leftover events");
709 eprintln!("[live] OK: {} messages round-tripped and cleaned up", msgs.len());
710 }
711
712 #[tokio::test]
713 async fn redundancy_a_dropped_relay_still_delivers() {
714 let relay = MemoryRelay::new();
717 let community = community();
718 let channel = community.channels[0].clone();
719 let alice = Keys::generate();
720
721 let outer = seal_message_with_ephemeral(
722 &Keys::generate(), &alice, &channel.key, &channel.id, channel.epoch, "survives", 1,
723 )
724 .unwrap();
725 relay.inject(&outer, &["r2".to_string()]); let reader = member_view(&community);
728 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
729 assert_eq!(msgs.len(), 1);
730 assert_eq!(msgs[0].content, "survives");
731 }
732}