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