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