1use std::collections::HashSet;
15use std::sync::{Arc, LazyLock, Mutex as StdMutex};
16
17use nostr_sdk::prelude::{Client, Event, Filter, Kind, PublicKey, RelayStatus, RelayUrl, SubscriptionId};
18use tokio::sync::mpsc::UnboundedSender;
19use tokio::sync::Mutex;
20
21use super::community::CommunityV2;
22use super::stream;
23use super::{derive, inbound};
24use crate::community::{CommunityId, ConcordProtocol, Epoch};
25use crate::event_handler::InboundEventHandler;
26use crate::state::SessionGuard;
27
28static V2_SUB_ID: LazyLock<Mutex<Option<SubscriptionId>>> = LazyLock::new(|| Mutex::new(None));
30static V2_POOLWIDE_SUB_ID: LazyLock<Mutex<Option<SubscriptionId>>> = LazyLock::new(|| Mutex::new(None));
32static V2_SUB_SET: LazyLock<Mutex<Vec<String>>> = LazyLock::new(|| Mutex::new(Vec::new()));
35static V2_SEEN_WRAPS: LazyLock<Mutex<HashSet<[u8; 32]>>> = LazyLock::new(|| Mutex::new(HashSet::new()));
43const SEEN_WRAPS_CAP: usize = 8192;
45static V2_FOLLOW_TX: LazyLock<StdMutex<Option<UnboundedSender<CommunityId>>>> = LazyLock::new(|| StdMutex::new(None));
55static V2_FOLLOW_PENDING: LazyLock<StdMutex<HashSet<[u8; 32]>>> = LazyLock::new(|| StdMutex::new(HashSet::new()));
57static V2_FOLLOW_LOCKS: LazyLock<StdMutex<std::collections::HashMap<[u8; 32], Arc<Mutex<()>>>>> =
64 LazyLock::new(|| StdMutex::new(std::collections::HashMap::new()));
65
66pub(crate) fn follow_lock(id: &CommunityId) -> Arc<Mutex<()>> {
68 V2_FOLLOW_LOCKS.lock().unwrap().entry(id.0).or_default().clone()
69}
70
71pub async fn subscription_id() -> Option<SubscriptionId> {
72 V2_SUB_ID.lock().await.clone()
73}
74
75pub async fn poolwide_subscription_id() -> Option<SubscriptionId> {
76 V2_POOLWIDE_SUB_ID.lock().await.clone()
77}
78
79pub async fn clear() {
82 *V2_SUB_ID.lock().await = None;
83 *V2_POOLWIDE_SUB_ID.lock().await = None;
84 V2_SUB_SET.lock().await.clear();
85 V2_SEEN_WRAPS.lock().await.clear();
86 *V2_FOLLOW_TX.lock().unwrap() = None;
89 V2_FOLLOW_PENDING.lock().unwrap().clear();
90 V2_FOLLOW_LOCKS.lock().unwrap().clear();
91 super::streamauth::clear();
94}
95
96pub fn plane_authors(communities: &[CommunityV2]) -> Vec<PublicKey> {
107 let mut out = Vec::new();
108 for c in communities {
109 out.push(derive::guestbook_group_key(&c.community_root, c.id(), c.root_epoch).pk());
110 out.push(control_author(c));
111 out.push(super::derive::dissolved_group_key(c.id()).pk());
113 for ch in &c.channels {
114 if ch.private && ch.key.is_none() {
118 continue;
119 }
120 let (secret, epoch) = c.channel_secret(ch);
121 out.push(derive::channel_group_key(&secret, &ch.id, epoch).pk());
122 }
123 out.extend(rekey_authors(c));
124 }
125 out.sort_by_key(|p| p.to_hex());
126 out.dedup();
127 out
128}
129
130pub(crate) fn control_author(c: &CommunityV2) -> PublicKey {
134 derive::control_group_key(&c.community_root, c.id(), c.root_epoch).pk()
135}
136
137pub(crate) fn rekey_authors(c: &CommunityV2) -> Vec<PublicKey> {
144 let mut out = vec![derive::base_rekey_group_key(&c.community_root, c.id(), Epoch(c.root_epoch.0.saturating_add(1))).pk()];
148 for ch in &c.channels {
149 if ch.private {
150 out.push(derive::channel_rekey_group_key(&c.community_root, &ch.id, Epoch(ch.epoch.0.saturating_add(1))).pk());
151 }
152 }
153 out
154}
155
156pub fn load_held_v2() -> Vec<CommunityV2> {
163 let ids = crate::db::community::list_community_ids().unwrap_or_default();
164 ids.iter()
165 .filter(|id| matches!(crate::db::community::community_protocol(id).ok().flatten(), Some(ConcordProtocol::V2)))
166 .filter(|id| !crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false))
167 .filter_map(|id| crate::db::community::load_community_v2(id).ok().flatten())
168 .collect()
169}
170
171pub async fn refresh_subscription(client: &Client) {
175 {
179 let communities = load_held_v2();
180 let mut relays: Vec<String> = communities.iter().flat_map(|c| c.relays.iter().cloned()).collect();
181 relays.sort();
182 relays.dedup();
183 if !relays.is_empty() {
184 for r in &relays {
186 let _ = client.pool().add_relay(r.as_str(), crate::community_relay_options()).await;
187 }
188 client.connect().await;
189 let wanted: Vec<RelayUrl> = relays.iter().filter_map(|r| RelayUrl::parse(r).ok()).collect();
193 for _ in 0..24 {
194 let pool = client.pool().all_relays().await;
195 if wanted.iter().any(|u| pool.get(u).map(|r| r.status() == RelayStatus::Connected).unwrap_or(false)) {
196 break;
197 }
198 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
199 }
200 for c in &communities {
205 super::streamauth::register_community(c);
206 }
207 super::streamauth::prime_auth(client, &relays).await;
208 }
209 }
210
211 let mut sub_guard = V2_SUB_ID.lock().await;
219 let mut set_guard = V2_SUB_SET.lock().await;
220
221 let communities = load_held_v2();
222 let authors = plane_authors(&communities);
223 let mut relays: Vec<String> = communities.iter().flat_map(|c| c.relays.iter().cloned()).collect();
224 relays.sort();
225 relays.dedup();
226
227 let mut new_set: Vec<String> = authors.iter().map(|p| p.to_hex()).collect();
228 new_set.sort();
229
230 if sub_guard.is_some() && *set_guard == new_set && (authors.is_empty() || V2_POOLWIDE_SUB_ID.lock().await.is_some()) {
234 return; }
236 if let Some(old) = sub_guard.take() {
237 client.unsubscribe(&old).await;
238 }
239 *set_guard = new_set;
240
241 if authors.is_empty() {
242 if let Some(old_pw) = V2_POOLWIDE_SUB_ID.lock().await.take() {
243 client.unsubscribe(&old_pw).await;
244 }
245 return;
246 }
247
248 let filter = Filter::new()
249 .kinds([Kind::Custom(stream::KIND_WRAP), Kind::Custom(stream::KIND_WRAP_EPHEMERAL)])
250 .authors(authors)
251 .limit(0);
252
253 {
254 let mut pw = V2_POOLWIDE_SUB_ID.lock().await;
255 if let Some(old) = pw.take() {
256 client.unsubscribe(&old).await;
257 }
258 if let Ok(out) = client.subscribe(filter.clone(), None).await {
259 *pw = Some(out.val);
260 }
261 }
262 if let Ok(out) = client.subscribe_to(relays.iter().cloned(), filter, None).await {
263 *sub_guard = Some(out.val);
264 }
265}
266
267pub(crate) async fn resubscribe_relay(client: &Client, relay: &RelayUrl) {
275 let targeted = V2_SUB_ID.lock().await.clone();
276 let poolwide = V2_POOLWIDE_SUB_ID.lock().await.clone();
277 if targeted.is_none() && poolwide.is_none() {
278 return; }
280 let communities = load_held_v2();
281 let authors = plane_authors(&communities);
282 if authors.is_empty() {
283 return;
284 }
285 let filter = Filter::new()
286 .kinds([Kind::Custom(stream::KIND_WRAP), Kind::Custom(stream::KIND_WRAP_EPHEMERAL)])
287 .authors(authors)
288 .limit(0);
289 for id in [targeted, poolwide].into_iter().flatten() {
290 let _ = client.subscribe_with_id_to([relay.clone()], id, filter.clone(), None).await;
291 }
292}
293
294pub async fn dispatch_event(session: &SessionGuard, event: Event, handler: Arc<dyn InboundEventHandler>) {
299 let Some(my_pk) = crate::my_public_key() else {
300 return;
301 };
302 if !session.is_valid() {
303 return;
304 }
305 {
308 let mut seen = V2_SEEN_WRAPS.lock().await;
309 if !seen.insert(event.id.to_bytes()) {
310 return;
311 }
312 if seen.len() > SEEN_WRAPS_CAP {
313 let keep = event.id.to_bytes();
314 seen.clear();
315 seen.insert(keep);
316 }
317 }
318 let communities = load_held_v2();
319 for c in &communities {
320 match inbound::dispatch_wrap(&event, c, &my_pk, &*handler) {
321 inbound::DispatchedV2::NotOurs => continue,
322 inbound::DispatchedV2::Control { .. } | inbound::DispatchedV2::Rekey { .. } => {
327 enqueue_follow(c.id());
328 return;
329 }
330 inbound::DispatchedV2::Dissolved { community_id } => {
331 if crate::db::community::set_community_dissolved(&community_id).unwrap_or(false) {
336 handler.on_community_dissolved(&community_id);
337 if let Some(client) = crate::state::nostr_client() {
338 refresh_subscription(&client).await;
339 }
340 }
341 return;
342 }
343 inbound::DispatchedV2::Chat { channel_id, event } => {
350 if !session.is_valid() {
351 return;
352 }
353 match inbound::persist_chat_event(&event, &channel_id, &my_pk, session).await {
354 Some(inbound::ChatPersist::New(message)) => handler.on_community_message(&channel_id, &message, true),
355 Some(inbound::ChatPersist::Updated { message, .. }) => handler.on_community_update(&channel_id, &message.id, &message),
358 Some(inbound::ChatPersist::ReactionRemoved { message, .. }) => handler.on_community_update(&channel_id, &message.id, &message),
361 Some(inbound::ChatPersist::Removed(target_id)) => handler.on_community_removed(&channel_id, &target_id),
362 None => {}
363 }
364 return;
365 }
366 inbound::DispatchedV2::Presence { .. } => {
367 if !session.is_valid() {
372 return;
373 }
374 let gb = derive::guestbook_group_key(&c.community_root, c.id(), c.root_epoch);
375 if let Ok(opened) = super::stream::open_wrap(&event, &gb) {
376 if let Ok(ev) = super::guestbook::parse_guestbook_event(&opened) {
377 let changed = super::service::ingest_guestbook_event(c, ev, event.created_at.as_secs()).unwrap_or(false);
378 if changed && session.is_valid() {
379 handler.on_community_refreshed(&crate::simd::hex::bytes_to_hex_32(&c.id().0));
380 }
381 }
382 }
383 return;
384 }
385 _ => return, }
387 }
388}
389
390pub fn follow_worker_running() -> bool {
394 V2_FOLLOW_TX.lock().unwrap().as_ref().map(|tx| !tx.is_closed()).unwrap_or(false)
395}
396
397pub fn enqueue_follow(id: &CommunityId) {
403 let mut pending = V2_FOLLOW_PENDING.lock().unwrap();
404 if !pending.insert(id.0) {
405 return; }
407 match V2_FOLLOW_TX.lock().unwrap().as_ref() {
408 Some(tx) if tx.send(*id).is_ok() => {}
409 _ => {
410 pending.remove(&id.0); }
412 }
413}
414
415pub fn spawn_follow_worker(handler: Arc<dyn InboundEventHandler>) {
420 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<CommunityId>();
421 *V2_FOLLOW_TX.lock().unwrap() = Some(tx);
422 V2_FOLLOW_PENDING.lock().unwrap().clear();
423 let session = SessionGuard::capture();
424 tokio::spawn(async move {
425 while let Some(id) = rx.recv().await {
426 if !session.is_valid() {
427 break;
428 }
429 V2_FOLLOW_PENDING.lock().unwrap().remove(&id.0);
432 follow_community(&session, &id, &*handler).await;
433 }
434 });
435}
436
437async fn follow_community(session: &SessionGuard, id: &CommunityId, handler: &dyn InboundEventHandler) {
444 let Some(client) = crate::state::nostr_client() else {
445 return;
446 };
447 let lock = follow_lock(id);
450 let _guard = lock.lock().await;
451 let community_id = crate::simd::hex::bytes_to_hex_32(&id.0);
452 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
453
454 let Ok(Some(current)) = crate::db::community::load_community_v2(id) else {
456 return; };
458 match super::service::follow_rekeys(&transport, ¤t, session).await {
459 Ok(follow) if follow.dissolved => {
462 if !session.is_valid() {
463 return;
464 }
465 handler.on_community_dissolved(&community_id);
466 return;
467 }
468 Ok(follow) if follow.self_removed => {
469 if !session.is_valid() {
470 return;
471 }
472 let _ = crate::db::community::delete_community(&community_id);
473 refresh_subscription(&client).await;
474 handler.on_community_self_removed(&community_id);
475 return;
476 }
477 Ok(follow) if follow.updated.is_some() => {
478 if !session.is_valid() {
479 return;
480 }
481 refresh_subscription(&client).await;
482 handler.on_community_refreshed(&community_id);
483 }
484 Ok(_) => {}
485 Err(_) => return,
486 }
487
488 let Ok(Some(current)) = crate::db::community::load_community_v2(id) else {
490 return;
491 };
492 if let Ok(Some(_)) = super::service::follow_control(&transport, ¤t, session).await {
493 if !session.is_valid() {
494 return;
495 }
496 refresh_subscription(&client).await;
497 handler.on_community_refreshed(&community_id);
498 enqueue_follow(id);
504 }
505
506 let Ok(Some(current)) = crate::db::community::load_community_v2(id) else {
512 return;
513 };
514 if let Ok(fresh) = super::service::sync_guestbook(&transport, ¤t, session).await {
515 if fresh.is_empty() || !session.is_valid() {
516 return;
517 }
518 surface_presence(¤t, &fresh, handler);
519 handler.on_community_refreshed(&community_id);
520 }
521}
522
523fn surface_presence(
527 community: &CommunityV2,
528 fresh: &[super::guestbook::GuestbookEvent],
529 handler: &dyn InboundEventHandler,
530) {
531 use super::guestbook::GuestbookEntry;
532 use nostr_sdk::prelude::ToBech32;
533 let Some(primary) = community.primary_channel() else {
534 return;
535 };
536 let chat_id = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
537 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
538 let banned = crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default();
539 for ev in fresh {
540 let (member, joined, at_ms, invited_by) = match &ev.entry {
541 GuestbookEntry::Join { member, at_ms, invited_by } => (member, true, *at_ms, invited_by.clone()),
542 GuestbookEntry::Leave { member, at_ms } => (member, false, *at_ms, None),
543 GuestbookEntry::Kick { .. } | GuestbookEntry::Snapshot { .. } => continue,
544 };
545 if banned.contains(&member.to_hex()) {
546 continue;
547 }
548 let Ok(npub) = member.to_bech32() else { continue };
549 let event_id = crate::simd::hex::bytes_to_hex_32(&ev.rumor_id);
550 let (by, label) = match &invited_by {
551 Some((c, l)) => (Some(c.as_str()), Some(l.as_str())),
552 None => (None, None),
553 };
554 handler.on_community_presence(&chat_id, &npub, joined, &event_id, at_ms / 1000, by, label);
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561 use super::super::control::{genesis, CommunityMetadata};
562 use crate::community::Epoch;
563 use nostr_sdk::prelude::Keys;
564
565 fn a_community(name: &str) -> CommunityV2 {
566 let owner = Keys::generate();
567 let g = genesis(&owner, CommunityMetadata { name: name.into(), ..Default::default() }, 1_000).unwrap();
568 CommunityV2::from_genesis(&g, name, None, vec!["wss://r".into()], 0)
569 }
570
571 #[test]
572 fn plane_authors_covers_the_dispatched_planes_only() {
573 let c = a_community("A");
574 let authors = plane_authors(std::slice::from_ref(&c));
575
576 let gb = derive::guestbook_group_key(&c.community_root, c.id(), c.root_epoch).pk();
579 let control = derive::control_group_key(&c.community_root, c.id(), c.root_epoch).pk();
580 let general = {
581 let (s, e) = c.channel_secret(&c.channels[0]);
582 derive::channel_group_key(&s, &c.channels[0].id, e).pk()
583 };
584 let next_base = derive::base_rekey_group_key(&c.community_root, c.id(), Epoch(1)).pk();
587 let dissolved = derive::dissolved_group_key(c.id()).pk();
589 assert!(
590 authors.contains(&gb)
591 && authors.contains(&control)
592 && authors.contains(&general)
593 && authors.contains(&next_base)
594 && authors.contains(&dissolved)
595 );
596 assert_eq!(authors.len(), 5, "guestbook + control + dissolved + chat + base-rekey planes are subscribed");
597 }
598
599 #[test]
600 fn plane_authors_is_deterministic_deduped_and_multi_community() {
601 let a = a_community("A");
602 let b = a_community("B");
603 let one = plane_authors(std::slice::from_ref(&a));
604 assert_eq!(plane_authors(std::slice::from_ref(&a)), one);
606 let two = plane_authors(&[a.clone(), b.clone()]);
608 assert_eq!(two.len(), one.len() * 2);
609 assert_eq!(plane_authors(&[b, a]), two);
611 }
612
613 #[tokio::test]
614 async fn dispatch_event_routes_a_v2_message_to_the_handler() {
615 use crate::community::transport::memory::MemoryRelay;
616 use crate::community::transport::{Query, Transport};
617 use crate::types::Message;
618 use std::sync::Mutex as StdMutex;
619
620 #[derive(Default)]
621 struct Recorder {
622 got: StdMutex<Vec<(String, String)>>,
623 }
624 impl InboundEventHandler for Recorder {
625 fn on_community_message(&self, chat_id: &str, msg: &Message, _new: bool) {
626 self.got.lock().unwrap().push((chat_id.to_string(), msg.content.clone()));
627 }
628 }
629
630 let _g = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
632 crate::db::close_database();
633 crate::db::clear_id_caches();
634 let tmp = tempfile::tempdir().unwrap();
635 let acct = {
636 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
637 let mut s = String::from("npub1");
638 for i in 0..58 {
639 s.push(B[(i * 5 + 1) % 32] as char);
640 }
641 s
642 };
643 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
644 crate::db::set_app_data_dir(tmp.path().to_path_buf());
645 crate::db::set_current_account(acct.clone()).unwrap();
646 crate::db::init_database(&acct).unwrap();
647 let _ = crate::state::take_nostr_client();
648 let me = Keys::generate();
649 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
650 crate::state::set_my_public_key(me.public_key());
651
652 let relay = MemoryRelay::new();
656 let community = super::super::service::create_community(&relay, "Live", vec!["wss://r".into()], None).await.unwrap();
657 let general = community.channels[0].id;
658 let member = Keys::generate();
659 let group = derive::channel_group_key(&community.community_root, &general, community.root_epoch);
660 let rumor = super::super::chat::build_message_rumor(member.public_key(), &general, community.root_epoch, "live ping", None, &[], vec![], 5_000);
661 let (wrap, _) = super::super::chat::seal_chat_rumor(&rumor, &group, &member, nostr_sdk::prelude::Timestamp::from_secs(5), false).unwrap();
662 let _ = relay.publish(&wrap, &community.relays).await;
663 let q = Query { kinds: vec![stream::KIND_WRAP], authors: vec![group.pk_hex()], ..Default::default() };
664 let wrap = relay.fetch(&q, &community.relays).await.unwrap().into_iter().find(|w| w.pubkey == group.pk()).unwrap();
665
666 let rec = Arc::new(Recorder::default());
672 let session = SessionGuard::capture();
673 crate::community::v2::realtime::clear().await; dispatch_event(&session, wrap.clone(), rec.clone()).await;
675 dispatch_event(&session, wrap, rec.clone()).await;
676
677 let got = rec.got.lock().unwrap();
678 assert_eq!(got.len(), 1, "a re-delivered wrap fires the handler exactly once");
679 assert_eq!(got[0].1, "live ping");
680 assert_eq!(got[0].0, crate::simd::hex::bytes_to_hex_32(&general.0));
681 }
682
683 #[tokio::test]
684 async fn follow_queue_coalesces_a_burst_and_re_enqueues_after_processing() {
685 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<CommunityId>();
688 *V2_FOLLOW_TX.lock().unwrap() = Some(tx);
689 V2_FOLLOW_PENDING.lock().unwrap().clear();
690
691 let id = CommunityId([0x11; 32]);
692 enqueue_follow(&id);
694 enqueue_follow(&id);
695 enqueue_follow(&id);
696 assert_eq!(rx.recv().await, Some(id), "first trigger queues a follow");
697 assert!(rx.try_recv().is_err(), "the burst coalesced to exactly one");
698
699 V2_FOLLOW_PENDING.lock().unwrap().remove(&id.0);
702 enqueue_follow(&id);
703 assert_eq!(rx.recv().await, Some(id), "a trigger after processing re-queues");
704
705 let id2 = CommunityId([0x22; 32]);
707 enqueue_follow(&id2);
708 assert_eq!(rx.recv().await, Some(id2));
709
710 *V2_FOLLOW_TX.lock().unwrap() = None;
711 V2_FOLLOW_PENDING.lock().unwrap().clear();
712 }
713
714 #[tokio::test]
715 async fn a_dissolved_community_honors_no_new_events_and_fires_death_once() {
716 use super::super::service;
717 use crate::community::transport::memory::MemoryRelay;
718 use crate::community::transport::Transport;
719 use crate::types::Message;
720 use std::sync::Mutex as StdMutex;
721
722 #[derive(Default)]
723 struct Recorder {
724 messages: StdMutex<Vec<String>>,
725 deaths: StdMutex<Vec<String>>,
726 }
727 impl InboundEventHandler for Recorder {
728 fn on_community_message(&self, _chat: &str, msg: &Message, _new: bool) {
729 self.messages.lock().unwrap().push(msg.content.clone());
730 }
731 fn on_community_dissolved(&self, community_id: &str) {
732 self.deaths.lock().unwrap().push(community_id.to_string());
733 }
734 }
735
736 let _g = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
737 crate::db::close_database();
738 crate::db::clear_id_caches();
739 let tmp = tempfile::tempdir().unwrap();
740 let acct = {
741 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
742 let mut s = String::from("npub1");
743 for i in 0..58 {
744 s.push(B[(i * 3 + 2) % 32] as char);
745 }
746 s
747 };
748 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
749 crate::db::set_app_data_dir(tmp.path().to_path_buf());
750 crate::db::set_current_account(acct.clone()).unwrap();
751 crate::db::init_database(&acct).unwrap();
752 let _ = crate::state::take_nostr_client();
753 let me = Keys::generate();
754 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
755 crate::state::set_my_public_key(me.public_key());
756
757 let relay = MemoryRelay::new();
758 let community = service::create_community(&relay, "Doomed", vec!["wss://r".into()], None).await.unwrap();
759 let general = community.channels[0].id;
760
761 let rumor = super::super::dissolution::dissolved_tombstone_rumor(me.public_key(), community.id(), 8_000);
766 let tombstone = super::super::dissolution::seal_dissolved(&rumor, community.id(), &me, nostr_sdk::prelude::Timestamp::from_secs(8_000)).unwrap();
767 let _ = relay.publish(&tombstone, &community.relays).await;
768 assert!(!crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&community.id().0)).unwrap(), "not yet locally sealed");
769
770 let rec = Arc::new(Recorder::default());
771 let session = SessionGuard::capture();
772 clear().await;
773 dispatch_event(&session, tombstone.clone(), rec.clone()).await;
776 dispatch_event(&session, tombstone, rec.clone()).await;
777 assert!(crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&community.id().0)).unwrap());
778
779 let member = Keys::generate();
783 let cgroup = derive::channel_group_key(&community.community_root, &general, community.root_epoch);
784 let rumor = super::super::chat::build_message_rumor(member.public_key(), &general, community.root_epoch, "into the grave", None, &[], vec![], 9_000);
785 let (mw, _) = super::super::chat::seal_chat_rumor(&rumor, &cgroup, &member, nostr_sdk::prelude::Timestamp::from_secs(9), false).unwrap();
786 dispatch_event(&session, mw, rec.clone()).await;
787
788 assert_eq!(rec.deaths.lock().unwrap().len(), 1, "death is announced exactly once");
789 assert!(rec.messages.lock().unwrap().is_empty(), "a post-tombstone message is never honored (CORD-02 §9)");
790 }
791
792 #[test]
793 fn a_private_channel_subscribes_to_its_own_chat_plane() {
794 let mut c = a_community("Priv");
795 c.channels.push(super::super::community::ChannelV2 {
796 id: crate::community::ChannelId([0x33; 32]),
797 name: "mods".into(),
798 private: true,
799 key: Some([0x44; 32]),
800 epoch: Epoch(1),
801 voice: None,
802 meta_custom: None,
803 meta_extra: Default::default(),
804 });
805 let authors = plane_authors(std::slice::from_ref(&c));
806 let priv_chat = derive::channel_group_key(&[0x44; 32], &c.channels[1].id, Epoch(1)).pk();
808 assert!(authors.contains(&priv_chat), "a private channel subscribes to its own chat plane");
809 let next_rekey = derive::channel_rekey_group_key(&c.community_root, &c.channels[1].id, Epoch(2)).pk();
812 assert!(authors.contains(&next_rekey), "a private channel's next rekey plane is subscribed");
813 }
814}