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