1use std::collections::{HashMap, HashSet};
10use std::sync::{LazyLock, Mutex as StdMutex};
11use std::sync::Arc;
12use std::time::Duration;
13
14use nostr_sdk::prelude::*;
15use tokio::sync::Mutex;
16
17use crate::community::{derive, inbound, roster, service, Channel, CommunityId, Epoch};
18use crate::community::transport::LiveTransport;
19use crate::event_handler::InboundEventHandler;
20use crate::state::SessionGuard;
21use crate::stored_event::event_kind;
22
23const CHANNEL_FOLLOW_MAX_ATTEMPTS: usize = 5;
26const CHANNEL_FOLLOW_BACKOFF_MS: u64 = 700;
27
28static COMMUNITY_SUB_ID: LazyLock<Mutex<Option<SubscriptionId>>> = LazyLock::new(|| Mutex::new(None));
31
32static COMMUNITY_ROUTES: LazyLock<Mutex<HashMap<String, Channel>>> =
35 LazyLock::new(|| Mutex::new(HashMap::new()));
36
37static CONTROL_ROUTES: LazyLock<Mutex<HashMap<String, String>>> =
40 LazyLock::new(|| Mutex::new(HashMap::new()));
41
42static REFRESH_CONTROL_INFLIGHT: LazyLock<StdMutex<HashSet<String>>> =
44 LazyLock::new(|| StdMutex::new(HashSet::new()));
45
46pub async fn subscription_id() -> Option<SubscriptionId> {
49 COMMUNITY_SUB_ID.lock().await.clone()
50}
51
52pub async fn clear() {
55 *COMMUNITY_SUB_ID.lock().await = None;
56 COMMUNITY_ROUTES.lock().await.clear();
57 CONTROL_ROUTES.lock().await.clear();
58 REFRESH_CONTROL_INFLIGHT.lock().unwrap_or_else(|e| e.into_inner()).clear();
59}
60
61pub async fn rebuild_routes() -> (Vec<String>, HashSet<String>) {
66 let mut routes: HashMap<String, Channel> = HashMap::new();
67 let mut control_routes: HashMap<String, String> = HashMap::new();
68 let mut pseudonyms: Vec<String> = Vec::new();
69 let mut relays: HashSet<String> = HashSet::new();
70
71 if let Ok(ids) = crate::db::community::list_community_ids() {
72 for id in ids {
73 if let Ok(Some(community)) = crate::db::community::load_community(&id) {
74 for r in &community.relays {
75 relays.insert(r.clone());
76 }
77 for ch in &community.channels {
78 for (epoch, key) in ch.read_epoch_keys() {
82 let pseudonym = derive::channel_pseudonym(&key, &ch.id, epoch).to_hex();
83 pseudonyms.push(pseudonym.clone());
84 routes.insert(pseudonym, ch.clone());
85 }
86 let next_chan = derive::rekey_pseudonym(
89 &community.server_root_key, &ch.id, Epoch(ch.epoch.0 + 1),
90 ).to_hex();
91 pseudonyms.push(next_chan.clone());
92 control_routes.insert(next_chan, community.id.to_hex());
93 }
94 let ctrl = roster::control_pseudonym(
96 &community.server_root_key, &community.id, community.server_root_epoch,
97 );
98 pseudonyms.push(ctrl.clone());
99 control_routes.insert(ctrl, community.id.to_hex());
100 let next_base = derive::base_rekey_pseudonym(
102 &community.server_root_key, &community.id,
103 Epoch(community.server_root_epoch.0 + 1),
104 ).to_hex();
105 pseudonyms.push(next_base.clone());
106 control_routes.insert(next_base, community.id.to_hex());
107 }
108 }
109 }
110
111 *COMMUNITY_ROUTES.lock().await = routes;
112 *CONTROL_ROUTES.lock().await = control_routes;
113 (pseudonyms, relays)
114}
115
116pub async fn refresh_subscription(client: &Client) {
119 let (pseudonyms, relays) = rebuild_routes().await;
120
121 let mut sub_guard = COMMUNITY_SUB_ID.lock().await;
125 if let Some(old_id) = sub_guard.take() {
126 client.unsubscribe(&old_id).await;
127 }
128
129 if pseudonyms.is_empty() {
130 return;
131 }
132
133 for r in &relays {
136 let _ = client.pool().add_relay(r.as_str(), crate::community_relay_options()).await;
137 }
138 client.connect().await;
139
140 let filter = Filter::new()
141 .kinds([
142 Kind::Custom(event_kind::COMMUNITY_MESSAGE),
143 Kind::Custom(event_kind::COMMUNITY_REACTION),
144 Kind::Custom(event_kind::COMMUNITY_EDIT),
145 Kind::Custom(event_kind::COMMUNITY_DELETE),
146 Kind::Custom(event_kind::COMMUNITY_PRESENCE),
147 Kind::Custom(event_kind::COMMUNITY_KICK),
148 Kind::Custom(event_kind::COMMUNITY_WEBXDC),
149 Kind::Custom(event_kind::COMMUNITY_CONTROL),
150 Kind::Custom(event_kind::COMMUNITY_REKEY),
151 ])
152 .custom_tags(SingleLetterTag::lowercase(Alphabet::Z), pseudonyms)
153 .limit(0);
154
155 match client.subscribe_to(relays.iter().cloned(), filter, None).await {
156 Ok(output) => *sub_guard = Some(output.val),
157 Err(e) => crate::log_warn!("[community] subscribe failed: {:?}", e),
158 }
159}
160
161pub async fn dispatch_event(
166 session: &SessionGuard,
167 event: Event,
168 handler: Arc<dyn InboundEventHandler>,
169) {
170 let Some(my_pk) = crate::my_public_key() else { return; };
171 let Some(pseudonym) = event.tags.iter().find_map(|t| {
172 let s = t.as_slice();
173 (s.len() >= 2 && s[0] == "z").then(|| s[1].clone())
174 }) else { return; };
175
176 let kind = event.kind.as_u16();
178 if kind == event_kind::COMMUNITY_CONTROL || kind == event_kind::COMMUNITY_REKEY {
179 let community_id = CONTROL_ROUTES.lock().await.get(&pseudonym).cloned();
180 if let Some(community_id) = community_id {
181 if session.is_valid() {
182 tokio::spawn(refresh_control(community_id, handler.clone()));
185 }
186 }
187 return;
188 }
189
190 let Some(channel) = COMMUNITY_ROUTES.lock().await.get(&pseudonym).cloned() else { return; };
191 if !session.is_valid() {
192 return;
193 }
194
195 let outcome = {
196 let mut state = crate::state::STATE.lock().await;
197 inbound::process_incoming(&mut state, &event, &channel, &my_pk)
198 };
199 let chat_id = channel.id.to_hex();
200 match outcome {
201 Some(inbound::IncomingEvent::NewMessage(msg)) => {
202 let _ = crate::db::events::save_message(&chat_id, &msg).await;
203 handler.on_community_message(&chat_id, &msg, true);
204 }
205 Some(inbound::IncomingEvent::Updated { target_id, message, edit_event }) => {
206 if let Some(ev) = edit_event {
208 let mut ev = (*ev).clone();
209 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(&chat_id) {
210 ev.chat_id = cid;
211 }
212 let _ = crate::db::events::save_event(&ev).await;
213 } else {
214 let _ = crate::db::events::save_message(&chat_id, &message).await;
215 }
216 handler.on_community_update(&chat_id, &target_id, &message);
217 }
218 Some(inbound::IncomingEvent::Removed { target_id }) => {
219 let _ = crate::db::events::delete_event(&target_id).await;
220 handler.on_community_removed(&chat_id, &target_id);
221 }
222 Some(inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label }) => {
223 handler.on_community_presence(
224 &chat_id, &npub, joined, &event_id, created_at,
225 invited_by.as_deref(), invited_label.as_deref(),
226 );
227 }
228 Some(inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at }) => {
229 handler.on_community_webxdc(
230 &chat_id, &npub, &topic_id, node_addr.as_deref(), &event_id, created_at,
231 );
232 }
233 Some(inbound::IncomingEvent::Typing { npub, until }) => {
234 handler.on_community_typing(&chat_id, &npub, until);
235 }
236 Some(inbound::IncomingEvent::Kicked { community_id })
237 | Some(inbound::IncomingEvent::SelfLeft { community_id }) => {
238 handler.on_community_self_removed(&community_id);
241 }
242 None => {}
243 }
244}
245
246pub async fn refresh_control(community_id: String, handler: Arc<dyn InboundEventHandler>) {
251 {
253 let mut inflight = REFRESH_CONTROL_INFLIGHT.lock().unwrap_or_else(|e| e.into_inner());
254 if !inflight.insert(community_id.clone()) {
255 return;
256 }
257 }
258 struct RefreshClaim(String);
259 impl Drop for RefreshClaim {
260 fn drop(&mut self) {
261 REFRESH_CONTROL_INFLIGHT.lock().unwrap_or_else(|e| e.into_inner()).remove(&self.0);
262 }
263 }
264 let _claim = RefreshClaim(community_id.clone());
265
266 let session = SessionGuard::capture();
267 let Some(id_bytes) = hex_to_id32(&community_id) else { return; };
268 let Some(community) = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten() else { return; };
269 let bt = LiveTransport::with_timeout(Duration::from_secs(20));
270 let pre_server_epoch = community.server_root_epoch.0;
271 let pre_channel_epochs: Vec<(String, u64)> =
272 community.channels.iter().map(|c| (c.id.to_hex(), c.epoch.0)).collect();
273
274 if let Ok(c) = service::catch_up_server_root(&bt, &community).await {
278 if !session.is_valid() { return; }
279 if c.removed { handler.on_community_self_removed(&community_id); return; }
280 }
281 if !session.is_valid() { return; }
282 let community = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten().unwrap_or(community);
283 let _ = service::fetch_and_apply_control(&bt, &community).await;
284 if !session.is_valid() { return; }
285 if let Some(c) = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten() {
287 if service::am_i_banned(&c) {
288 handler.on_community_self_removed(&community_id);
289 return;
290 }
291 }
292 if !session.is_valid() { return; }
293
294 let base_delta = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten()
298 .map(|c| c.server_root_epoch.0).unwrap_or(pre_server_epoch).saturating_sub(pre_server_epoch);
299 for attempt in 0..CHANNEL_FOLLOW_MAX_ATTEMPTS {
300 if !session.is_valid() { return; }
301 let Some(cur) = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten() else { break; };
302 for ch in &cur.channels {
303 let _ = service::catch_up_channel_rekeys(&bt, &cur, &ch.id).await;
304 }
305 let caught = base_delta == 0 || crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten()
306 .map(|c| c.channels.iter().all(|ch| {
307 let pre = pre_channel_epochs.iter().find(|(id, _)| id == &ch.id.to_hex()).map(|(_, e)| *e).unwrap_or(ch.epoch.0);
308 ch.epoch.0 >= pre.saturating_add(base_delta)
309 }))
310 .unwrap_or(true);
311 if caught { break; }
312 if attempt + 1 < CHANNEL_FOLLOW_MAX_ATTEMPTS {
313 tokio::time::sleep(Duration::from_millis(CHANNEL_FOLLOW_BACKOFF_MS)).await;
314 }
315 }
316 if !session.is_valid() { return; }
317 let community = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten().unwrap_or(community);
318 let _ = service::retry_pending_read_cut(&bt, &community).await;
319 if !session.is_valid() { return; }
320 let community = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten().unwrap_or(community);
321
322 let advanced = community.server_root_epoch.0 != pre_server_epoch
325 || community.channels.iter().any(|c| {
326 pre_channel_epochs.iter().find(|(id, _)| id == &c.id.to_hex()).map(|(_, e)| *e != c.epoch.0).unwrap_or(true)
327 });
328 if advanced && session.is_valid() {
329 if let Some(client) = crate::state::nostr_client() {
330 refresh_subscription(&client).await;
331 }
332 } else {
333 let _ = rebuild_routes().await;
334 }
335 if !session.is_valid() { return; }
336 crate::community::list::refresh_membership_current(&community);
337 handler.on_community_refreshed(&community_id);
338}
339
340pub async fn teardown_local(community_id: &str) {
345 let _ = crate::db::community::delete_community_retain_keys(community_id);
346 if let Some(client) = crate::state::nostr_client() {
347 refresh_subscription(&client).await;
348 }
349}
350
351fn hex_to_id32(hex: &str) -> Option<[u8; 32]> {
353 (hex.len() == 64).then(|| crate::simd::hex::hex_to_bytes_32(hex))
354}