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