use std::collections::{HashMap, HashSet};
use std::sync::{LazyLock, Mutex as StdMutex};
use std::sync::Arc;
use std::time::Duration;
use nostr_sdk::prelude::*;
use tokio::sync::Mutex;
use crate::community::{derive, inbound, roster, service, Channel, CommunityId, Epoch};
use crate::community::transport::LiveTransport;
use crate::event_handler::InboundEventHandler;
use crate::state::SessionGuard;
use crate::stored_event::event_kind;
const CHANNEL_FOLLOW_MAX_ATTEMPTS: usize = 5;
const CHANNEL_FOLLOW_BACKOFF_MS: u64 = 700;
static COMMUNITY_SUB_ID: LazyLock<Mutex<Option<SubscriptionId>>> = LazyLock::new(|| Mutex::new(None));
static COMMUNITY_SUB_SET: LazyLock<Mutex<Vec<String>>> = LazyLock::new(|| Mutex::new(Vec::new()));
static COMMUNITY_POOLWIDE_SUB_ID: LazyLock<Mutex<Option<SubscriptionId>>> = LazyLock::new(|| Mutex::new(None));
static COMMUNITY_ROUTES: LazyLock<Mutex<HashMap<String, Channel>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static CONTROL_ROUTES: LazyLock<Mutex<HashMap<String, String>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static REFRESH_CONTROL_INFLIGHT: LazyLock<StdMutex<HashSet<String>>> =
LazyLock::new(|| StdMutex::new(HashSet::new()));
pub async fn subscription_id() -> Option<SubscriptionId> {
COMMUNITY_SUB_ID.lock().await.clone()
}
pub async fn poolwide_subscription_id() -> Option<SubscriptionId> {
COMMUNITY_POOLWIDE_SUB_ID.lock().await.clone()
}
pub async fn clear() {
*COMMUNITY_SUB_ID.lock().await = None;
*COMMUNITY_POOLWIDE_SUB_ID.lock().await = None;
COMMUNITY_SUB_SET.lock().await.clear();
COMMUNITY_ROUTES.lock().await.clear();
CONTROL_ROUTES.lock().await.clear();
REFRESH_CONTROL_INFLIGHT.lock().unwrap_or_else(|e| e.into_inner()).clear();
}
pub async fn rebuild_routes() -> (Vec<String>, HashSet<String>) {
let mut routes: HashMap<String, Channel> = HashMap::new();
let mut control_routes: HashMap<String, String> = HashMap::new();
let mut pseudonyms: Vec<String> = Vec::new();
let mut relays: HashSet<String> = HashSet::new();
if let Ok(ids) = crate::db::community::list_community_ids() {
for id in ids {
if let Ok(Some(community)) = crate::db::community::load_community(&id) {
for r in &community.relays {
relays.insert(r.clone());
}
for ch in &community.channels {
for (epoch, key) in ch.read_epoch_keys() {
let pseudonym = derive::channel_pseudonym(&key, &ch.id, epoch).to_hex();
pseudonyms.push(pseudonym.clone());
routes.insert(pseudonym, ch.clone());
}
let next_chan = derive::rekey_pseudonym(
&community.server_root_key, &ch.id, Epoch(ch.epoch.0 + 1),
).to_hex();
pseudonyms.push(next_chan.clone());
control_routes.insert(next_chan, community.id.to_hex());
}
let ctrl = roster::control_pseudonym(
&community.server_root_key, &community.id, community.server_root_epoch,
);
pseudonyms.push(ctrl.clone());
control_routes.insert(ctrl, community.id.to_hex());
let next_base = derive::base_rekey_pseudonym(
&community.server_root_key, &community.id,
Epoch(community.server_root_epoch.0 + 1),
).to_hex();
pseudonyms.push(next_base.clone());
control_routes.insert(next_base, community.id.to_hex());
}
}
}
*COMMUNITY_ROUTES.lock().await = routes;
*CONTROL_ROUTES.lock().await = control_routes;
(pseudonyms, relays)
}
pub async fn refresh_subscription(client: &Client) {
let (pseudonyms, relays) = rebuild_routes().await;
let mut new_set = pseudonyms.clone();
new_set.sort();
let mut sub_guard = COMMUNITY_SUB_ID.lock().await;
let mut set_guard = COMMUNITY_SUB_SET.lock().await;
if sub_guard.is_some() && *set_guard == new_set {
return;
}
if let Some(old_id) = sub_guard.take() {
client.unsubscribe(&old_id).await;
}
*set_guard = new_set;
if pseudonyms.is_empty() {
if let Some(old_pw) = COMMUNITY_POOLWIDE_SUB_ID.lock().await.take() {
client.unsubscribe(&old_pw).await;
}
return;
}
for r in &relays {
let _ = client.pool().add_relay(r.as_str(), crate::community_relay_options()).await;
}
client.connect().await;
{
let wanted: Vec<RelayUrl> = relays.iter().filter_map(|r| RelayUrl::parse(r).ok()).collect();
for _ in 0..24 {
let pool = client.pool().all_relays().await;
let any_live = wanted.iter().any(|u| {
pool.get(u).map(|r| r.status() == RelayStatus::Connected).unwrap_or(false)
});
if any_live {
break;
}
tokio::time::sleep(Duration::from_millis(250)).await;
}
}
let filter = Filter::new()
.kinds([
Kind::Custom(event_kind::COMMUNITY_MESSAGE),
Kind::Custom(event_kind::COMMUNITY_REACTION),
Kind::Custom(event_kind::COMMUNITY_EDIT),
Kind::Custom(event_kind::COMMUNITY_DELETE),
Kind::Custom(event_kind::COMMUNITY_PRESENCE),
Kind::Custom(event_kind::COMMUNITY_KICK),
Kind::Custom(event_kind::COMMUNITY_TYPING),
Kind::Custom(event_kind::COMMUNITY_WEBXDC),
Kind::Custom(event_kind::COMMUNITY_CONTROL),
Kind::Custom(event_kind::COMMUNITY_REKEY),
])
.custom_tags(SingleLetterTag::lowercase(Alphabet::Z), pseudonyms)
.limit(0);
{
let mut pw = COMMUNITY_POOLWIDE_SUB_ID.lock().await;
if let Some(old) = pw.take() {
client.unsubscribe(&old).await;
}
if let Ok(out) = client.subscribe(filter.clone(), None).await {
*pw = Some(out.val);
}
}
if let Ok(output) = client.subscribe_to(relays.iter().cloned(), filter, None).await {
*sub_guard = Some(output.val);
}
}
pub async fn dispatch_event(
session: &SessionGuard,
event: Event,
handler: Arc<dyn InboundEventHandler>,
) {
let Some(my_pk) = crate::my_public_key() else { return; };
let Some(pseudonym) = event.tags.iter().find_map(|t| {
let s = t.as_slice();
(s.len() >= 2 && s[0] == "z").then(|| s[1].clone())
}) else { return; };
let kind = event.kind.as_u16();
if kind == event_kind::COMMUNITY_CONTROL || kind == event_kind::COMMUNITY_REKEY {
let community_id = CONTROL_ROUTES.lock().await.get(&pseudonym).cloned();
if let Some(community_id) = community_id {
if session.is_valid() {
tokio::spawn(refresh_control(community_id, handler.clone()));
}
}
return;
}
let Some(channel) = COMMUNITY_ROUTES.lock().await.get(&pseudonym).cloned() else {
return;
};
if !session.is_valid() {
return;
}
let outcome = {
let mut state = crate::state::STATE.lock().await;
inbound::process_incoming(&mut state, &event, &channel, &my_pk)
};
let chat_id = channel.id.to_hex();
match outcome {
Some(inbound::IncomingEvent::NewMessage(mut msg)) => {
if !msg.replied_to.is_empty() {
let _ = crate::db::events::populate_reply_context(&mut msg).await;
}
let _ = crate::db::events::save_message(&chat_id, &msg).await;
handler.on_community_message(&chat_id, &msg, true);
}
Some(inbound::IncomingEvent::Updated { target_id, message, edit_event }) => {
if let Some(ev) = edit_event {
let mut ev = (*ev).clone();
if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(&chat_id) {
ev.chat_id = cid;
}
let _ = crate::db::events::save_event(&ev).await;
} else {
let _ = crate::db::events::save_message(&chat_id, &message).await;
}
handler.on_community_update(&chat_id, &target_id, &message);
}
Some(inbound::IncomingEvent::Removed { target_id }) => {
let _ = crate::db::events::delete_event(&target_id).await;
handler.on_community_removed(&chat_id, &target_id);
}
Some(inbound::IncomingEvent::ReactionRemoved { message_id, reaction_id, message }) => {
let _ = crate::db::events::delete_event(&reaction_id).await;
handler.on_community_update(&chat_id, &message_id, &message);
}
Some(inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label }) => {
handler.on_community_presence(
&chat_id, &npub, joined, &event_id, created_at,
invited_by.as_deref(), invited_label.as_deref(),
);
}
Some(inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at }) => {
handler.on_community_webxdc(
&chat_id, &npub, &topic_id, node_addr.as_deref(), &event_id, created_at,
);
}
Some(inbound::IncomingEvent::Typing { npub, until }) => {
handler.on_community_typing(&chat_id, &npub, until);
}
Some(inbound::IncomingEvent::Kicked { community_id })
| Some(inbound::IncomingEvent::SelfLeft { community_id }) => {
handler.on_community_self_removed(&community_id);
}
None => {}
}
}
pub async fn refresh_control(community_id: String, handler: Arc<dyn InboundEventHandler>) {
{
let mut inflight = REFRESH_CONTROL_INFLIGHT.lock().unwrap_or_else(|e| e.into_inner());
if !inflight.insert(community_id.clone()) {
return;
}
}
struct RefreshClaim(String);
impl Drop for RefreshClaim {
fn drop(&mut self) {
REFRESH_CONTROL_INFLIGHT.lock().unwrap_or_else(|e| e.into_inner()).remove(&self.0);
}
}
let _claim = RefreshClaim(community_id.clone());
let session = SessionGuard::capture();
let Some(id_bytes) = hex_to_id32(&community_id) else { return; };
let Some(community) = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten() else { return; };
let bt = LiveTransport::with_timeout(Duration::from_secs(20));
let pre_server_epoch = community.server_root_epoch.0;
let pre_channel_epochs: Vec<(String, u64)> =
community.channels.iter().map(|c| (c.id.to_hex(), c.epoch.0)).collect();
if let Ok(c) = service::catch_up_server_root(&bt, &community).await {
if !session.is_valid() { return; }
if c.removed { handler.on_community_self_removed(&community_id); return; }
}
if !session.is_valid() { return; }
let community = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten().unwrap_or(community);
let _ = service::fetch_and_apply_control(&bt, &community).await;
if !session.is_valid() { return; }
if let Some(c) = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten() {
if service::am_i_banned(&c) {
handler.on_community_self_removed(&community_id);
return;
}
}
if !session.is_valid() { return; }
let base_delta = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten()
.map(|c| c.server_root_epoch.0).unwrap_or(pre_server_epoch).saturating_sub(pre_server_epoch);
for attempt in 0..CHANNEL_FOLLOW_MAX_ATTEMPTS {
if !session.is_valid() { return; }
let Some(cur) = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten() else { break; };
for ch in &cur.channels {
let _ = service::catch_up_channel_rekeys(&bt, &cur, &ch.id).await;
}
let caught = base_delta == 0 || crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten()
.map(|c| c.channels.iter().all(|ch| {
let pre = pre_channel_epochs.iter().find(|(id, _)| id == &ch.id.to_hex()).map(|(_, e)| *e).unwrap_or(ch.epoch.0);
ch.epoch.0 >= pre.saturating_add(base_delta)
}))
.unwrap_or(true);
if caught { break; }
if attempt + 1 < CHANNEL_FOLLOW_MAX_ATTEMPTS {
tokio::time::sleep(Duration::from_millis(CHANNEL_FOLLOW_BACKOFF_MS)).await;
}
}
if !session.is_valid() { return; }
let community = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten().unwrap_or(community);
let _ = service::retry_pending_read_cut(&bt, &community).await;
if !session.is_valid() { return; }
let community = crate::db::community::load_community(&CommunityId(id_bytes)).ok().flatten().unwrap_or(community);
let advanced = community.server_root_epoch.0 != pre_server_epoch
|| community.channels.iter().any(|c| {
pre_channel_epochs.iter().find(|(id, _)| id == &c.id.to_hex()).map(|(_, e)| *e != c.epoch.0).unwrap_or(true)
});
if advanced && session.is_valid() {
if let Some(client) = crate::state::nostr_client() {
refresh_subscription(&client).await;
}
} else {
let _ = rebuild_routes().await;
}
if !session.is_valid() { return; }
crate::community::list::refresh_membership_current(&community);
handler.on_community_refreshed(&community_id);
}
pub async fn teardown_local(community_id: &str) {
let _ = crate::db::community::delete_community_retain_keys(community_id);
if let Some(client) = crate::state::nostr_client() {
refresh_subscription(&client).await;
}
}
fn hex_to_id32(hex: &str) -> Option<[u8; 32]> {
(hex.len() == 64).then(|| crate::simd::hex::hex_to_bytes_32(hex))
}