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;
27use crate::ClientRelayExt;
28
29static V2_SUB_ID: LazyLock<Mutex<Option<SubscriptionId>>> = LazyLock::new(|| Mutex::new(None));
31static V2_POOLWIDE_SUB_ID: LazyLock<Mutex<Option<SubscriptionId>>> = LazyLock::new(|| Mutex::new(None));
33static V2_SUB_SET: LazyLock<Mutex<Vec<String>>> = LazyLock::new(|| Mutex::new(Vec::new()));
36static V2_SEEN_WRAPS: LazyLock<Mutex<HashSet<[u8; 32]>>> = LazyLock::new(|| Mutex::new(HashSet::new()));
44const SEEN_WRAPS_CAP: usize = 8192;
46static V2_FOLLOW_TX: LazyLock<StdMutex<Option<UnboundedSender<CommunityId>>>> = LazyLock::new(|| StdMutex::new(None));
56static V2_FOLLOW_PENDING: LazyLock<StdMutex<HashSet<[u8; 32]>>> = LazyLock::new(|| StdMutex::new(HashSet::new()));
58static V2_FOLLOW_LOCKS: LazyLock<StdMutex<std::collections::HashMap<[u8; 32], Arc<Mutex<()>>>>> =
65 LazyLock::new(|| StdMutex::new(std::collections::HashMap::new()));
66
67pub(crate) fn follow_lock(id: &CommunityId) -> Arc<Mutex<()>> {
69 V2_FOLLOW_LOCKS.lock().unwrap().entry(id.0).or_default().clone()
70}
71
72pub async fn subscribed_author_set() -> Vec<String> {
77 V2_SUB_SET.lock().await.clone()
78}
79
80#[cfg(debug_assertions)]
86pub async fn debug_run_follow_stages(id: &CommunityId, session: &SessionGuard) -> (String, String, String) {
87 let lock = follow_lock(id);
88 let _guard = lock.lock().await;
89 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
90
91 let Ok(Some(c)) = crate::db::community::load_community_v2(id) else {
92 return ("community gone".into(), "-".into(), "-".into());
93 };
94 let rekeys = match super::service::follow_rekeys(&transport, &c, session).await {
95 Ok(f) => format!(
96 "Ok(updated={} self_removed={} dissolved={})",
97 f.updated.as_ref().map(|u| format!("root_e{}", u.root_epoch.0)).unwrap_or_else(|| "no".into()),
98 f.self_removed, f.dissolved
99 ),
100 Err(e) => format!("ERR: {e}"),
101 };
102 let Ok(Some(c)) = crate::db::community::load_community_v2(id) else {
103 return (rekeys, "community gone".into(), "-".into());
104 };
105 let control = match super::service::follow_control(&transport, &c, session).await {
106 Ok(v) => format!("Ok(changed={})", v.is_some()),
107 Err(e) => format!("ERR: {e}"),
108 };
109 let Ok(Some(c)) = crate::db::community::load_community_v2(id) else {
110 return (rekeys, control, "community gone".into());
111 };
112 let guestbook = match super::service::sync_guestbook(&transport, &c, session).await {
113 Ok(fresh) => format!("Ok(fresh={})", fresh.len()),
114 Err(e) => format!("ERR: {e}"),
115 };
116 (rekeys, control, guestbook)
117}
118
119pub async fn subscription_id() -> Option<SubscriptionId> {
120 V2_SUB_ID.lock().await.clone()
121}
122
123pub async fn poolwide_subscription_id() -> Option<SubscriptionId> {
124 V2_POOLWIDE_SUB_ID.lock().await.clone()
125}
126
127pub async fn clear() {
130 *V2_SUB_ID.lock().await = None;
131 *V2_POOLWIDE_SUB_ID.lock().await = None;
132 V2_SUB_SET.lock().await.clear();
133 V2_SEEN_WRAPS.lock().await.clear();
134 *V2_FOLLOW_TX.lock().unwrap() = None;
137 V2_FOLLOW_PENDING.lock().unwrap().clear();
138 V2_FOLLOW_LOCKS.lock().unwrap().clear();
139 super::streamauth::clear();
142}
143
144pub fn plane_authors(communities: &[CommunityV2]) -> Vec<PublicKey> {
155 let mut out = Vec::new();
156 for c in communities {
157 out.push(derive::guestbook_group_key(&c.community_root, c.id(), c.root_epoch).pk());
158 out.push(control_author(c));
159 out.push(super::derive::dissolved_group_key(c.id()).pk());
161 for ch in &c.channels {
162 if ch.private && ch.key.is_none() {
166 continue;
167 }
168 let (secret, epoch) = c.channel_secret(ch);
169 out.push(derive::channel_group_key(&secret, &ch.id, epoch).pk());
170 }
171 out.extend(rekey_authors(c));
172 }
173 out.sort_by_key(|p| p.to_hex());
174 out.dedup();
175 out
176}
177
178pub(crate) fn control_author(c: &CommunityV2) -> PublicKey {
182 derive::control_group_key(&c.community_root, c.id(), c.root_epoch).pk()
183}
184
185pub(crate) fn rekey_authors(c: &CommunityV2) -> Vec<PublicKey> {
192 let mut out = vec![derive::base_rekey_group_key(&c.community_root, c.id(), Epoch(c.root_epoch.0.saturating_add(1))).pk()];
196 for ch in &c.channels {
197 if ch.private {
198 out.push(derive::channel_rekey_group_key(&c.community_root, &ch.id, Epoch(ch.epoch.0.saturating_add(1))).pk());
199 }
200 }
201 out
202}
203
204pub fn load_held_v2() -> Vec<CommunityV2> {
211 let ids = crate::db::community::list_community_ids().unwrap_or_default();
212 ids.iter()
213 .filter(|id| matches!(crate::db::community::community_protocol(id).ok().flatten(), Some(ConcordProtocol::V2)))
214 .filter(|id| !crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false))
215 .filter_map(|id| crate::db::community::load_community_v2(id).ok().flatten())
216 .collect()
217}
218
219pub async fn refresh_subscription(client: &Client) {
223 {
227 let communities = load_held_v2();
228 let mut relays: Vec<String> = communities.iter().flat_map(|c| c.relays.iter().cloned()).collect();
229 relays.sort();
230 relays.dedup();
231 if !relays.is_empty() {
232 for r in &relays {
234 let _ = client.add_managed_relay(r.as_str()).capabilities(crate::community_relay_capabilities()).await;
235 }
236 client.connect().await;
237 let wanted: Vec<RelayUrl> = relays.iter().filter_map(|r| RelayUrl::parse(r).ok()).collect();
241 for _ in 0..24 {
242 let pool = client.relays().all().await;
243 if wanted.iter().any(|u| pool.get(u).map(|r| r.status() == RelayStatus::Connected).unwrap_or(false)) {
244 break;
245 }
246 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
247 }
248 for c in &communities {
253 super::streamauth::register_community(c);
254 }
255 super::streamauth::prime_auth(client, &relays).await;
256 }
257 }
258
259 let mut sub_guard = V2_SUB_ID.lock().await;
267 let mut set_guard = V2_SUB_SET.lock().await;
268
269 let communities = load_held_v2();
270 let authors = plane_authors(&communities);
271 let mut relays: Vec<String> = communities.iter().flat_map(|c| c.relays.iter().cloned()).collect();
272 relays.sort();
273 relays.dedup();
274
275 let mut new_set: Vec<String> = authors.iter().map(|p| p.to_hex()).collect();
276 new_set.sort();
277
278 if sub_guard.is_some() && *set_guard == new_set && (authors.is_empty() || V2_POOLWIDE_SUB_ID.lock().await.is_some()) {
282 return; }
284 if let Some(old) = sub_guard.take() {
285 let _ = client.unsubscribe(&old).await;
286 }
287 *set_guard = new_set;
288
289 if authors.is_empty() {
290 if let Some(old_pw) = V2_POOLWIDE_SUB_ID.lock().await.take() {
291 let _ = client.unsubscribe(&old_pw).await;
292 }
293 return;
294 }
295
296 let filter = Filter::new()
297 .kinds([Kind::Custom(stream::KIND_WRAP), Kind::Custom(stream::KIND_WRAP_EPHEMERAL)])
298 .authors(authors)
299 .limit(0);
300
301 {
302 let mut pw = V2_POOLWIDE_SUB_ID.lock().await;
303 if let Some(old) = pw.take() {
304 let _ = client.unsubscribe(&old).await;
305 }
306 if let Ok(out) = client.subscribe(filter.clone()).await {
307 *pw = Some(out.value);
308 }
309 }
310 if let Ok(out) = client
311 .subscribe(nostr_sdk::prelude::ReqTarget::manual(
312 relays.iter().cloned().map(|u| (u, vec![filter.clone()])),
313 ))
314 .await
315 {
316 *sub_guard = Some(out.value);
317 }
318}
319
320pub(crate) async fn resubscribe_relay(client: &Client, relay: &RelayUrl) {
328 let targeted = V2_SUB_ID.lock().await.clone();
329 let poolwide = V2_POOLWIDE_SUB_ID.lock().await.clone();
330 if targeted.is_none() && poolwide.is_none() {
331 return; }
333 let communities = load_held_v2();
334 let authors = plane_authors(&communities);
335 if authors.is_empty() {
336 return;
337 }
338 let filter = Filter::new()
339 .kinds([Kind::Custom(stream::KIND_WRAP), Kind::Custom(stream::KIND_WRAP_EPHEMERAL)])
340 .authors(authors)
341 .limit(0);
342 for id in [targeted, poolwide].into_iter().flatten() {
343 let _ = client
344 .subscribe(nostr_sdk::prelude::ReqTarget::single(relay.clone(), [filter.clone()]))
345 .with_id(id)
346 .await;
347 }
348}
349
350pub async fn dispatch_event(session: &SessionGuard, event: Event, handler: Arc<dyn InboundEventHandler>) {
355 let Some(my_pk) = crate::my_public_key() else {
356 return;
357 };
358 if !session.is_valid() {
359 return;
360 }
361 {
364 let mut seen = V2_SEEN_WRAPS.lock().await;
365 if !seen.insert(event.id.to_bytes()) {
366 return;
367 }
368 if seen.len() > SEEN_WRAPS_CAP {
369 let keep = event.id.to_bytes();
370 seen.clear();
371 seen.insert(keep);
372 }
373 }
374 let communities = load_held_v2();
375 for c in &communities {
376 match inbound::dispatch_wrap(&event, c, &my_pk, &*handler) {
377 inbound::DispatchedV2::NotOurs => continue,
378 inbound::DispatchedV2::Control { .. } | inbound::DispatchedV2::Rekey { .. } => {
383 enqueue_follow(c.id());
384 return;
385 }
386 inbound::DispatchedV2::Dissolved { community_id } => {
387 if crate::db::community::set_community_dissolved(&community_id).unwrap_or(false) {
392 handler.on_community_dissolved(&community_id);
393 if let Some(client) = crate::state::nostr_client() {
394 refresh_subscription(&client).await;
395 }
396 }
397 return;
398 }
399 inbound::DispatchedV2::Chat { channel_id, event } => {
406 if !session.is_valid() {
407 return;
408 }
409 match inbound::persist_chat_event(&event, &channel_id, &my_pk, session).await {
410 Some(inbound::ChatPersist::New(message)) => handler.on_community_message(&channel_id, &message, true),
411 Some(inbound::ChatPersist::Updated { mut message, .. }) => {
416 let _ = crate::db::events::populate_reply_context(&mut message).await;
417 handler.on_community_update(&channel_id, &message.id, &message);
418 }
419 Some(inbound::ChatPersist::ReactionRemoved { mut message, .. }) => {
422 let _ = crate::db::events::populate_reply_context(&mut message).await;
423 handler.on_community_update(&channel_id, &message.id, &message);
424 }
425 Some(inbound::ChatPersist::Removed(target_id)) => handler.on_community_removed(&channel_id, &target_id),
426 None => {}
427 }
428 return;
429 }
430 inbound::DispatchedV2::Presence { .. } => {
431 if !session.is_valid() {
436 return;
437 }
438 let gb = derive::guestbook_group_key(&c.community_root, c.id(), c.root_epoch);
439 if let Ok(opened) = super::stream::open_wrap(&event, &gb) {
440 if let Ok(ev) = super::guestbook::parse_guestbook_event(&opened) {
441 let changed = super::service::ingest_guestbook_event(c, ev, event.created_at.as_secs()).unwrap_or(false);
442 if changed && session.is_valid() {
443 handler.on_community_refreshed(&crate::simd::hex::bytes_to_hex_32(&c.id().0));
444 }
445 }
446 }
447 return;
448 }
449 inbound::DispatchedV2::Kick { target } => {
450 if !session.is_valid() {
451 return;
452 }
453 let gb = derive::guestbook_group_key(&c.community_root, c.id(), c.root_epoch);
454 let Ok(opened) = super::stream::open_wrap(&event, &gb) else { return };
455 let Ok(ev) = super::guestbook::parse_guestbook_event(&opened) else { return };
456 if !super::service::ingest_guestbook_event(c, ev, event.created_at.as_secs()).unwrap_or(false) {
457 return;
458 }
459 if !session.is_valid() {
460 return;
461 }
462 let community_id = crate::simd::hex::bytes_to_hex_32(&c.id().0);
463 let evicted = my_pk == target && super::service::stored_kick_verdict(c, &my_pk);
475 if evicted {
476 crate::log_warn!(
480 "[v2:teardown {}] KICK: the authorized guestbook fold rules us kicked",
481 &community_id[..8.min(community_id.len())]
482 );
483 handler.on_community_self_removed(&community_id);
484 } else {
485 crate::log_debug!(
486 "[v2:kick {}] declined: target={} is not ruled kicked by the fold",
487 &community_id[..8.min(community_id.len())], &target.to_hex()[..8]
488 );
489 handler.on_community_refreshed(&community_id);
490 }
491 return;
492 }
493 _ => return, }
495 }
496}
497
498pub fn follow_worker_running() -> bool {
502 V2_FOLLOW_TX.lock().unwrap().as_ref().map(|tx| !tx.is_closed()).unwrap_or(false)
503}
504
505pub fn enqueue_follow(id: &CommunityId) {
511 let mut pending = V2_FOLLOW_PENDING.lock().unwrap();
512 if !pending.insert(id.0) {
513 return; }
515 match V2_FOLLOW_TX.lock().unwrap().as_ref() {
516 Some(tx) if tx.send(*id).is_ok() => {}
517 _ => {
518 }
526 }
527}
528
529pub fn spawn_follow_worker(handler: Arc<dyn InboundEventHandler>) {
534 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<CommunityId>();
535 {
540 let pending = V2_FOLLOW_PENDING.lock().unwrap();
541 for id in pending.iter() {
542 let _ = tx.send(CommunityId(*id));
543 }
544 }
545 *V2_FOLLOW_TX.lock().unwrap() = Some(tx);
546 let session = SessionGuard::capture();
547 tokio::spawn(async move {
548 while let Some(id) = rx.recv().await {
549 if !session.is_valid() {
550 break;
551 }
552 V2_FOLLOW_PENDING.lock().unwrap().remove(&id.0);
555 follow_community(&session, &id, &*handler).await;
556 }
557 });
558}
559
560async fn follow_community(session: &SessionGuard, id: &CommunityId, handler: &dyn InboundEventHandler) {
567 let Some(client) = crate::state::nostr_client() else {
568 return;
569 };
570 let lock = follow_lock(id);
573 let _guard = lock.lock().await;
574 let community_id = crate::simd::hex::bytes_to_hex_32(&id.0);
575 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
576
577 let Ok(Some(current)) = crate::db::community::load_community_v2(id) else {
579 return; };
581 match super::service::follow_rekeys(&transport, ¤t, session).await {
582 Ok(follow) if follow.dissolved => {
585 if !session.is_valid() {
586 return;
587 }
588 crate::log_warn!("[v2:teardown {}] DISSOLVED tombstone", &community_id[..8.min(community_id.len())]);
589 handler.on_community_dissolved(&community_id);
590 return;
591 }
592 Ok(follow) if follow.self_removed => {
593 if !session.is_valid() {
594 return;
595 }
596 crate::log_warn!("[v2:teardown {}] REKEY EXCLUSION: an authorized rotation left us out", &community_id[..8.min(community_id.len())]);
597 let _ = crate::db::community::delete_community(&community_id);
598 refresh_subscription(&client).await;
599 handler.on_community_self_removed(&community_id);
600 return;
601 }
602 Ok(follow) if follow.updated.is_some() => {
603 if !session.is_valid() {
604 return;
605 }
606 refresh_subscription(&client).await;
607 handler.on_community_refreshed(&community_id);
608 }
609 Ok(_) => {}
610 Err(e) => {
611 crate::log_warn!("[v2:follow {}] rekey follow failed (will retry on next trigger): {}", &community_id[..8.min(community_id.len())], e);
615 return;
616 }
617 }
618
619 let Ok(Some(current)) = crate::db::community::load_community_v2(id) else {
621 return;
622 };
623 let control_changed = matches!(
624 super::service::follow_control(&transport, ¤t, session).await,
625 Ok(Some(_))
626 );
627 if !session.is_valid() {
628 return;
629 }
630 if let Ok(Some(folded)) = crate::db::community::load_community_v2(id) {
635 let keyed = super::service::absorb_parked_channel_keys(&folded, session);
636 if !keyed.is_empty() {
637 crate::log_info!(
638 "[v2:follow {}] adopted {} vended private-channel key(s)",
639 &community_id[..8.min(community_id.len())],
640 keyed.len()
641 );
642 refresh_subscription(&client).await;
647 }
648 for ch in keyed {
649 let hex = crate::simd::hex::bytes_to_hex_32(&ch.0);
650 let _ = crate::VectorCore::v2_backfill_channel(
654 id, &hex, 50, 2, None,
655 crate::community::transport::Evidence::Fast, 12,
656 )
657 .await;
658 if !session.is_valid() {
659 return;
660 }
661 handler.on_channel_keyed(&community_id, &hex);
662 }
663 }
664 if control_changed {
665 refresh_subscription(&client).await;
666 handler.on_community_refreshed(&community_id);
667 enqueue_follow(id);
673 }
674
675 if let Some(me) = crate::my_public_key() {
684 if crate::db::community::is_author_banned(&community_id, &me) {
685 if !session.is_valid() {
686 return;
687 }
688 crate::log_warn!("[v2:teardown {}] SELF-BAN: our npub is in the folded banlist", &community_id[..8.min(community_id.len())]);
689 handler.on_community_self_removed(&community_id);
690 return;
691 }
692 }
693
694 let Ok(Some(current)) = crate::db::community::load_community_v2(id) else {
700 return;
701 };
702 if let Ok(fresh) = super::service::sync_guestbook(&transport, ¤t, session).await {
703 if fresh.is_empty() || !session.is_valid() {
704 return;
705 }
706 surface_presence(¤t, &fresh, handler);
707 handler.on_community_refreshed(&community_id);
708 }
709}
710
711fn surface_presence(
715 community: &CommunityV2,
716 fresh: &[super::guestbook::GuestbookEvent],
717 handler: &dyn InboundEventHandler,
718) {
719 use super::guestbook::GuestbookEntry;
720 use nostr_sdk::prelude::ToBech32;
721 let Some(primary) = community.primary_channel() else {
722 return;
723 };
724 let chat_id = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
725 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
726 let banned = crate::db::community::banned_set(&cid_hex);
727 for ev in fresh {
728 let (member, joined, at_ms, invited_by) = match &ev.entry {
729 GuestbookEntry::Join { member, at_ms, invited_by } => (member, true, *at_ms, invited_by.clone()),
730 GuestbookEntry::Leave { member, at_ms } => (member, false, *at_ms, None),
731 GuestbookEntry::Kick { .. } | GuestbookEntry::Snapshot { .. } => continue,
732 };
733 if banned.contains(&member.to_bytes()) {
734 continue;
735 }
736 let Ok(npub) = member.to_bech32();
737 let event_id = crate::simd::hex::bytes_to_hex_32(&ev.rumor_id);
738 let (by, label) = match &invited_by {
739 Some((c, l)) => (Some(c.as_str()), Some(l.as_str())),
740 None => (None, None),
741 };
742 handler.on_community_presence(&chat_id, &npub, joined, &event_id, at_ms / 1000, by, label);
743 }
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749 use super::super::control::{genesis, CommunityMetadata};
750 use crate::community::Epoch;
751 use nostr_sdk::prelude::Keys;
752
753 fn a_community(name: &str) -> CommunityV2 {
754 let owner = Keys::generate();
755 let g = genesis(&owner, CommunityMetadata { name: name.into(), ..Default::default() }, 1_000).unwrap();
756 CommunityV2::from_genesis(&g, name, None, vec!["wss://r".into()], 0)
757 }
758
759 #[test]
760 fn plane_authors_covers_the_dispatched_planes_only() {
761 let c = a_community("A");
762 let authors = plane_authors(std::slice::from_ref(&c));
763
764 let gb = derive::guestbook_group_key(&c.community_root, c.id(), c.root_epoch).pk();
767 let control = derive::control_group_key(&c.community_root, c.id(), c.root_epoch).pk();
768 let general = {
769 let (s, e) = c.channel_secret(&c.channels[0]);
770 derive::channel_group_key(&s, &c.channels[0].id, e).pk()
771 };
772 let next_base = derive::base_rekey_group_key(&c.community_root, c.id(), Epoch(1)).pk();
775 let dissolved = derive::dissolved_group_key(c.id()).pk();
777 assert!(
778 authors.contains(&gb)
779 && authors.contains(&control)
780 && authors.contains(&general)
781 && authors.contains(&next_base)
782 && authors.contains(&dissolved)
783 );
784 assert_eq!(authors.len(), 5, "guestbook + control + dissolved + chat + base-rekey planes are subscribed");
785 }
786
787 #[test]
788 fn plane_authors_is_deterministic_deduped_and_multi_community() {
789 let a = a_community("A");
790 let b = a_community("B");
791 let one = plane_authors(std::slice::from_ref(&a));
792 assert_eq!(plane_authors(std::slice::from_ref(&a)), one);
794 let two = plane_authors(&[a.clone(), b.clone()]);
796 assert_eq!(two.len(), one.len() * 2);
797 assert_eq!(plane_authors(&[b, a]), two);
799 }
800
801 #[tokio::test]
802 async fn dispatch_event_routes_a_v2_message_to_the_handler() {
803 use crate::community::transport::memory::MemoryRelay;
804 use crate::community::transport::{Query, Transport};
805 use crate::types::Message;
806 use std::sync::Mutex as StdMutex;
807
808 #[derive(Default)]
809 struct Recorder {
810 got: StdMutex<Vec<(String, String)>>,
811 }
812 impl InboundEventHandler for Recorder {
813 fn on_community_message(&self, chat_id: &str, msg: &Message, _new: bool) {
814 self.got.lock().unwrap().push((chat_id.to_string(), msg.content.clone()));
815 }
816 }
817
818 let _g = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
820 crate::db::close_database();
821 crate::db::clear_id_caches();
822 let tmp = tempfile::tempdir().unwrap();
823 let acct = {
824 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
825 let mut s = String::from("npub1");
826 for i in 0..58 {
827 s.push(B[(i * 5 + 1) % 32] as char);
828 }
829 s
830 };
831 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
832 crate::db::set_app_data_dir(tmp.path().to_path_buf());
833 crate::db::set_current_account(acct.clone()).unwrap();
834 crate::db::init_database(&acct).unwrap();
835 let _ = crate::state::take_nostr_client();
836 let me = Keys::generate();
837 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
838 crate::state::set_my_public_key(me.public_key());
839
840 let relay = MemoryRelay::new();
844 let community = super::super::service::create_community(&relay, "Live", vec!["wss://r".into()], None).await.unwrap();
845 let general = community.channels[0].id;
846 let member = Keys::generate();
847 let group = derive::channel_group_key(&community.community_root, &general, community.root_epoch);
848 let rumor = super::super::chat::build_message_rumor(member.public_key(), &general, community.root_epoch, "live ping", None, &[], vec![], 5_000);
849 let (wrap, _) = super::super::chat::seal_chat_rumor(&rumor, &group, &member, nostr_sdk::prelude::Timestamp::from_secs(5), false).unwrap();
850 let _ = relay.publish(&wrap, &community.relays).await;
851 let q = Query { kinds: vec![stream::KIND_WRAP], authors: vec![group.pk_hex()], ..Default::default() };
852 let wrap = relay.fetch(&q, &community.relays).await.unwrap().into_iter().find(|w| w.pubkey == group.pk()).unwrap();
853
854 let rec = Arc::new(Recorder::default());
860 let session = SessionGuard::capture();
861 crate::community::v2::realtime::clear().await; dispatch_event(&session, wrap.clone(), rec.clone()).await;
863 dispatch_event(&session, wrap, rec.clone()).await;
864
865 let got = rec.got.lock().unwrap();
866 assert_eq!(got.len(), 1, "a re-delivered wrap fires the handler exactly once");
867 assert_eq!(got[0].1, "live ping");
868 assert_eq!(got[0].0, crate::simd::hex::bytes_to_hex_32(&general.0));
869 }
870
871 #[tokio::test]
872 async fn follow_queue_coalesces_a_burst_and_re_enqueues_after_processing() {
873 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<CommunityId>();
876 *V2_FOLLOW_TX.lock().unwrap() = Some(tx);
877 V2_FOLLOW_PENDING.lock().unwrap().clear();
878
879 let id = CommunityId([0x11; 32]);
880 enqueue_follow(&id);
882 enqueue_follow(&id);
883 enqueue_follow(&id);
884 assert_eq!(rx.recv().await, Some(id), "first trigger queues a follow");
885 assert!(rx.try_recv().is_err(), "the burst coalesced to exactly one");
886
887 V2_FOLLOW_PENDING.lock().unwrap().remove(&id.0);
890 enqueue_follow(&id);
891 assert_eq!(rx.recv().await, Some(id), "a trigger after processing re-queues");
892
893 let id2 = CommunityId([0x22; 32]);
895 enqueue_follow(&id2);
896 assert_eq!(rx.recv().await, Some(id2));
897
898 *V2_FOLLOW_TX.lock().unwrap() = None;
899 V2_FOLLOW_PENDING.lock().unwrap().clear();
900 }
901
902 #[tokio::test]
903 async fn a_dissolved_community_honors_no_new_events_and_fires_death_once() {
904 use super::super::service;
905 use crate::community::transport::memory::MemoryRelay;
906 use crate::community::transport::Transport;
907 use crate::types::Message;
908 use std::sync::Mutex as StdMutex;
909
910 #[derive(Default)]
911 struct Recorder {
912 messages: StdMutex<Vec<String>>,
913 deaths: StdMutex<Vec<String>>,
914 }
915 impl InboundEventHandler for Recorder {
916 fn on_community_message(&self, _chat: &str, msg: &Message, _new: bool) {
917 self.messages.lock().unwrap().push(msg.content.clone());
918 }
919 fn on_community_dissolved(&self, community_id: &str) {
920 self.deaths.lock().unwrap().push(community_id.to_string());
921 }
922 }
923
924 let _g = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
925 crate::db::close_database();
926 crate::db::clear_id_caches();
927 let tmp = tempfile::tempdir().unwrap();
928 let acct = {
929 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
930 let mut s = String::from("npub1");
931 for i in 0..58 {
932 s.push(B[(i * 3 + 2) % 32] as char);
933 }
934 s
935 };
936 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
937 crate::db::set_app_data_dir(tmp.path().to_path_buf());
938 crate::db::set_current_account(acct.clone()).unwrap();
939 crate::db::init_database(&acct).unwrap();
940 let _ = crate::state::take_nostr_client();
941 let me = Keys::generate();
942 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
943 crate::state::set_my_public_key(me.public_key());
944
945 let relay = MemoryRelay::new();
946 let community = service::create_community(&relay, "Doomed", vec!["wss://r".into()], None).await.unwrap();
947 let general = community.channels[0].id;
948
949 let rumor = super::super::dissolution::dissolved_tombstone_rumor(me.public_key(), community.id(), 8_000);
954 let tombstone = super::super::dissolution::seal_dissolved(&rumor, community.id(), &me, nostr_sdk::prelude::Timestamp::from_secs(8_000)).unwrap();
955 let _ = relay.publish(&tombstone, &community.relays).await;
956 assert!(!crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&community.id().0)).unwrap(), "not yet locally sealed");
957
958 let rec = Arc::new(Recorder::default());
959 let session = SessionGuard::capture();
960 clear().await;
961 dispatch_event(&session, tombstone.clone(), rec.clone()).await;
964 dispatch_event(&session, tombstone, rec.clone()).await;
965 assert!(crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&community.id().0)).unwrap());
966
967 let member = Keys::generate();
971 let cgroup = derive::channel_group_key(&community.community_root, &general, community.root_epoch);
972 let rumor = super::super::chat::build_message_rumor(member.public_key(), &general, community.root_epoch, "into the grave", None, &[], vec![], 9_000);
973 let (mw, _) = super::super::chat::seal_chat_rumor(&rumor, &cgroup, &member, nostr_sdk::prelude::Timestamp::from_secs(9), false).unwrap();
974 dispatch_event(&session, mw, rec.clone()).await;
975
976 assert_eq!(rec.deaths.lock().unwrap().len(), 1, "death is announced exactly once");
977 assert!(rec.messages.lock().unwrap().is_empty(), "a post-tombstone message is never honored (CORD-02 §9)");
978 }
979
980 #[test]
981 fn a_private_channel_subscribes_to_its_own_chat_plane() {
982 let mut c = a_community("Priv");
983 c.channels.push(super::super::community::ChannelV2 {
984 id: crate::community::ChannelId([0x33; 32]),
985 name: "mods".into(),
986 private: true,
987 key: Some([0x44; 32]),
988 epoch: Epoch(1),
989 voice: None,
990 meta_custom: None,
991 meta_extra: Default::default(),
992 });
993 let authors = plane_authors(std::slice::from_ref(&c));
994 let priv_chat = derive::channel_group_key(&[0x44; 32], &c.channels[1].id, Epoch(1)).pk();
996 assert!(authors.contains(&priv_chat), "a private channel subscribes to its own chat plane");
997 let next_rekey = derive::channel_rekey_group_key(&c.community_root, &c.channels[1].id, Epoch(2)).pk();
1000 assert!(authors.contains(&next_rekey), "a private channel's next rekey plane is subscribed");
1001 }
1002}