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
71#[cfg(debug_assertions)]
77pub async fn debug_run_follow_stages(id: &CommunityId, session: &SessionGuard) -> (String, String, String) {
78 let lock = follow_lock(id);
79 let _guard = lock.lock().await;
80 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
81
82 let Ok(Some(c)) = crate::db::community::load_community_v2(id) else {
83 return ("community gone".into(), "-".into(), "-".into());
84 };
85 let rekeys = match super::service::follow_rekeys(&transport, &c, session).await {
86 Ok(f) => format!(
87 "Ok(updated={} self_removed={} dissolved={})",
88 f.updated.as_ref().map(|u| format!("root_e{}", u.root_epoch.0)).unwrap_or_else(|| "no".into()),
89 f.self_removed, f.dissolved
90 ),
91 Err(e) => format!("ERR: {e}"),
92 };
93 let Ok(Some(c)) = crate::db::community::load_community_v2(id) else {
94 return (rekeys, "community gone".into(), "-".into());
95 };
96 let control = match super::service::follow_control(&transport, &c, session).await {
97 Ok(v) => format!("Ok(changed={})", v.is_some()),
98 Err(e) => format!("ERR: {e}"),
99 };
100 let Ok(Some(c)) = crate::db::community::load_community_v2(id) else {
101 return (rekeys, control, "community gone".into());
102 };
103 let guestbook = match super::service::sync_guestbook(&transport, &c, session).await {
104 Ok(fresh) => format!("Ok(fresh={})", fresh.len()),
105 Err(e) => format!("ERR: {e}"),
106 };
107 (rekeys, control, guestbook)
108}
109
110pub async fn subscription_id() -> Option<SubscriptionId> {
111 V2_SUB_ID.lock().await.clone()
112}
113
114pub async fn poolwide_subscription_id() -> Option<SubscriptionId> {
115 V2_POOLWIDE_SUB_ID.lock().await.clone()
116}
117
118pub async fn clear() {
121 *V2_SUB_ID.lock().await = None;
122 *V2_POOLWIDE_SUB_ID.lock().await = None;
123 V2_SUB_SET.lock().await.clear();
124 V2_SEEN_WRAPS.lock().await.clear();
125 *V2_FOLLOW_TX.lock().unwrap() = None;
128 V2_FOLLOW_PENDING.lock().unwrap().clear();
129 V2_FOLLOW_LOCKS.lock().unwrap().clear();
130 super::streamauth::clear();
133}
134
135pub fn plane_authors(communities: &[CommunityV2]) -> Vec<PublicKey> {
146 let mut out = Vec::new();
147 for c in communities {
148 out.push(derive::guestbook_group_key(&c.community_root, c.id(), c.root_epoch).pk());
149 out.push(control_author(c));
150 out.push(super::derive::dissolved_group_key(c.id()).pk());
152 for ch in &c.channels {
153 if ch.private && ch.key.is_none() {
157 continue;
158 }
159 let (secret, epoch) = c.channel_secret(ch);
160 out.push(derive::channel_group_key(&secret, &ch.id, epoch).pk());
161 }
162 out.extend(rekey_authors(c));
163 }
164 out.sort_by_key(|p| p.to_hex());
165 out.dedup();
166 out
167}
168
169pub(crate) fn control_author(c: &CommunityV2) -> PublicKey {
173 derive::control_group_key(&c.community_root, c.id(), c.root_epoch).pk()
174}
175
176pub(crate) fn rekey_authors(c: &CommunityV2) -> Vec<PublicKey> {
183 let mut out = vec![derive::base_rekey_group_key(&c.community_root, c.id(), Epoch(c.root_epoch.0.saturating_add(1))).pk()];
187 for ch in &c.channels {
188 if ch.private {
189 out.push(derive::channel_rekey_group_key(&c.community_root, &ch.id, Epoch(ch.epoch.0.saturating_add(1))).pk());
190 }
191 }
192 out
193}
194
195pub fn load_held_v2() -> Vec<CommunityV2> {
202 let ids = crate::db::community::list_community_ids().unwrap_or_default();
203 ids.iter()
204 .filter(|id| matches!(crate::db::community::community_protocol(id).ok().flatten(), Some(ConcordProtocol::V2)))
205 .filter(|id| !crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false))
206 .filter_map(|id| crate::db::community::load_community_v2(id).ok().flatten())
207 .collect()
208}
209
210pub async fn refresh_subscription(client: &Client) {
214 {
218 let communities = load_held_v2();
219 let mut relays: Vec<String> = communities.iter().flat_map(|c| c.relays.iter().cloned()).collect();
220 relays.sort();
221 relays.dedup();
222 if !relays.is_empty() {
223 for r in &relays {
225 let _ = client.pool().add_relay(r.as_str(), crate::community_relay_options()).await;
226 }
227 client.connect().await;
228 let wanted: Vec<RelayUrl> = relays.iter().filter_map(|r| RelayUrl::parse(r).ok()).collect();
232 for _ in 0..24 {
233 let pool = client.pool().all_relays().await;
234 if wanted.iter().any(|u| pool.get(u).map(|r| r.status() == RelayStatus::Connected).unwrap_or(false)) {
235 break;
236 }
237 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
238 }
239 for c in &communities {
244 super::streamauth::register_community(c);
245 }
246 super::streamauth::prime_auth(client, &relays).await;
247 }
248 }
249
250 let mut sub_guard = V2_SUB_ID.lock().await;
258 let mut set_guard = V2_SUB_SET.lock().await;
259
260 let communities = load_held_v2();
261 let authors = plane_authors(&communities);
262 let mut relays: Vec<String> = communities.iter().flat_map(|c| c.relays.iter().cloned()).collect();
263 relays.sort();
264 relays.dedup();
265
266 let mut new_set: Vec<String> = authors.iter().map(|p| p.to_hex()).collect();
267 new_set.sort();
268
269 if sub_guard.is_some() && *set_guard == new_set && (authors.is_empty() || V2_POOLWIDE_SUB_ID.lock().await.is_some()) {
273 return; }
275 if let Some(old) = sub_guard.take() {
276 client.unsubscribe(&old).await;
277 }
278 *set_guard = new_set;
279
280 if authors.is_empty() {
281 if let Some(old_pw) = V2_POOLWIDE_SUB_ID.lock().await.take() {
282 client.unsubscribe(&old_pw).await;
283 }
284 return;
285 }
286
287 let filter = Filter::new()
288 .kinds([Kind::Custom(stream::KIND_WRAP), Kind::Custom(stream::KIND_WRAP_EPHEMERAL)])
289 .authors(authors)
290 .limit(0);
291
292 {
293 let mut pw = V2_POOLWIDE_SUB_ID.lock().await;
294 if let Some(old) = pw.take() {
295 client.unsubscribe(&old).await;
296 }
297 if let Ok(out) = client.subscribe(filter.clone(), None).await {
298 *pw = Some(out.val);
299 }
300 }
301 if let Ok(out) = client.subscribe_to(relays.iter().cloned(), filter, None).await {
302 *sub_guard = Some(out.val);
303 }
304}
305
306pub(crate) async fn resubscribe_relay(client: &Client, relay: &RelayUrl) {
314 let targeted = V2_SUB_ID.lock().await.clone();
315 let poolwide = V2_POOLWIDE_SUB_ID.lock().await.clone();
316 if targeted.is_none() && poolwide.is_none() {
317 return; }
319 let communities = load_held_v2();
320 let authors = plane_authors(&communities);
321 if authors.is_empty() {
322 return;
323 }
324 let filter = Filter::new()
325 .kinds([Kind::Custom(stream::KIND_WRAP), Kind::Custom(stream::KIND_WRAP_EPHEMERAL)])
326 .authors(authors)
327 .limit(0);
328 for id in [targeted, poolwide].into_iter().flatten() {
329 let _ = client.subscribe_with_id_to([relay.clone()], id, filter.clone(), None).await;
330 }
331}
332
333pub async fn dispatch_event(session: &SessionGuard, event: Event, handler: Arc<dyn InboundEventHandler>) {
338 let Some(my_pk) = crate::my_public_key() else {
339 return;
340 };
341 if !session.is_valid() {
342 return;
343 }
344 {
347 let mut seen = V2_SEEN_WRAPS.lock().await;
348 if !seen.insert(event.id.to_bytes()) {
349 return;
350 }
351 if seen.len() > SEEN_WRAPS_CAP {
352 let keep = event.id.to_bytes();
353 seen.clear();
354 seen.insert(keep);
355 }
356 }
357 let communities = load_held_v2();
358 for c in &communities {
359 match inbound::dispatch_wrap(&event, c, &my_pk, &*handler) {
360 inbound::DispatchedV2::NotOurs => continue,
361 inbound::DispatchedV2::Control { .. } | inbound::DispatchedV2::Rekey { .. } => {
366 enqueue_follow(c.id());
367 return;
368 }
369 inbound::DispatchedV2::Dissolved { community_id } => {
370 if crate::db::community::set_community_dissolved(&community_id).unwrap_or(false) {
375 handler.on_community_dissolved(&community_id);
376 if let Some(client) = crate::state::nostr_client() {
377 refresh_subscription(&client).await;
378 }
379 }
380 return;
381 }
382 inbound::DispatchedV2::Chat { channel_id, event } => {
389 if !session.is_valid() {
390 return;
391 }
392 match inbound::persist_chat_event(&event, &channel_id, &my_pk, session).await {
393 Some(inbound::ChatPersist::New(message)) => handler.on_community_message(&channel_id, &message, true),
394 Some(inbound::ChatPersist::Updated { message, .. }) => handler.on_community_update(&channel_id, &message.id, &message),
397 Some(inbound::ChatPersist::ReactionRemoved { message, .. }) => handler.on_community_update(&channel_id, &message.id, &message),
400 Some(inbound::ChatPersist::Removed(target_id)) => handler.on_community_removed(&channel_id, &target_id),
401 None => {}
402 }
403 return;
404 }
405 inbound::DispatchedV2::Presence { .. } => {
406 if !session.is_valid() {
411 return;
412 }
413 let gb = derive::guestbook_group_key(&c.community_root, c.id(), c.root_epoch);
414 if let Ok(opened) = super::stream::open_wrap(&event, &gb) {
415 if let Ok(ev) = super::guestbook::parse_guestbook_event(&opened) {
416 let changed = super::service::ingest_guestbook_event(c, ev, event.created_at.as_secs()).unwrap_or(false);
417 if changed && session.is_valid() {
418 handler.on_community_refreshed(&crate::simd::hex::bytes_to_hex_32(&c.id().0));
419 }
420 }
421 }
422 return;
423 }
424 _ => return, }
426 }
427}
428
429pub fn follow_worker_running() -> bool {
433 V2_FOLLOW_TX.lock().unwrap().as_ref().map(|tx| !tx.is_closed()).unwrap_or(false)
434}
435
436pub fn enqueue_follow(id: &CommunityId) {
442 let mut pending = V2_FOLLOW_PENDING.lock().unwrap();
443 if !pending.insert(id.0) {
444 return; }
446 match V2_FOLLOW_TX.lock().unwrap().as_ref() {
447 Some(tx) if tx.send(*id).is_ok() => {}
448 _ => {
449 }
457 }
458}
459
460pub fn spawn_follow_worker(handler: Arc<dyn InboundEventHandler>) {
465 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<CommunityId>();
466 {
471 let pending = V2_FOLLOW_PENDING.lock().unwrap();
472 for id in pending.iter() {
473 let _ = tx.send(CommunityId(*id));
474 }
475 }
476 *V2_FOLLOW_TX.lock().unwrap() = Some(tx);
477 let session = SessionGuard::capture();
478 tokio::spawn(async move {
479 while let Some(id) = rx.recv().await {
480 if !session.is_valid() {
481 break;
482 }
483 V2_FOLLOW_PENDING.lock().unwrap().remove(&id.0);
486 follow_community(&session, &id, &*handler).await;
487 }
488 });
489}
490
491async fn follow_community(session: &SessionGuard, id: &CommunityId, handler: &dyn InboundEventHandler) {
498 let Some(client) = crate::state::nostr_client() else {
499 return;
500 };
501 let lock = follow_lock(id);
504 let _guard = lock.lock().await;
505 let community_id = crate::simd::hex::bytes_to_hex_32(&id.0);
506 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
507
508 let Ok(Some(current)) = crate::db::community::load_community_v2(id) else {
510 return; };
512 match super::service::follow_rekeys(&transport, ¤t, session).await {
513 Ok(follow) if follow.dissolved => {
516 if !session.is_valid() {
517 return;
518 }
519 handler.on_community_dissolved(&community_id);
520 return;
521 }
522 Ok(follow) if follow.self_removed => {
523 if !session.is_valid() {
524 return;
525 }
526 let _ = crate::db::community::delete_community(&community_id);
527 refresh_subscription(&client).await;
528 handler.on_community_self_removed(&community_id);
529 return;
530 }
531 Ok(follow) if follow.updated.is_some() => {
532 if !session.is_valid() {
533 return;
534 }
535 refresh_subscription(&client).await;
536 handler.on_community_refreshed(&community_id);
537 }
538 Ok(_) => {}
539 Err(e) => {
540 crate::log_warn!("[v2:follow {}] rekey follow failed (will retry on next trigger): {}", &community_id[..8.min(community_id.len())], e);
544 return;
545 }
546 }
547
548 let Ok(Some(current)) = crate::db::community::load_community_v2(id) else {
550 return;
551 };
552 if let Ok(Some(_)) = super::service::follow_control(&transport, ¤t, session).await {
553 if !session.is_valid() {
554 return;
555 }
556 refresh_subscription(&client).await;
557 handler.on_community_refreshed(&community_id);
558 enqueue_follow(id);
564 }
565
566 let Ok(Some(current)) = crate::db::community::load_community_v2(id) else {
572 return;
573 };
574 if let Ok(fresh) = super::service::sync_guestbook(&transport, ¤t, session).await {
575 if fresh.is_empty() || !session.is_valid() {
576 return;
577 }
578 surface_presence(¤t, &fresh, handler);
579 handler.on_community_refreshed(&community_id);
580 }
581}
582
583fn surface_presence(
587 community: &CommunityV2,
588 fresh: &[super::guestbook::GuestbookEvent],
589 handler: &dyn InboundEventHandler,
590) {
591 use super::guestbook::GuestbookEntry;
592 use nostr_sdk::prelude::ToBech32;
593 let Some(primary) = community.primary_channel() else {
594 return;
595 };
596 let chat_id = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
597 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
598 let banned = crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default();
599 for ev in fresh {
600 let (member, joined, at_ms, invited_by) = match &ev.entry {
601 GuestbookEntry::Join { member, at_ms, invited_by } => (member, true, *at_ms, invited_by.clone()),
602 GuestbookEntry::Leave { member, at_ms } => (member, false, *at_ms, None),
603 GuestbookEntry::Kick { .. } | GuestbookEntry::Snapshot { .. } => continue,
604 };
605 if banned.contains(&member.to_hex()) {
606 continue;
607 }
608 let Ok(npub) = member.to_bech32() else { continue };
609 let event_id = crate::simd::hex::bytes_to_hex_32(&ev.rumor_id);
610 let (by, label) = match &invited_by {
611 Some((c, l)) => (Some(c.as_str()), Some(l.as_str())),
612 None => (None, None),
613 };
614 handler.on_community_presence(&chat_id, &npub, joined, &event_id, at_ms / 1000, by, label);
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use super::super::control::{genesis, CommunityMetadata};
622 use crate::community::Epoch;
623 use nostr_sdk::prelude::Keys;
624
625 fn a_community(name: &str) -> CommunityV2 {
626 let owner = Keys::generate();
627 let g = genesis(&owner, CommunityMetadata { name: name.into(), ..Default::default() }, 1_000).unwrap();
628 CommunityV2::from_genesis(&g, name, None, vec!["wss://r".into()], 0)
629 }
630
631 #[test]
632 fn plane_authors_covers_the_dispatched_planes_only() {
633 let c = a_community("A");
634 let authors = plane_authors(std::slice::from_ref(&c));
635
636 let gb = derive::guestbook_group_key(&c.community_root, c.id(), c.root_epoch).pk();
639 let control = derive::control_group_key(&c.community_root, c.id(), c.root_epoch).pk();
640 let general = {
641 let (s, e) = c.channel_secret(&c.channels[0]);
642 derive::channel_group_key(&s, &c.channels[0].id, e).pk()
643 };
644 let next_base = derive::base_rekey_group_key(&c.community_root, c.id(), Epoch(1)).pk();
647 let dissolved = derive::dissolved_group_key(c.id()).pk();
649 assert!(
650 authors.contains(&gb)
651 && authors.contains(&control)
652 && authors.contains(&general)
653 && authors.contains(&next_base)
654 && authors.contains(&dissolved)
655 );
656 assert_eq!(authors.len(), 5, "guestbook + control + dissolved + chat + base-rekey planes are subscribed");
657 }
658
659 #[test]
660 fn plane_authors_is_deterministic_deduped_and_multi_community() {
661 let a = a_community("A");
662 let b = a_community("B");
663 let one = plane_authors(std::slice::from_ref(&a));
664 assert_eq!(plane_authors(std::slice::from_ref(&a)), one);
666 let two = plane_authors(&[a.clone(), b.clone()]);
668 assert_eq!(two.len(), one.len() * 2);
669 assert_eq!(plane_authors(&[b, a]), two);
671 }
672
673 #[tokio::test]
674 async fn dispatch_event_routes_a_v2_message_to_the_handler() {
675 use crate::community::transport::memory::MemoryRelay;
676 use crate::community::transport::{Query, Transport};
677 use crate::types::Message;
678 use std::sync::Mutex as StdMutex;
679
680 #[derive(Default)]
681 struct Recorder {
682 got: StdMutex<Vec<(String, String)>>,
683 }
684 impl InboundEventHandler for Recorder {
685 fn on_community_message(&self, chat_id: &str, msg: &Message, _new: bool) {
686 self.got.lock().unwrap().push((chat_id.to_string(), msg.content.clone()));
687 }
688 }
689
690 let _g = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
692 crate::db::close_database();
693 crate::db::clear_id_caches();
694 let tmp = tempfile::tempdir().unwrap();
695 let acct = {
696 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
697 let mut s = String::from("npub1");
698 for i in 0..58 {
699 s.push(B[(i * 5 + 1) % 32] as char);
700 }
701 s
702 };
703 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
704 crate::db::set_app_data_dir(tmp.path().to_path_buf());
705 crate::db::set_current_account(acct.clone()).unwrap();
706 crate::db::init_database(&acct).unwrap();
707 let _ = crate::state::take_nostr_client();
708 let me = Keys::generate();
709 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
710 crate::state::set_my_public_key(me.public_key());
711
712 let relay = MemoryRelay::new();
716 let community = super::super::service::create_community(&relay, "Live", vec!["wss://r".into()], None).await.unwrap();
717 let general = community.channels[0].id;
718 let member = Keys::generate();
719 let group = derive::channel_group_key(&community.community_root, &general, community.root_epoch);
720 let rumor = super::super::chat::build_message_rumor(member.public_key(), &general, community.root_epoch, "live ping", None, &[], vec![], 5_000);
721 let (wrap, _) = super::super::chat::seal_chat_rumor(&rumor, &group, &member, nostr_sdk::prelude::Timestamp::from_secs(5), false).unwrap();
722 let _ = relay.publish(&wrap, &community.relays).await;
723 let q = Query { kinds: vec![stream::KIND_WRAP], authors: vec![group.pk_hex()], ..Default::default() };
724 let wrap = relay.fetch(&q, &community.relays).await.unwrap().into_iter().find(|w| w.pubkey == group.pk()).unwrap();
725
726 let rec = Arc::new(Recorder::default());
732 let session = SessionGuard::capture();
733 crate::community::v2::realtime::clear().await; dispatch_event(&session, wrap.clone(), rec.clone()).await;
735 dispatch_event(&session, wrap, rec.clone()).await;
736
737 let got = rec.got.lock().unwrap();
738 assert_eq!(got.len(), 1, "a re-delivered wrap fires the handler exactly once");
739 assert_eq!(got[0].1, "live ping");
740 assert_eq!(got[0].0, crate::simd::hex::bytes_to_hex_32(&general.0));
741 }
742
743 #[tokio::test]
744 async fn follow_queue_coalesces_a_burst_and_re_enqueues_after_processing() {
745 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<CommunityId>();
748 *V2_FOLLOW_TX.lock().unwrap() = Some(tx);
749 V2_FOLLOW_PENDING.lock().unwrap().clear();
750
751 let id = CommunityId([0x11; 32]);
752 enqueue_follow(&id);
754 enqueue_follow(&id);
755 enqueue_follow(&id);
756 assert_eq!(rx.recv().await, Some(id), "first trigger queues a follow");
757 assert!(rx.try_recv().is_err(), "the burst coalesced to exactly one");
758
759 V2_FOLLOW_PENDING.lock().unwrap().remove(&id.0);
762 enqueue_follow(&id);
763 assert_eq!(rx.recv().await, Some(id), "a trigger after processing re-queues");
764
765 let id2 = CommunityId([0x22; 32]);
767 enqueue_follow(&id2);
768 assert_eq!(rx.recv().await, Some(id2));
769
770 *V2_FOLLOW_TX.lock().unwrap() = None;
771 V2_FOLLOW_PENDING.lock().unwrap().clear();
772 }
773
774 #[tokio::test]
775 async fn a_dissolved_community_honors_no_new_events_and_fires_death_once() {
776 use super::super::service;
777 use crate::community::transport::memory::MemoryRelay;
778 use crate::community::transport::Transport;
779 use crate::types::Message;
780 use std::sync::Mutex as StdMutex;
781
782 #[derive(Default)]
783 struct Recorder {
784 messages: StdMutex<Vec<String>>,
785 deaths: StdMutex<Vec<String>>,
786 }
787 impl InboundEventHandler for Recorder {
788 fn on_community_message(&self, _chat: &str, msg: &Message, _new: bool) {
789 self.messages.lock().unwrap().push(msg.content.clone());
790 }
791 fn on_community_dissolved(&self, community_id: &str) {
792 self.deaths.lock().unwrap().push(community_id.to_string());
793 }
794 }
795
796 let _g = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
797 crate::db::close_database();
798 crate::db::clear_id_caches();
799 let tmp = tempfile::tempdir().unwrap();
800 let acct = {
801 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
802 let mut s = String::from("npub1");
803 for i in 0..58 {
804 s.push(B[(i * 3 + 2) % 32] as char);
805 }
806 s
807 };
808 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
809 crate::db::set_app_data_dir(tmp.path().to_path_buf());
810 crate::db::set_current_account(acct.clone()).unwrap();
811 crate::db::init_database(&acct).unwrap();
812 let _ = crate::state::take_nostr_client();
813 let me = Keys::generate();
814 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
815 crate::state::set_my_public_key(me.public_key());
816
817 let relay = MemoryRelay::new();
818 let community = service::create_community(&relay, "Doomed", vec!["wss://r".into()], None).await.unwrap();
819 let general = community.channels[0].id;
820
821 let rumor = super::super::dissolution::dissolved_tombstone_rumor(me.public_key(), community.id(), 8_000);
826 let tombstone = super::super::dissolution::seal_dissolved(&rumor, community.id(), &me, nostr_sdk::prelude::Timestamp::from_secs(8_000)).unwrap();
827 let _ = relay.publish(&tombstone, &community.relays).await;
828 assert!(!crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&community.id().0)).unwrap(), "not yet locally sealed");
829
830 let rec = Arc::new(Recorder::default());
831 let session = SessionGuard::capture();
832 clear().await;
833 dispatch_event(&session, tombstone.clone(), rec.clone()).await;
836 dispatch_event(&session, tombstone, rec.clone()).await;
837 assert!(crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&community.id().0)).unwrap());
838
839 let member = Keys::generate();
843 let cgroup = derive::channel_group_key(&community.community_root, &general, community.root_epoch);
844 let rumor = super::super::chat::build_message_rumor(member.public_key(), &general, community.root_epoch, "into the grave", None, &[], vec![], 9_000);
845 let (mw, _) = super::super::chat::seal_chat_rumor(&rumor, &cgroup, &member, nostr_sdk::prelude::Timestamp::from_secs(9), false).unwrap();
846 dispatch_event(&session, mw, rec.clone()).await;
847
848 assert_eq!(rec.deaths.lock().unwrap().len(), 1, "death is announced exactly once");
849 assert!(rec.messages.lock().unwrap().is_empty(), "a post-tombstone message is never honored (CORD-02 §9)");
850 }
851
852 #[test]
853 fn a_private_channel_subscribes_to_its_own_chat_plane() {
854 let mut c = a_community("Priv");
855 c.channels.push(super::super::community::ChannelV2 {
856 id: crate::community::ChannelId([0x33; 32]),
857 name: "mods".into(),
858 private: true,
859 key: Some([0x44; 32]),
860 epoch: Epoch(1),
861 voice: None,
862 meta_custom: None,
863 meta_extra: Default::default(),
864 });
865 let authors = plane_authors(std::slice::from_ref(&c));
866 let priv_chat = derive::channel_group_key(&[0x44; 32], &c.channels[1].id, Epoch(1)).pk();
868 assert!(authors.contains(&priv_chat), "a private channel subscribes to its own chat plane");
869 let next_rekey = derive::channel_rekey_group_key(&c.community_root, &c.channels[1].id, Epoch(2)).pk();
872 assert!(authors.contains(&next_rekey), "a private channel's next rekey plane is subscribed");
873 }
874}