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