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