Skip to main content

hashtree_cli/socialgraph/
mod.rs

1pub mod access;
2pub mod crawler;
3pub mod local_lists;
4pub mod snapshot;
5
6pub use access::SocialGraphAccessControl;
7pub use crawler::SocialGraphCrawler;
8pub use local_lists::{
9    read_local_list_file_state, sync_local_list_files_force, sync_local_list_files_if_changed,
10    LocalListFileState, LocalListSyncOutcome,
11};
12
13mod index_buckets;
14
15use index_buckets::{
16    dedupe_events, latest_metadata_events_by_pubkey, EventIndexBucket, ProfileIndexBucket,
17};
18
19use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
20use std::path::{Path, PathBuf};
21use std::sync::{Arc, Mutex as StdMutex};
22
23use anyhow::{Context, Result};
24use bytes::Bytes;
25use futures::executor::block_on;
26use hashtree_core::{nhash_encode_full, BufferedStore, Cid, HashTree, HashTreeConfig, NHashData};
27use hashtree_index::BTree;
28use hashtree_nostr::{
29    is_parameterized_replaceable_kind, is_replaceable_kind, stored_event_from_nostr_sdk_event,
30    ListEventsOptions, NostrEventStore, NostrEventStoreError, ProfileGuard as NostrProfileGuard,
31    StoredNostrEvent,
32};
33#[cfg(test)]
34use hashtree_nostr::{
35    reset_profile as reset_nostr_profile, set_profile_enabled as set_nostr_profile_enabled,
36    take_profile as take_nostr_profile,
37};
38use heed::EnvFlags;
39use nostr::{Event, Filter, JsonUtil, Kind, SingleLetterTag};
40use nostr_social_graph::{
41    BinaryBudget, GraphStats, NostrEvent as GraphEvent, SocialGraph,
42    SocialGraphBackend as NostrSocialGraphBackend,
43};
44use nostr_social_graph_heed::HeedSocialGraph;
45
46use crate::managed_env::ManagedEnv;
47use crate::storage::{LocalStore, StorageRouter};
48
49#[cfg(test)]
50use std::sync::OnceLock;
51#[cfg(test)]
52use std::time::Instant;
53
54pub type UserSet = BTreeSet<[u8; 32]>;
55
56const DEFAULT_ROOT_HEX: &str = "0000000000000000000000000000000000000000000000000000000000000000";
57const EVENTS_ROOT_FILE: &str = "events-root.msgpack";
58const AMBIENT_EVENTS_ROOT_FILE: &str = "events-root-ambient.msgpack";
59const AMBIENT_EVENTS_BLOB_DIR: &str = "ambient-blobs";
60const PROFILE_SEARCH_ROOT_FILE: &str = "profile-search-root.msgpack";
61const PROFILES_BY_PUBKEY_ROOT_FILE: &str = "profiles-by-pubkey-root.msgpack";
62const UNKNOWN_FOLLOW_DISTANCE: u32 = 1000;
63const DEFAULT_SOCIALGRAPH_MAP_SIZE_BYTES: u64 = 4 * 1024 * 1024 * 1024;
64const MIN_SOCIALGRAPH_MAP_SIZE_BYTES: u64 = 64 * 1024 * 1024;
65const SOCIALGRAPH_MAX_DBS: u32 = 16;
66const PROFILE_SEARCH_INDEX_ORDER: usize = 64;
67const PROFILE_SEARCH_PREFIX: &str = "p:";
68const PROFILE_NAME_MAX_LENGTH: usize = 100;
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum EventStorageClass {
72    Public,
73    Ambient,
74}
75
76#[cfg_attr(not(test), allow(dead_code))]
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub(crate) enum EventQueryScope {
79    PublicOnly,
80    AmbientOnly,
81    All,
82}
83
84#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
85struct StoredCid {
86    hash: [u8; 32],
87    key: Option<[u8; 32]>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
91pub struct StoredProfileSearchEntry {
92    pub pubkey: String,
93    pub name: String,
94    #[serde(default)]
95    pub aliases: Vec<String>,
96    #[serde(default)]
97    pub nip05: Option<String>,
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub follow_distance: Option<u32>,
100    pub created_at: u64,
101    pub event_nhash: String,
102}
103
104#[derive(Debug, Clone, Default, serde::Serialize)]
105pub struct SocialGraphStats {
106    pub total_users: usize,
107    pub root: Option<String>,
108    pub total_follows: usize,
109    pub max_depth: u32,
110    pub size_by_distance: BTreeMap<u32, usize>,
111    pub enabled: bool,
112}
113
114#[derive(Debug, Clone)]
115struct DistanceCache {
116    stats: SocialGraphStats,
117    users_by_distance: BTreeMap<u32, Vec<[u8; 32]>>,
118}
119
120#[derive(Debug, thiserror::Error)]
121#[error("{0}")]
122pub struct UpstreamGraphBackendError(String);
123
124pub struct SocialGraphStore {
125    graph: StdMutex<HeedSocialGraph>,
126    // `HeedSocialGraph` owns a raw Heed handle. Declaring it before this
127    // managed clone makes Rust drop the graph first, then close Heed's cache.
128    _graph_env_lifecycle: ManagedEnv,
129    distance_cache: StdMutex<Option<DistanceCache>>,
130    public_events: EventIndexBucket,
131    ambient_events: EventIndexBucket,
132    profile_index: ProfileIndexBucket,
133    profile_index_overmute_threshold: StdMutex<f64>,
134}
135
136pub trait SocialGraphBackend: Send + Sync {
137    fn stats(&self) -> Result<SocialGraphStats>;
138    fn users_by_follow_distance(&self, distance: u32) -> Result<Vec<[u8; 32]>>;
139    fn follow_distance(&self, pk_bytes: &[u8; 32]) -> Result<Option<u32>>;
140    fn follow_list_created_at(&self, owner: &[u8; 32]) -> Result<Option<u64>>;
141    fn followed_targets(&self, owner: &[u8; 32]) -> Result<UserSet>;
142    fn is_overmuted_user(&self, user_pk: &[u8; 32], threshold: f64) -> Result<bool>;
143    fn profile_search_root(&self) -> Result<Option<Cid>> {
144        Ok(None)
145    }
146    fn snapshot_chunks(&self, root: &[u8; 32], options: &BinaryBudget) -> Result<Vec<Bytes>>;
147    fn ingest_event(&self, event: &Event) -> Result<()>;
148    fn ingest_event_with_storage_class(
149        &self,
150        event: &Event,
151        storage_class: EventStorageClass,
152    ) -> Result<()> {
153        let _ = storage_class;
154        self.ingest_event(event)
155    }
156    fn ingest_events(&self, events: &[Event]) -> Result<()> {
157        for event in events {
158            self.ingest_event(event)?;
159        }
160        Ok(())
161    }
162    fn ingest_events_with_storage_class(
163        &self,
164        events: &[Event],
165        storage_class: EventStorageClass,
166    ) -> Result<()> {
167        for event in events {
168            self.ingest_event_with_storage_class(event, storage_class)?;
169        }
170        Ok(())
171    }
172    fn ingest_graph_events(&self, events: &[Event]) -> Result<()> {
173        self.ingest_events(events)
174    }
175    fn query_events(&self, filter: &Filter, limit: usize) -> Result<Vec<Event>>;
176}
177
178#[cfg(test)]
179pub type TestLockGuard = tokio::sync::MutexGuard<'static, ()>;
180
181#[cfg(test)]
182static NDB_TEST_LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
183
184#[cfg(test)]
185fn test_mutex() -> &'static tokio::sync::Mutex<()> {
186    NDB_TEST_LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
187}
188
189#[cfg(test)]
190pub async fn test_lock() -> TestLockGuard {
191    test_mutex().lock().await
192}
193
194#[cfg(test)]
195pub fn test_lock_blocking() -> TestLockGuard {
196    test_mutex().blocking_lock()
197}
198
199pub fn open_social_graph_store(data_dir: &Path) -> Result<Arc<SocialGraphStore>> {
200    open_social_graph_store_with_mapsize(data_dir, None)
201}
202
203pub fn open_social_graph_store_with_mapsize(
204    data_dir: &Path,
205    mapsize_bytes: Option<u64>,
206) -> Result<Arc<SocialGraphStore>> {
207    let db_dir = data_dir.join("socialgraph");
208    open_social_graph_store_at_path(&db_dir, mapsize_bytes)
209}
210
211pub fn open_social_graph_store_with_storage(
212    data_dir: &Path,
213    store: Arc<StorageRouter>,
214    mapsize_bytes: Option<u64>,
215) -> Result<Arc<SocialGraphStore>> {
216    let db_dir = data_dir.join("socialgraph");
217    open_social_graph_store_at_path_with_storage(&db_dir, store, mapsize_bytes)
218}
219
220#[cfg(test)]
221pub fn open_test_social_graph_store(data_dir: &Path) -> Result<Arc<SocialGraphStore>> {
222    open_test_social_graph_store_with_mapsize(data_dir, None)
223}
224
225#[cfg(test)]
226pub fn open_test_social_graph_store_with_mapsize(
227    data_dir: &Path,
228    mapsize_bytes: Option<u64>,
229) -> Result<Arc<SocialGraphStore>> {
230    open_test_social_graph_store_at_path(&data_dir.join("socialgraph"), mapsize_bytes)
231}
232
233#[cfg(test)]
234pub fn open_test_social_graph_store_with_storage(
235    data_dir: &Path,
236    store: Arc<StorageRouter>,
237    mapsize_bytes: Option<u64>,
238) -> Result<Arc<SocialGraphStore>> {
239    open_embedded_social_graph_store_with_storage(data_dir, store, mapsize_bytes)
240}
241
242#[cfg(test)]
243pub fn open_test_social_graph_store_at_path(
244    db_dir: &Path,
245    mapsize_bytes: Option<u64>,
246) -> Result<Arc<SocialGraphStore>> {
247    open_embedded_social_graph_store_at_path(db_dir, mapsize_bytes)
248}
249
250pub fn open_embedded_social_graph_store_with_storage(
251    data_dir: &Path,
252    store: Arc<StorageRouter>,
253    mapsize_bytes: Option<u64>,
254) -> Result<Arc<SocialGraphStore>> {
255    let db_dir = data_dir.join("socialgraph");
256    open_social_graph_store_at_path_with_storage_and_env_flags(
257        &db_dir,
258        store,
259        mapsize_bytes,
260        EnvFlags::NO_LOCK,
261    )
262}
263
264pub fn open_social_graph_store_at_path(
265    db_dir: &Path,
266    mapsize_bytes: Option<u64>,
267) -> Result<Arc<SocialGraphStore>> {
268    let config = hashtree_config::Config::load_or_default();
269    let backend = &config.storage.backend;
270    let local_store = Arc::new(
271        LocalStore::new_with_lmdb_map_size(db_dir.join("blobs"), backend, mapsize_bytes)
272            .map_err(|err| anyhow::anyhow!("Failed to create social graph blob store: {err}"))?,
273    );
274    let store = Arc::new(StorageRouter::new(local_store));
275    open_social_graph_store_at_path_with_storage(db_dir, store, mapsize_bytes)
276}
277
278pub fn open_embedded_social_graph_store_at_path(
279    db_dir: &Path,
280    mapsize_bytes: Option<u64>,
281) -> Result<Arc<SocialGraphStore>> {
282    let local_store = Arc::new(
283        LocalStore::new_with_lmdb_map_size(
284            db_dir.join("blobs"),
285            &hashtree_config::StorageBackend::Fs,
286            mapsize_bytes,
287        )
288        .map_err(|err| anyhow::anyhow!("Failed to create social graph blob store: {err}"))?,
289    );
290    let store = Arc::new(StorageRouter::new(local_store));
291    open_social_graph_store_at_path_with_storage_and_env_flags(
292        db_dir,
293        store,
294        mapsize_bytes,
295        EnvFlags::NO_LOCK,
296    )
297}
298
299pub fn open_social_graph_store_at_path_with_storage(
300    db_dir: &Path,
301    store: Arc<StorageRouter>,
302    mapsize_bytes: Option<u64>,
303) -> Result<Arc<SocialGraphStore>> {
304    open_social_graph_store_at_path_with_storage_and_env_flags(
305        db_dir,
306        store,
307        mapsize_bytes,
308        EnvFlags::empty(),
309    )
310}
311
312fn open_social_graph_store_at_path_with_storage_and_env_flags(
313    db_dir: &Path,
314    store: Arc<StorageRouter>,
315    mapsize_bytes: Option<u64>,
316    env_flags: EnvFlags,
317) -> Result<Arc<SocialGraphStore>> {
318    let ambient_backend = store.local_store().backend();
319    let ambient_local = Arc::new(
320        LocalStore::new_with_lmdb_map_size(
321            db_dir.join(AMBIENT_EVENTS_BLOB_DIR),
322            &ambient_backend,
323            mapsize_bytes,
324        )
325        .map_err(|err| {
326            anyhow::anyhow!("Failed to create social graph ambient blob store: {err}")
327        })?,
328    );
329    let ambient_store = Arc::new(StorageRouter::new(ambient_local));
330    open_social_graph_store_at_path_with_storage_split_and_env_flags(
331        db_dir,
332        store,
333        ambient_store,
334        mapsize_bytes,
335        env_flags,
336    )
337}
338
339pub fn open_social_graph_store_at_path_with_storage_split(
340    db_dir: &Path,
341    public_store: Arc<StorageRouter>,
342    ambient_store: Arc<StorageRouter>,
343    mapsize_bytes: Option<u64>,
344) -> Result<Arc<SocialGraphStore>> {
345    open_social_graph_store_at_path_with_storage_split_and_env_flags(
346        db_dir,
347        public_store,
348        ambient_store,
349        mapsize_bytes,
350        EnvFlags::empty(),
351    )
352}
353
354fn open_social_graph_store_at_path_with_storage_split_and_env_flags(
355    db_dir: &Path,
356    public_store: Arc<StorageRouter>,
357    ambient_store: Arc<StorageRouter>,
358    mapsize_bytes: Option<u64>,
359    env_flags: EnvFlags,
360) -> Result<Arc<SocialGraphStore>> {
361    std::fs::create_dir_all(db_dir)?;
362    if let Some(size) = mapsize_bytes {
363        ensure_social_graph_mapsize_with_env_flags(db_dir, size, env_flags)?;
364    }
365    let graph_map_size = social_graph_map_size(mapsize_bytes)?;
366    let graph = unsafe {
367        HeedSocialGraph::open_with_env_flags_and_map_size(
368            db_dir,
369            DEFAULT_ROOT_HEX,
370            env_flags,
371            graph_map_size,
372        )
373    }
374    .context("open nostr-social-graph heed backend")?;
375    let mut lifecycle_options = heed::EnvOpenOptions::new();
376    lifecycle_options
377        .map_size(graph_map_size)
378        .max_dbs(SOCIALGRAPH_MAX_DBS);
379    unsafe {
380        lifecycle_options.flags(env_flags);
381    }
382    let graph_env_lifecycle = unsafe { ManagedEnv::open(&lifecycle_options, db_dir) }
383        .context("manage nostr-social-graph heed backend lifecycle")?;
384
385    Ok(Arc::new(SocialGraphStore {
386        graph: StdMutex::new(graph),
387        _graph_env_lifecycle: graph_env_lifecycle,
388        distance_cache: StdMutex::new(None),
389        public_events: EventIndexBucket {
390            event_store: NostrEventStore::new(Arc::clone(&public_store)),
391            root_path: db_dir.join(EVENTS_ROOT_FILE),
392        },
393        ambient_events: EventIndexBucket {
394            event_store: NostrEventStore::new(ambient_store),
395            root_path: db_dir.join(AMBIENT_EVENTS_ROOT_FILE),
396        },
397        profile_index: ProfileIndexBucket {
398            store: Arc::clone(&public_store),
399            tree: HashTree::new(HashTreeConfig::new(Arc::clone(&public_store))),
400            index: BTree::new(
401                public_store,
402                hashtree_index::BTreeOptions {
403                    order: Some(PROFILE_SEARCH_INDEX_ORDER),
404                },
405            ),
406            by_pubkey_root_path: db_dir.join(PROFILES_BY_PUBKEY_ROOT_FILE),
407            search_root_path: db_dir.join(PROFILE_SEARCH_ROOT_FILE),
408        },
409        profile_index_overmute_threshold: StdMutex::new(1.0),
410    }))
411}
412
413pub fn set_social_graph_root(store: &SocialGraphStore, pk_bytes: &[u8; 32]) {
414    if let Err(err) = store.set_root(pk_bytes) {
415        tracing::warn!("Failed to set social graph root: {err}");
416    }
417}
418
419pub fn get_follow_distance(
420    backend: &(impl SocialGraphBackend + ?Sized),
421    pk_bytes: &[u8; 32],
422) -> Option<u32> {
423    backend.follow_distance(pk_bytes).ok().flatten()
424}
425
426pub fn get_follows(
427    backend: &(impl SocialGraphBackend + ?Sized),
428    pk_bytes: &[u8; 32],
429) -> Vec<[u8; 32]> {
430    match backend.followed_targets(pk_bytes) {
431        Ok(set) => set.into_iter().collect(),
432        Err(_) => Vec::new(),
433    }
434}
435
436pub fn is_overmuted(
437    backend: &(impl SocialGraphBackend + ?Sized),
438    _root_pk: &[u8; 32],
439    user_pk: &[u8; 32],
440    threshold: f64,
441) -> bool {
442    backend
443        .is_overmuted_user(user_pk, threshold)
444        .unwrap_or(false)
445}
446
447pub fn ingest_event(backend: &(impl SocialGraphBackend + ?Sized), _sub_id: &str, event_json: &str) {
448    let event = match Event::from_json(event_json) {
449        Ok(event) => event,
450        Err(_) => return,
451    };
452
453    if let Err(err) = backend.ingest_event(&event) {
454        tracing::warn!("Failed to ingest social graph event: {err}");
455    }
456}
457
458pub fn ingest_parsed_event(
459    backend: &(impl SocialGraphBackend + ?Sized),
460    event: &Event,
461) -> Result<()> {
462    backend.ingest_event(event)
463}
464
465pub fn ingest_parsed_event_with_storage_class(
466    backend: &(impl SocialGraphBackend + ?Sized),
467    event: &Event,
468    storage_class: EventStorageClass,
469) -> Result<()> {
470    backend.ingest_event_with_storage_class(event, storage_class)
471}
472
473pub fn ingest_parsed_events(
474    backend: &(impl SocialGraphBackend + ?Sized),
475    events: &[Event],
476) -> Result<()> {
477    backend.ingest_events(events)
478}
479
480pub fn ingest_parsed_events_with_storage_class(
481    backend: &(impl SocialGraphBackend + ?Sized),
482    events: &[Event],
483    storage_class: EventStorageClass,
484) -> Result<()> {
485    backend.ingest_events_with_storage_class(events, storage_class)
486}
487
488pub fn ingest_graph_parsed_events(
489    backend: &(impl SocialGraphBackend + ?Sized),
490    events: &[Event],
491) -> Result<()> {
492    backend.ingest_graph_events(events)
493}
494
495pub fn query_events(
496    backend: &(impl SocialGraphBackend + ?Sized),
497    filter: &Filter,
498    limit: usize,
499) -> Vec<Event> {
500    backend.query_events(filter, limit).unwrap_or_default()
501}
502
503impl SocialGraphStore {
504    pub fn set_profile_index_overmute_threshold(&self, threshold: f64) {
505        *self
506            .profile_index_overmute_threshold
507            .lock()
508            .expect("profile index overmute threshold") = threshold;
509    }
510
511    fn profile_index_overmute_threshold(&self) -> f64 {
512        *self
513            .profile_index_overmute_threshold
514            .lock()
515            .expect("profile index overmute threshold")
516    }
517
518    fn invalidate_distance_cache(&self) {
519        *self.distance_cache.lock().unwrap() = None;
520    }
521
522    fn build_distance_cache(state: nostr_social_graph::SocialGraphState) -> Result<DistanceCache> {
523        let unique_ids = state
524            .unique_ids
525            .into_iter()
526            .map(|(pubkey, id)| decode_pubkey(&pubkey).map(|decoded| (id, decoded)))
527            .collect::<Result<HashMap<_, _>>>()?;
528
529        let mut users_by_distance = BTreeMap::new();
530        let mut size_by_distance = BTreeMap::new();
531        for (distance, users) in state.users_by_follow_distance {
532            let decoded = users
533                .into_iter()
534                .filter_map(|id| unique_ids.get(&id).copied())
535                .collect::<Vec<_>>();
536            size_by_distance.insert(distance, decoded.len());
537            users_by_distance.insert(distance, decoded);
538        }
539
540        let total_follows = state
541            .followed_by_user
542            .iter()
543            .map(|(_, targets)| targets.len())
544            .sum::<usize>();
545        let total_users = size_by_distance.values().copied().sum();
546        let max_depth = size_by_distance.keys().copied().max().unwrap_or_default();
547
548        Ok(DistanceCache {
549            stats: SocialGraphStats {
550                total_users,
551                root: Some(state.root),
552                total_follows,
553                max_depth,
554                size_by_distance,
555                enabled: true,
556            },
557            users_by_distance,
558        })
559    }
560
561    fn load_distance_cache(&self) -> Result<DistanceCache> {
562        if let Some(cache) = self.distance_cache.lock().unwrap().clone() {
563            return Ok(cache);
564        }
565
566        let state = {
567            let graph = self.graph.lock().unwrap();
568            graph.export_state().context("export social graph state")?
569        };
570        let cache = Self::build_distance_cache(state)?;
571        *self.distance_cache.lock().unwrap() = Some(cache.clone());
572        Ok(cache)
573    }
574
575    fn set_root(&self, root: &[u8; 32]) -> Result<()> {
576        let root_hex = hex::encode(root);
577        {
578            let mut graph = self.graph.lock().unwrap();
579            if should_replace_placeholder_root(&graph)? {
580                let fresh = SocialGraph::new(&root_hex);
581                graph
582                    .replace_state(&fresh.export_state())
583                    .context("replace placeholder social graph root")?;
584            } else {
585                graph
586                    .set_root(&root_hex)
587                    .context("set nostr-social-graph root")?;
588            }
589        }
590        self.invalidate_distance_cache();
591        Ok(())
592    }
593
594    fn stats(&self) -> Result<SocialGraphStats> {
595        Ok(self.load_distance_cache()?.stats)
596    }
597
598    fn follow_distance(&self, pk_bytes: &[u8; 32]) -> Result<Option<u32>> {
599        let graph = self.graph.lock().unwrap();
600        let distance = graph
601            .get_follow_distance(&hex::encode(pk_bytes))
602            .context("read social graph follow distance")?;
603        Ok((distance != UNKNOWN_FOLLOW_DISTANCE).then_some(distance))
604    }
605
606    fn users_by_follow_distance(&self, distance: u32) -> Result<Vec<[u8; 32]>> {
607        Ok(self
608            .load_distance_cache()?
609            .users_by_distance
610            .get(&distance)
611            .cloned()
612            .unwrap_or_default())
613    }
614
615    fn follow_list_created_at(&self, owner: &[u8; 32]) -> Result<Option<u64>> {
616        let graph = self.graph.lock().unwrap();
617        graph
618            .get_follow_list_created_at(&hex::encode(owner))
619            .context("read social graph follow list timestamp")
620    }
621
622    fn followed_targets(&self, owner: &[u8; 32]) -> Result<UserSet> {
623        let graph = self.graph.lock().unwrap();
624        decode_pubkey_set(
625            graph
626                .get_followed_by_user(&hex::encode(owner))
627                .context("read followed targets")?,
628        )
629    }
630
631    fn is_overmuted_user(&self, user_pk: &[u8; 32], threshold: f64) -> Result<bool> {
632        if threshold <= 0.0 {
633            return Ok(false);
634        }
635        let graph = self.graph.lock().unwrap();
636        graph
637            .is_overmuted(&hex::encode(user_pk), threshold)
638            .context("check social graph overmute")
639    }
640
641    #[cfg_attr(not(test), allow(dead_code))]
642    pub fn profile_search_root(&self) -> Result<Option<Cid>> {
643        self.profile_index.search_root()
644    }
645
646    #[cfg_attr(not(test), allow(dead_code))]
647    pub fn profiles_by_pubkey_root(&self) -> Result<Option<Cid>> {
648        self.profile_index.by_pubkey_root()
649    }
650
651    pub fn public_events_root(&self) -> Result<Option<Cid>> {
652        self.public_events.events_root()
653    }
654
655    #[cfg_attr(test, allow(dead_code))]
656    pub(crate) fn public_events_root_for_write(&self) -> Result<Option<Cid>> {
657        self.public_events.events_root_for_write()
658    }
659
660    pub(crate) fn write_public_events_root(&self, root: Option<&Cid>) -> Result<()> {
661        self.public_events.write_events_root(root)
662    }
663
664    #[cfg_attr(not(test), allow(dead_code))]
665    pub fn latest_profile_event(&self, pubkey_hex: &str) -> Result<Option<Event>> {
666        self.profile_index.profile_event_for_pubkey(pubkey_hex)
667    }
668
669    #[cfg_attr(not(test), allow(dead_code))]
670    pub fn profile_search_entries_for_prefix(
671        &self,
672        prefix: &str,
673    ) -> Result<Vec<(String, StoredProfileSearchEntry)>> {
674        self.profile_index.search_entries_for_prefix(prefix)
675    }
676
677    pub fn sync_profile_index_for_events(&self, events: &[Event]) -> Result<()> {
678        self.update_profile_index_for_events(events)
679    }
680
681    pub(crate) fn rebuild_profile_index_for_events(&self, events: &[Event]) -> Result<()> {
682        let latest_by_pubkey = self.filtered_latest_metadata_events_by_pubkey(events)?;
683        let (by_pubkey_root, search_root) = self
684            .profile_index
685            .rebuild_profile_events_with_distances(latest_by_pubkey.into_values(), |event| {
686                self.follow_distance(&event.pubkey.to_bytes())
687            })?;
688        self.profile_index
689            .write_by_pubkey_root(by_pubkey_root.as_ref())?;
690        self.profile_index.write_search_root(search_root.as_ref())?;
691        Ok(())
692    }
693
694    pub(crate) async fn rebuild_profile_index_for_events_async(
695        &self,
696        events: &[Event],
697    ) -> Result<()> {
698        let latest_by_pubkey = self.filtered_latest_metadata_events_by_pubkey(events)?;
699        let (by_pubkey_root, search_root) = self
700            .profile_index
701            .rebuild_profile_events_async_with_distances(latest_by_pubkey.into_values(), |event| {
702                self.follow_distance(&event.pubkey.to_bytes())
703            })
704            .await?;
705        self.profile_index
706            .write_by_pubkey_root(by_pubkey_root.as_ref())?;
707        self.profile_index.write_search_root(search_root.as_ref())?;
708        Ok(())
709    }
710
711    pub fn rebuild_profile_index_from_stored_events(&self) -> Result<usize> {
712        let public_events_root = self.public_events.events_root()?;
713        let ambient_events_root = self.ambient_events.events_root()?;
714        if public_events_root.is_none() && ambient_events_root.is_none() {
715            self.profile_index.write_by_pubkey_root(None)?;
716            self.profile_index.write_search_root(None)?;
717            return Ok(0);
718        }
719
720        let mut events = Vec::new();
721        for (bucket, root) in [
722            (&self.public_events, public_events_root),
723            (&self.ambient_events, ambient_events_root),
724        ] {
725            let Some(root) = root else {
726                continue;
727            };
728            let stored = block_on(bucket.event_store.list_by_kind_lossy(
729                Some(&root),
730                Kind::Metadata.as_u16() as u32,
731                ListEventsOptions::default(),
732            ))
733            .map_err(map_event_store_error)?;
734            events.extend(
735                stored
736                    .into_iter()
737                    .map(stored_event_to_nostr_event)
738                    .collect::<Result<Vec<_>>>()?,
739            );
740        }
741
742        let latest_count = self
743            .filtered_latest_metadata_events_by_pubkey(&events)?
744            .len();
745        self.rebuild_profile_index_for_events(&events)?;
746        Ok(latest_count)
747    }
748
749    pub async fn rebuild_profile_index_from_stored_events_async(&self) -> Result<usize> {
750        let public_events_root = self.public_events.events_root()?;
751        let ambient_events_root = self.ambient_events.events_root()?;
752        if public_events_root.is_none() && ambient_events_root.is_none() {
753            self.profile_index.write_by_pubkey_root(None)?;
754            self.profile_index.write_search_root(None)?;
755            return Ok(0);
756        }
757
758        let mut events = Vec::new();
759        for (bucket, root) in [
760            (&self.public_events, public_events_root),
761            (&self.ambient_events, ambient_events_root),
762        ] {
763            let Some(root) = root else {
764                continue;
765            };
766            let stored = bucket
767                .event_store
768                .list_by_kind_lossy(
769                    Some(&root),
770                    Kind::Metadata.as_u16() as u32,
771                    ListEventsOptions::default(),
772                )
773                .await
774                .map_err(map_event_store_error)?;
775            events.extend(
776                stored
777                    .into_iter()
778                    .map(stored_event_to_nostr_event)
779                    .collect::<Result<Vec<_>>>()?,
780            );
781        }
782
783        let latest_count = self
784            .filtered_latest_metadata_events_by_pubkey(&events)?
785            .len();
786        self.rebuild_profile_index_for_events_async(&events).await?;
787        Ok(latest_count)
788    }
789
790    pub fn rebuild_event_indexes_from_stored_events(&self) -> Result<(usize, usize)> {
791        let public_count =
792            self.rebuild_event_index_bucket_from_stored_events(&self.public_events)?;
793        let ambient_count =
794            self.rebuild_event_index_bucket_from_stored_events(&self.ambient_events)?;
795        self.rebuild_profile_index_from_stored_events()?;
796        Ok((public_count, ambient_count))
797    }
798
799    pub async fn rebuild_event_indexes_from_stored_events_async(&self) -> Result<(usize, usize)> {
800        let public_count = self
801            .rebuild_event_index_bucket_from_stored_events_async(&self.public_events)
802            .await?;
803        let ambient_count = self
804            .rebuild_event_index_bucket_from_stored_events_async(&self.ambient_events)
805            .await?;
806        self.rebuild_profile_index_from_stored_events_async()
807            .await?;
808        Ok((public_count, ambient_count))
809    }
810
811    fn rebuild_event_index_bucket_from_stored_events(
812        &self,
813        bucket: &EventIndexBucket,
814    ) -> Result<usize> {
815        let Some(root) = bucket.events_root()? else {
816            bucket.write_events_root(None)?;
817            return Ok(0);
818        };
819
820        let manifest = match block_on(bucket.event_store.get_manifest(Some(&root))) {
821            Ok(manifest) => manifest,
822            Err(err) => {
823                tracing::warn!(
824                    "Clearing invalid social graph event index root {} before rebuild: {}",
825                    hex::encode(root.hash),
826                    err
827                );
828                bucket.write_events_root(None)?;
829                return Ok(0);
830            }
831        };
832        if manifest.by_kind_time_author.is_none() {
833            let next_root = block_on(bucket.event_store.upgrade_manifest_indexes(Some(&root)))
834                .map_err(map_event_store_error)?;
835            if next_root.as_ref() != Some(&root) {
836                bucket.write_events_root(next_root.as_ref())?;
837                return Ok(0);
838            }
839        }
840
841        let stored = block_on(
842            bucket
843                .event_store
844                .list_recent_lossy(Some(&root), ListEventsOptions::default()),
845        )
846        .map_err(map_event_store_error)?;
847        let count = stored.len();
848        let next_root =
849            block_on(bucket.event_store.build(None, stored)).map_err(map_event_store_error)?;
850        bucket.write_events_root(next_root.as_ref())?;
851        Ok(count)
852    }
853
854    async fn rebuild_event_index_bucket_from_stored_events_async(
855        &self,
856        bucket: &EventIndexBucket,
857    ) -> Result<usize> {
858        let Some(root) = bucket.events_root()? else {
859            bucket.write_events_root(None)?;
860            return Ok(0);
861        };
862
863        let manifest = match bucket.event_store.get_manifest(Some(&root)).await {
864            Ok(manifest) => manifest,
865            Err(err) => {
866                tracing::warn!(
867                    "Clearing invalid social graph event index root {} before rebuild: {}",
868                    hex::encode(root.hash),
869                    err
870                );
871                bucket.write_events_root(None)?;
872                return Ok(0);
873            }
874        };
875        if manifest.by_kind_time_author.is_none() {
876            let next_root = bucket
877                .event_store
878                .upgrade_manifest_indexes(Some(&root))
879                .await
880                .map_err(map_event_store_error)?;
881            if next_root.as_ref() != Some(&root) {
882                bucket.write_events_root(next_root.as_ref())?;
883                return Ok(0);
884            }
885        }
886
887        let stored = bucket
888            .event_store
889            .list_recent_lossy(Some(&root), ListEventsOptions::default())
890            .await
891            .map_err(map_event_store_error)?;
892        let count = stored.len();
893        let next_root = bucket
894            .event_store
895            .build(None, stored)
896            .await
897            .map_err(map_event_store_error)?;
898        bucket.write_events_root(next_root.as_ref())?;
899        Ok(count)
900    }
901
902    fn update_profile_index_for_events(&self, events: &[Event]) -> Result<()> {
903        let latest_by_pubkey = latest_metadata_events_by_pubkey(events);
904        let threshold = self.profile_index_overmute_threshold();
905
906        if latest_by_pubkey.is_empty() {
907            return Ok(());
908        }
909
910        let mut updates = Vec::with_capacity(latest_by_pubkey.len());
911        for event in latest_by_pubkey.into_values() {
912            let overmuted = self.is_overmuted_user(&event.pubkey.to_bytes(), threshold)?;
913            let follow_distance = if overmuted {
914                None
915            } else {
916                self.follow_distance(&event.pubkey.to_bytes())?
917            };
918            updates.push((event, follow_distance, overmuted));
919        }
920
921        let by_pubkey_root = self.profile_index.by_pubkey_root()?;
922        let search_root = self.profile_index.search_root()?;
923        let (by_pubkey_root, search_root, changed) = self.profile_index.update_profile_events(
924            by_pubkey_root.as_ref(),
925            search_root.as_ref(),
926            &updates,
927        )?;
928        if changed {
929            self.profile_index
930                .write_by_pubkey_root(by_pubkey_root.as_ref())?;
931            self.profile_index.write_search_root(search_root.as_ref())?;
932        }
933
934        Ok(())
935    }
936
937    fn filtered_latest_metadata_events_by_pubkey<'a>(
938        &self,
939        events: &'a [Event],
940    ) -> Result<BTreeMap<String, &'a Event>> {
941        let threshold = self.profile_index_overmute_threshold();
942        let mut latest_by_pubkey = BTreeMap::<String, &Event>::new();
943        for event in events.iter().filter(|event| event.kind == Kind::Metadata) {
944            if self.is_overmuted_user(&event.pubkey.to_bytes(), threshold)? {
945                continue;
946            }
947            let pubkey = event.pubkey.to_hex();
948            match latest_by_pubkey.get(&pubkey) {
949                Some(current) if compare_nostr_events(event, current).is_le() => {}
950                _ => {
951                    latest_by_pubkey.insert(pubkey, event);
952                }
953            }
954        }
955        Ok(latest_by_pubkey)
956    }
957
958    fn snapshot_chunks(&self, root: &[u8; 32], options: &BinaryBudget) -> Result<Vec<Bytes>> {
959        let state = {
960            let graph = self.graph.lock().unwrap();
961            graph.export_state().context("export social graph state")?
962        };
963        let mut graph = SocialGraph::from_state(state).context("rebuild social graph state")?;
964        let root_hex = hex::encode(root);
965        if graph.get_root() != root_hex {
966            graph
967                .set_root(&root_hex)
968                .context("set snapshot social graph root")?;
969        }
970        let chunks = graph
971            .to_binary_chunks_with_budget(*options)
972            .context("encode social graph snapshot")?;
973        Ok(chunks.into_iter().map(Bytes::from).collect())
974    }
975
976    fn ingest_event(&self, event: &Event) -> Result<()> {
977        self.ingest_event_with_storage_class(event, self.default_storage_class_for(event)?)
978    }
979
980    fn ingest_events(&self, events: &[Event]) -> Result<()> {
981        if events.is_empty() {
982            return Ok(());
983        }
984
985        let mut public = Vec::new();
986        let mut ambient = Vec::new();
987        for event in events {
988            match self.default_storage_class_for(event)? {
989                EventStorageClass::Public => public.push(event.clone()),
990                EventStorageClass::Ambient => ambient.push(event.clone()),
991            }
992        }
993
994        if !public.is_empty() {
995            self.ingest_events_with_storage_class(&public, EventStorageClass::Public)?;
996        }
997        if !ambient.is_empty() {
998            self.ingest_events_with_storage_class(&ambient, EventStorageClass::Ambient)?;
999        }
1000
1001        Ok(())
1002    }
1003
1004    fn apply_graph_events_only(&self, events: &[Event]) -> Result<()> {
1005        let graph_events = events
1006            .iter()
1007            .filter(|event| is_social_graph_event(event.kind))
1008            .collect::<Vec<_>>();
1009        if graph_events.is_empty() {
1010            return Ok(());
1011        }
1012
1013        {
1014            let mut graph = self.graph.lock().unwrap();
1015            let mut snapshot = SocialGraph::from_state(
1016                graph
1017                    .export_state()
1018                    .context("export social graph state for graph-only ingest")?,
1019            )
1020            .context("rebuild social graph state for graph-only ingest")?;
1021            for event in graph_events {
1022                snapshot.handle_event(&graph_event_from_nostr(event), true, 0.0);
1023            }
1024            graph
1025                .replace_state(&snapshot.export_state())
1026                .context("replace graph-only social graph state")?;
1027        }
1028        self.invalidate_distance_cache();
1029        Ok(())
1030    }
1031
1032    fn query_events(&self, filter: &Filter, limit: usize) -> Result<Vec<Event>> {
1033        self.query_events_in_scope(filter, limit, EventQueryScope::All)
1034    }
1035
1036    fn default_storage_class_for(&self, event: &Event) -> Result<EventStorageClass> {
1037        let graph = self.graph.lock().unwrap();
1038        let root_hex = graph.get_root().context("read social graph root")?;
1039        if root_hex != DEFAULT_ROOT_HEX && root_hex == event.pubkey.to_hex() {
1040            return Ok(EventStorageClass::Public);
1041        }
1042        Ok(EventStorageClass::Ambient)
1043    }
1044
1045    fn bucket(&self, storage_class: EventStorageClass) -> &EventIndexBucket {
1046        match storage_class {
1047            EventStorageClass::Public => &self.public_events,
1048            EventStorageClass::Ambient => &self.ambient_events,
1049        }
1050    }
1051
1052    fn ingest_event_with_storage_class(
1053        &self,
1054        event: &Event,
1055        storage_class: EventStorageClass,
1056    ) -> Result<()> {
1057        let current_root = self.bucket(storage_class).events_root_for_write()?;
1058        let next_root = self
1059            .bucket(storage_class)
1060            .store_event(current_root.as_ref(), event)?;
1061        self.bucket(storage_class)
1062            .write_events_root(Some(&next_root))?;
1063
1064        if is_social_graph_event(event.kind) {
1065            {
1066                let mut graph = self.graph.lock().unwrap();
1067                graph
1068                    .handle_event(&graph_event_from_nostr(event), true, 0.0)
1069                    .context("ingest social graph event into nostr-social-graph")?;
1070            }
1071            self.invalidate_distance_cache();
1072        }
1073
1074        self.update_profile_index_for_events(std::slice::from_ref(event))?;
1075
1076        Ok(())
1077    }
1078
1079    fn ingest_events_with_storage_class(
1080        &self,
1081        events: &[Event],
1082        storage_class: EventStorageClass,
1083    ) -> Result<()> {
1084        if events.is_empty() {
1085            return Ok(());
1086        }
1087
1088        let bucket = self.bucket(storage_class);
1089        let current_root = bucket.events_root_for_write()?;
1090        let stored_events = events
1091            .iter()
1092            .map(stored_event_from_nostr_sdk_event)
1093            .collect::<Vec<_>>();
1094        let next_root = block_on(
1095            bucket
1096                .event_store
1097                .build(current_root.as_ref(), stored_events),
1098        )
1099        .map_err(map_event_store_error)?;
1100        bucket.write_events_root(next_root.as_ref())?;
1101
1102        let graph_events = events
1103            .iter()
1104            .filter(|event| is_social_graph_event(event.kind))
1105            .collect::<Vec<_>>();
1106        if !graph_events.is_empty() {
1107            let mut graph = self.graph.lock().unwrap();
1108            let mut snapshot = SocialGraph::from_state(
1109                graph
1110                    .export_state()
1111                    .context("export social graph state for batch ingest")?,
1112            )
1113            .context("rebuild social graph state for batch ingest")?;
1114            for event in graph_events {
1115                snapshot.handle_event(&graph_event_from_nostr(event), true, 0.0);
1116            }
1117            graph
1118                .replace_state(&snapshot.export_state())
1119                .context("replace batched social graph state")?;
1120            self.invalidate_distance_cache();
1121        }
1122
1123        self.update_profile_index_for_events(events)?;
1124
1125        Ok(())
1126    }
1127
1128    pub(crate) fn query_events_in_scope(
1129        &self,
1130        filter: &Filter,
1131        limit: usize,
1132        scope: EventQueryScope,
1133    ) -> Result<Vec<Event>> {
1134        if limit == 0 {
1135            return Ok(Vec::new());
1136        }
1137
1138        let buckets: &[&EventIndexBucket] = match scope {
1139            EventQueryScope::PublicOnly => &[&self.public_events],
1140            EventQueryScope::AmbientOnly => &[&self.ambient_events],
1141            EventQueryScope::All => &[&self.public_events, &self.ambient_events],
1142        };
1143
1144        let mut candidates = Vec::new();
1145        for bucket in buckets {
1146            candidates.extend(bucket.query_events(filter, limit)?);
1147        }
1148
1149        let mut deduped = dedupe_events(candidates);
1150        deduped.retain(|event| filter.match_event(event, Default::default()));
1151        deduped.truncate(limit);
1152        Ok(deduped)
1153    }
1154}
1155
1156impl SocialGraphBackend for SocialGraphStore {
1157    fn stats(&self) -> Result<SocialGraphStats> {
1158        SocialGraphStore::stats(self)
1159    }
1160
1161    fn users_by_follow_distance(&self, distance: u32) -> Result<Vec<[u8; 32]>> {
1162        SocialGraphStore::users_by_follow_distance(self, distance)
1163    }
1164
1165    fn follow_distance(&self, pk_bytes: &[u8; 32]) -> Result<Option<u32>> {
1166        SocialGraphStore::follow_distance(self, pk_bytes)
1167    }
1168
1169    fn follow_list_created_at(&self, owner: &[u8; 32]) -> Result<Option<u64>> {
1170        SocialGraphStore::follow_list_created_at(self, owner)
1171    }
1172
1173    fn followed_targets(&self, owner: &[u8; 32]) -> Result<UserSet> {
1174        SocialGraphStore::followed_targets(self, owner)
1175    }
1176
1177    fn is_overmuted_user(&self, user_pk: &[u8; 32], threshold: f64) -> Result<bool> {
1178        SocialGraphStore::is_overmuted_user(self, user_pk, threshold)
1179    }
1180
1181    fn profile_search_root(&self) -> Result<Option<Cid>> {
1182        SocialGraphStore::profile_search_root(self)
1183    }
1184
1185    fn snapshot_chunks(&self, root: &[u8; 32], options: &BinaryBudget) -> Result<Vec<Bytes>> {
1186        SocialGraphStore::snapshot_chunks(self, root, options)
1187    }
1188
1189    fn ingest_event(&self, event: &Event) -> Result<()> {
1190        SocialGraphStore::ingest_event(self, event)
1191    }
1192
1193    fn ingest_event_with_storage_class(
1194        &self,
1195        event: &Event,
1196        storage_class: EventStorageClass,
1197    ) -> Result<()> {
1198        SocialGraphStore::ingest_event_with_storage_class(self, event, storage_class)
1199    }
1200
1201    fn ingest_events(&self, events: &[Event]) -> Result<()> {
1202        SocialGraphStore::ingest_events(self, events)
1203    }
1204
1205    fn ingest_events_with_storage_class(
1206        &self,
1207        events: &[Event],
1208        storage_class: EventStorageClass,
1209    ) -> Result<()> {
1210        SocialGraphStore::ingest_events_with_storage_class(self, events, storage_class)
1211    }
1212
1213    fn ingest_graph_events(&self, events: &[Event]) -> Result<()> {
1214        SocialGraphStore::apply_graph_events_only(self, events)
1215    }
1216
1217    fn query_events(&self, filter: &Filter, limit: usize) -> Result<Vec<Event>> {
1218        SocialGraphStore::query_events(self, filter, limit)
1219    }
1220}
1221
1222impl NostrSocialGraphBackend for SocialGraphStore {
1223    type Error = UpstreamGraphBackendError;
1224
1225    fn get_root(&self) -> std::result::Result<String, Self::Error> {
1226        let graph = self.graph.lock().unwrap();
1227        graph
1228            .get_root()
1229            .context("read social graph root")
1230            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1231    }
1232
1233    fn set_root(&mut self, root: &str) -> std::result::Result<(), Self::Error> {
1234        let root_bytes =
1235            decode_pubkey(root).map_err(|err| UpstreamGraphBackendError(err.to_string()))?;
1236        SocialGraphStore::set_root(self, &root_bytes)
1237            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1238    }
1239
1240    fn handle_event(
1241        &mut self,
1242        event: &GraphEvent,
1243        allow_unknown_authors: bool,
1244        overmute_threshold: f64,
1245    ) -> std::result::Result<(), Self::Error> {
1246        {
1247            let mut graph = self.graph.lock().unwrap();
1248            graph
1249                .handle_event(event, allow_unknown_authors, overmute_threshold)
1250                .context("ingest social graph event into heed backend")
1251                .map_err(|err| UpstreamGraphBackendError(err.to_string()))?;
1252        }
1253        self.invalidate_distance_cache();
1254        Ok(())
1255    }
1256
1257    fn get_follow_distance(&self, user: &str) -> std::result::Result<u32, Self::Error> {
1258        let graph = self.graph.lock().unwrap();
1259        graph
1260            .get_follow_distance(user)
1261            .context("read social graph follow distance")
1262            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1263    }
1264
1265    fn is_following(
1266        &self,
1267        follower: &str,
1268        followed_user: &str,
1269    ) -> std::result::Result<bool, Self::Error> {
1270        let graph = self.graph.lock().unwrap();
1271        graph
1272            .is_following(follower, followed_user)
1273            .context("read social graph following edge")
1274            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1275    }
1276
1277    fn get_followed_by_user(&self, user: &str) -> std::result::Result<Vec<String>, Self::Error> {
1278        let graph = self.graph.lock().unwrap();
1279        graph
1280            .get_followed_by_user(user)
1281            .context("read followed-by-user list")
1282            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1283    }
1284
1285    fn get_followers_by_user(&self, user: &str) -> std::result::Result<Vec<String>, Self::Error> {
1286        let graph = self.graph.lock().unwrap();
1287        graph
1288            .get_followers_by_user(user)
1289            .context("read followers-by-user list")
1290            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1291    }
1292
1293    fn get_muted_by_user(&self, user: &str) -> std::result::Result<Vec<String>, Self::Error> {
1294        let graph = self.graph.lock().unwrap();
1295        graph
1296            .get_muted_by_user(user)
1297            .context("read muted-by-user list")
1298            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1299    }
1300
1301    fn get_user_muted_by(&self, user: &str) -> std::result::Result<Vec<String>, Self::Error> {
1302        let graph = self.graph.lock().unwrap();
1303        graph
1304            .get_user_muted_by(user)
1305            .context("read user-muted-by list")
1306            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1307    }
1308
1309    fn get_follow_list_created_at(
1310        &self,
1311        user: &str,
1312    ) -> std::result::Result<Option<u64>, Self::Error> {
1313        let graph = self.graph.lock().unwrap();
1314        graph
1315            .get_follow_list_created_at(user)
1316            .context("read social graph follow list timestamp")
1317            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1318    }
1319
1320    fn get_mute_list_created_at(
1321        &self,
1322        user: &str,
1323    ) -> std::result::Result<Option<u64>, Self::Error> {
1324        let graph = self.graph.lock().unwrap();
1325        graph
1326            .get_mute_list_created_at(user)
1327            .context("read social graph mute list timestamp")
1328            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1329    }
1330
1331    fn is_overmuted(&self, user: &str, threshold: f64) -> std::result::Result<bool, Self::Error> {
1332        let graph = self.graph.lock().unwrap();
1333        graph
1334            .is_overmuted(user, threshold)
1335            .context("check social graph overmute")
1336            .map_err(|err| UpstreamGraphBackendError(err.to_string()))
1337    }
1338}
1339
1340impl<T> SocialGraphBackend for Arc<T>
1341where
1342    T: SocialGraphBackend + ?Sized,
1343{
1344    fn stats(&self) -> Result<SocialGraphStats> {
1345        self.as_ref().stats()
1346    }
1347
1348    fn users_by_follow_distance(&self, distance: u32) -> Result<Vec<[u8; 32]>> {
1349        self.as_ref().users_by_follow_distance(distance)
1350    }
1351
1352    fn follow_distance(&self, pk_bytes: &[u8; 32]) -> Result<Option<u32>> {
1353        self.as_ref().follow_distance(pk_bytes)
1354    }
1355
1356    fn follow_list_created_at(&self, owner: &[u8; 32]) -> Result<Option<u64>> {
1357        self.as_ref().follow_list_created_at(owner)
1358    }
1359
1360    fn followed_targets(&self, owner: &[u8; 32]) -> Result<UserSet> {
1361        self.as_ref().followed_targets(owner)
1362    }
1363
1364    fn is_overmuted_user(&self, user_pk: &[u8; 32], threshold: f64) -> Result<bool> {
1365        self.as_ref().is_overmuted_user(user_pk, threshold)
1366    }
1367
1368    fn profile_search_root(&self) -> Result<Option<Cid>> {
1369        self.as_ref().profile_search_root()
1370    }
1371
1372    fn snapshot_chunks(&self, root: &[u8; 32], options: &BinaryBudget) -> Result<Vec<Bytes>> {
1373        self.as_ref().snapshot_chunks(root, options)
1374    }
1375
1376    fn ingest_event(&self, event: &Event) -> Result<()> {
1377        self.as_ref().ingest_event(event)
1378    }
1379
1380    fn ingest_event_with_storage_class(
1381        &self,
1382        event: &Event,
1383        storage_class: EventStorageClass,
1384    ) -> Result<()> {
1385        self.as_ref()
1386            .ingest_event_with_storage_class(event, storage_class)
1387    }
1388
1389    fn ingest_events(&self, events: &[Event]) -> Result<()> {
1390        self.as_ref().ingest_events(events)
1391    }
1392
1393    fn ingest_events_with_storage_class(
1394        &self,
1395        events: &[Event],
1396        storage_class: EventStorageClass,
1397    ) -> Result<()> {
1398        self.as_ref()
1399            .ingest_events_with_storage_class(events, storage_class)
1400    }
1401
1402    fn ingest_graph_events(&self, events: &[Event]) -> Result<()> {
1403        self.as_ref().ingest_graph_events(events)
1404    }
1405
1406    fn query_events(&self, filter: &Filter, limit: usize) -> Result<Vec<Event>> {
1407        self.as_ref().query_events(filter, limit)
1408    }
1409}
1410
1411fn should_replace_placeholder_root(graph: &HeedSocialGraph) -> Result<bool> {
1412    if graph.get_root().context("read current social graph root")? != DEFAULT_ROOT_HEX {
1413        return Ok(false);
1414    }
1415
1416    let GraphStats {
1417        users,
1418        follows,
1419        mutes,
1420        ..
1421    } = graph.size().context("size social graph")?;
1422    Ok(users <= 1 && follows == 0 && mutes == 0)
1423}
1424
1425fn decode_pubkey_set(values: Vec<String>) -> Result<UserSet> {
1426    let mut set = UserSet::new();
1427    for value in values {
1428        set.insert(decode_pubkey(&value)?);
1429    }
1430    Ok(set)
1431}
1432
1433fn decode_pubkey(value: &str) -> Result<[u8; 32]> {
1434    let mut bytes = [0u8; 32];
1435    hex::decode_to_slice(value, &mut bytes)
1436        .with_context(|| format!("decode social graph pubkey {value}"))?;
1437    Ok(bytes)
1438}
1439
1440fn is_social_graph_event(kind: Kind) -> bool {
1441    kind == Kind::ContactList || kind == Kind::MuteList
1442}
1443
1444fn graph_event_from_nostr(event: &Event) -> GraphEvent {
1445    GraphEvent {
1446        created_at: event.created_at.as_secs(),
1447        content: event.content.clone(),
1448        tags: event
1449            .tags
1450            .iter()
1451            .map(|tag| tag.as_slice().to_vec())
1452            .collect(),
1453        kind: event.kind.as_u16() as u32,
1454        pubkey: event.pubkey.to_hex(),
1455        id: event.id.to_hex(),
1456        sig: event.sig.to_string(),
1457    }
1458}
1459
1460pub(crate) fn stored_event_to_nostr_event(event: StoredNostrEvent) -> Result<Event> {
1461    Ok(event.to_nostr_sdk_event()?)
1462}
1463
1464fn encode_cid(cid: &Cid) -> Result<Vec<u8>> {
1465    rmp_serde::to_vec_named(&StoredCid {
1466        hash: cid.hash,
1467        key: cid.key,
1468    })
1469    .context("encode social graph events root")
1470}
1471
1472fn decode_cid(bytes: &[u8]) -> Result<Option<Cid>> {
1473    let stored: StoredCid =
1474        rmp_serde::from_slice(bytes).context("decode social graph events root")?;
1475    Ok(Some(Cid {
1476        hash: stored.hash,
1477        key: stored.key,
1478    }))
1479}
1480
1481fn read_root_file(path: &Path) -> Result<Option<Cid>> {
1482    let Ok(bytes) = std::fs::read(path) else {
1483        return Ok(None);
1484    };
1485    decode_cid(&bytes)
1486}
1487
1488fn write_root_file(path: &Path, root: Option<&Cid>) -> Result<()> {
1489    let Some(root) = root else {
1490        if path.exists() {
1491            std::fs::remove_file(path)?;
1492        }
1493        return Ok(());
1494    };
1495
1496    let encoded = encode_cid(root)?;
1497    let tmp_path = path.with_extension("tmp");
1498    std::fs::write(&tmp_path, encoded)?;
1499    std::fs::rename(tmp_path, path)?;
1500    Ok(())
1501}
1502
1503fn normalize_profile_name(value: &serde_json::Value) -> Option<String> {
1504    let raw = value.as_str()?;
1505    let trimmed = raw.split_whitespace().collect::<Vec<_>>().join(" ");
1506    if trimmed.is_empty() {
1507        return None;
1508    }
1509    Some(trimmed.chars().take(PROFILE_NAME_MAX_LENGTH).collect())
1510}
1511
1512fn extract_profile_names(profile: &serde_json::Map<String, serde_json::Value>) -> Vec<String> {
1513    let mut names = Vec::new();
1514    let mut seen = HashSet::new();
1515
1516    for key in ["display_name", "displayName", "name", "username"] {
1517        let Some(value) = profile.get(key).and_then(normalize_profile_name) else {
1518            continue;
1519        };
1520        let lowered = value.to_lowercase();
1521        if seen.insert(lowered) {
1522            names.push(value);
1523        }
1524    }
1525
1526    names
1527}
1528
1529fn should_reject_profile_nip05(local_part: &str, primary_name: &str) -> bool {
1530    if local_part.len() == 1 || local_part.starts_with("npub1") {
1531        return true;
1532    }
1533
1534    primary_name
1535        .to_lowercase()
1536        .split_whitespace()
1537        .collect::<String>()
1538        .contains(local_part)
1539}
1540
1541fn normalize_profile_nip05(
1542    profile: &serde_json::Map<String, serde_json::Value>,
1543    primary_name: Option<&str>,
1544) -> Option<String> {
1545    let raw = profile.get("nip05")?.as_str()?;
1546    let local_part = raw.split('@').next()?.trim().to_lowercase();
1547    if local_part.is_empty() {
1548        return None;
1549    }
1550    let truncated: String = local_part.chars().take(PROFILE_NAME_MAX_LENGTH).collect();
1551    if truncated.is_empty() {
1552        return None;
1553    }
1554    if primary_name.is_some_and(|name| should_reject_profile_nip05(&truncated, name)) {
1555        return None;
1556    }
1557    Some(truncated)
1558}
1559
1560fn is_search_stop_word(word: &str) -> bool {
1561    matches!(
1562        word,
1563        "a" | "an"
1564            | "the"
1565            | "and"
1566            | "or"
1567            | "but"
1568            | "in"
1569            | "on"
1570            | "at"
1571            | "to"
1572            | "for"
1573            | "of"
1574            | "with"
1575            | "by"
1576            | "from"
1577            | "is"
1578            | "it"
1579            | "as"
1580            | "be"
1581            | "was"
1582            | "are"
1583            | "this"
1584            | "that"
1585            | "these"
1586            | "those"
1587            | "i"
1588            | "you"
1589            | "he"
1590            | "she"
1591            | "we"
1592            | "they"
1593            | "my"
1594            | "your"
1595            | "his"
1596            | "her"
1597            | "its"
1598            | "our"
1599            | "their"
1600            | "what"
1601            | "which"
1602            | "who"
1603            | "whom"
1604            | "how"
1605            | "when"
1606            | "where"
1607            | "why"
1608            | "will"
1609            | "would"
1610            | "could"
1611            | "should"
1612            | "can"
1613            | "may"
1614            | "might"
1615            | "must"
1616            | "have"
1617            | "has"
1618            | "had"
1619            | "do"
1620            | "does"
1621            | "did"
1622            | "been"
1623            | "being"
1624            | "get"
1625            | "got"
1626            | "just"
1627            | "now"
1628            | "then"
1629            | "so"
1630            | "if"
1631            | "not"
1632            | "no"
1633            | "yes"
1634            | "all"
1635            | "any"
1636            | "some"
1637            | "more"
1638            | "most"
1639            | "other"
1640            | "into"
1641            | "over"
1642            | "after"
1643            | "before"
1644            | "about"
1645            | "up"
1646            | "down"
1647            | "out"
1648            | "off"
1649            | "through"
1650            | "during"
1651            | "under"
1652            | "again"
1653            | "further"
1654            | "once"
1655    )
1656}
1657
1658fn is_pure_search_number(word: &str) -> bool {
1659    if !word.chars().all(|ch| ch.is_ascii_digit()) {
1660        return false;
1661    }
1662    !(word.len() == 4
1663        && word
1664            .parse::<u16>()
1665            .is_ok_and(|year| (1900..=2099).contains(&year)))
1666}
1667
1668fn split_compound_search_word(word: &str) -> Vec<String> {
1669    let mut parts = Vec::new();
1670    let mut current = String::new();
1671    let chars: Vec<char> = word.chars().collect();
1672
1673    for (index, ch) in chars.iter().copied().enumerate() {
1674        let split_before = current.chars().last().is_some_and(|prev| {
1675            (prev.is_lowercase() && ch.is_uppercase())
1676                || (prev.is_ascii_digit() && ch.is_alphabetic())
1677                || (prev.is_alphabetic() && ch.is_ascii_digit())
1678                || (prev.is_uppercase()
1679                    && ch.is_uppercase()
1680                    && chars.get(index + 1).is_some_and(|next| next.is_lowercase()))
1681        });
1682
1683        if split_before && !current.is_empty() {
1684            parts.push(std::mem::take(&mut current));
1685        }
1686
1687        current.push(ch);
1688    }
1689
1690    if !current.is_empty() {
1691        parts.push(current);
1692    }
1693
1694    parts
1695}
1696
1697fn parse_search_keywords(text: &str) -> Vec<String> {
1698    let mut keywords = Vec::new();
1699    let mut seen = HashSet::new();
1700
1701    for word in text
1702        .split(|ch: char| !ch.is_alphanumeric())
1703        .filter(|word| !word.is_empty())
1704    {
1705        let mut variants = Vec::with_capacity(1 + word.len() / 4);
1706        variants.push(word.to_lowercase());
1707        variants.extend(
1708            split_compound_search_word(word)
1709                .into_iter()
1710                .map(|part| part.to_lowercase()),
1711        );
1712
1713        for lowered in variants {
1714            if lowered.chars().count() < 2
1715                || is_search_stop_word(&lowered)
1716                || is_pure_search_number(&lowered)
1717            {
1718                continue;
1719            }
1720            if seen.insert(lowered.clone()) {
1721                keywords.push(lowered);
1722            }
1723        }
1724    }
1725
1726    keywords
1727}
1728
1729fn profile_search_terms_for_event(event: &Event) -> Vec<String> {
1730    let profile = match serde_json::from_str::<serde_json::Value>(&event.content) {
1731        Ok(serde_json::Value::Object(profile)) => profile,
1732        _ => serde_json::Map::new(),
1733    };
1734    let names = extract_profile_names(&profile);
1735    let primary_name = names.first().map(String::as_str);
1736    let mut parts = Vec::new();
1737    if let Some(name) = primary_name {
1738        parts.push(name.to_string());
1739    }
1740    if let Some(nip05) = normalize_profile_nip05(&profile, primary_name) {
1741        parts.push(nip05);
1742    }
1743    parts.push(event.pubkey.to_hex());
1744    if names.len() > 1 {
1745        parts.extend(names.into_iter().skip(1));
1746    }
1747    parse_search_keywords(&parts.join(" "))
1748}
1749
1750fn compare_nostr_events(left: &Event, right: &Event) -> std::cmp::Ordering {
1751    left.created_at
1752        .as_secs()
1753        .cmp(&right.created_at.as_secs())
1754        .then_with(|| left.id.to_hex().cmp(&right.id.to_hex()))
1755}
1756
1757fn map_event_store_error(err: NostrEventStoreError) -> anyhow::Error {
1758    anyhow::anyhow!("nostr event store error: {err}")
1759}
1760
1761#[cfg(test)]
1762fn ensure_social_graph_mapsize(db_dir: &Path, requested_bytes: u64) -> Result<()> {
1763    ensure_social_graph_mapsize_with_env_flags(db_dir, requested_bytes, EnvFlags::empty())
1764}
1765
1766fn ensure_social_graph_mapsize_with_env_flags(
1767    db_dir: &Path,
1768    requested_bytes: u64,
1769    env_flags: EnvFlags,
1770) -> Result<()> {
1771    let map_size = social_graph_map_size(Some(requested_bytes))?;
1772
1773    let mut options = heed::EnvOpenOptions::new();
1774    options.map_size(map_size).max_dbs(SOCIALGRAPH_MAX_DBS);
1775    unsafe {
1776        options.flags(env_flags);
1777    }
1778    let env = unsafe { ManagedEnv::open(&options, db_dir) }
1779        .context("open social graph LMDB env for resize")?;
1780    if env.info().map_size < map_size {
1781        unsafe { env.resize(map_size) }.context("resize social graph LMDB env")?;
1782    }
1783
1784    Ok(())
1785}
1786
1787fn social_graph_map_size(requested_bytes: Option<u64>) -> Result<usize> {
1788    let requested = match requested_bytes {
1789        Some(bytes) => bytes.max(MIN_SOCIALGRAPH_MAP_SIZE_BYTES),
1790        None => DEFAULT_SOCIALGRAPH_MAP_SIZE_BYTES,
1791    };
1792    let page_size = page_size_bytes() as u64;
1793    let rounded = requested
1794        .checked_add(page_size.saturating_sub(1))
1795        .map(|size| size / page_size * page_size)
1796        .unwrap_or(requested);
1797    usize::try_from(rounded).context("social graph mapsize exceeds usize")
1798}
1799
1800fn page_size_bytes() -> usize {
1801    page_size::get_granularity()
1802}
1803
1804#[cfg(test)]
1805mod tests;