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