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