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