1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::sync::Mutex;
5use std::time::Duration;
6use std::time::Instant;
7
8use anyhow::{Context, Result};
9use hashtree_nostr::{
10 CrawlConfig, CrawlReport, ListEventsOptions, NostrBridge, NostrEventStore, RelayFetchMode,
11 HASHTREE_ROOT_KIND,
12};
13use nostr::{
14 Alphabet, Event, EventBuilder, Filter, Kind, PublicKey, SingleLetterTag, Tag, TagKind,
15 Timestamp,
16};
17use nostr_sdk::{
18 pool::RelayLimits, prelude::RelayPoolNotification, Client, ClientOptions, Keys, RelayStatus,
19};
20use tokio::sync::{watch, Mutex as AsyncMutex};
21use tracing::{debug, info, warn};
22
23use crate::blossom_push::background_blossom_push_incremental_with_store;
24use crate::socialgraph::crawler::SOCIALGRAPH_RELAY_EVENT_MAX_SIZE;
25use crate::socialgraph::{self, SocialGraphBackend, SocialGraphStore};
26use crate::HashtreeStore;
27
28#[cfg(not(test))]
29const MIRROR_STARTUP_DELAY: Duration = Duration::from_secs(8);
30#[cfg(test)]
31const MIRROR_STARTUP_DELAY: Duration = Duration::from_millis(50);
32
33#[cfg(not(test))]
34const MIRROR_CONNECT_SETTLE_DELAY: Duration = Duration::from_secs(1);
35#[cfg(test)]
36const MIRROR_CONNECT_SETTLE_DELAY: Duration = Duration::from_millis(250);
37
38#[cfg(not(test))]
39const MIRROR_AUTHOR_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
40#[cfg(test)]
41const MIRROR_AUTHOR_REFRESH_INTERVAL: Duration = Duration::from_millis(100);
42
43#[cfg(not(test))]
44const MIRROR_RECONNECT_HISTORY_SYNC_COOLDOWN: Duration = Duration::from_secs(30);
45#[cfg(test)]
46const MIRROR_RECONNECT_HISTORY_SYNC_COOLDOWN: Duration = Duration::from_millis(100);
47
48const KIND_LONG_FORM_CONTENT: u16 = 30_023;
49const DEFAULT_HISTORY_KINDS: [u16; 7] = [0, 1, 3, 6, 7, 9735, KIND_LONG_FORM_CONTENT];
50const DEFAULT_EVENT_TREE_NAME: &str = "nostr-event-index";
51const DEFAULT_PROFILE_SEARCH_TREE_NAME: &str = "profile-search";
52const DEFAULT_PROFILES_BY_PUBKEY_TREE_NAME: &str = "profiles-by-pubkey";
53const MIRROR_UPLOAD_STATE_DIR: &str = "nostr-mirror";
54const MIRROR_UPLOADED_ROOT_SUFFIX: &str = ".uploaded-root";
55const METADATA_HISTORY_SYNC_PER_AUTHOR_EVENT_LIMIT: usize = 1;
56const METADATA_HISTORY_SYNC_AUTHOR_BATCH_SIZE: usize = 64;
57const DEFAULT_FULL_TEXT_NOTE_HISTORY_FOLLOW_DISTANCE: u32 = 2;
58const DEFAULT_FULL_TEXT_NOTE_HISTORY_MAX_RELAY_PAGES: usize = 0;
59const FULL_TEXT_HISTORY_PRIORITY_MAX_DISTANCE: u32 = 1;
60const FULL_TEXT_HISTORY_PRIORITY_SAMPLE_LIMIT: usize = 32;
61const LARGE_HISTORY_SYNC_AUTHOR_MULTIPLIER: usize = 8;
62const LARGE_HISTORY_SYNC_PER_AUTHOR_EVENT_LIMIT: usize = 16;
63const LARGE_HISTORY_SYNC_MAX_RELAY_PAGES: usize = 20;
64
65#[cfg(not(test))]
66const MIRROR_MISSING_PROFILE_BACKFILL_INTERVAL: Duration = Duration::from_secs(300);
67#[cfg(test)]
68const MIRROR_MISSING_PROFILE_BACKFILL_INTERVAL: Duration = Duration::from_millis(100);
69
70#[cfg(not(test))]
71const MIRROR_ROOT_PUBLISH_DEBOUNCE: Duration = Duration::from_secs(5);
72#[cfg(test)]
73const MIRROR_ROOT_PUBLISH_DEBOUNCE: Duration = Duration::from_millis(20);
74
75#[cfg(not(test))]
76const MIRROR_ROOT_PUBLISH_MAX_STALENESS: Duration = Duration::from_secs(30);
77#[cfg(test)]
78const MIRROR_ROOT_PUBLISH_MAX_STALENESS: Duration = Duration::from_millis(100);
79
80#[cfg(not(test))]
81const MIRROR_ROOT_UPLOAD_RETRY_INTERVAL: Duration = Duration::from_secs(60);
82#[cfg(test)]
83const MIRROR_ROOT_UPLOAD_RETRY_INTERVAL: Duration = Duration::from_millis(100);
84
85#[cfg(not(test))]
86const MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT: Duration = Duration::from_secs(2);
87#[cfg(test)]
88const MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT: Duration = Duration::from_millis(250);
89
90#[cfg(not(test))]
91const MIRROR_ROOT_PUBLISH_RETRY_TIMEOUT: Duration = Duration::from_secs(5);
92#[cfg(test)]
93const MIRROR_ROOT_PUBLISH_RETRY_TIMEOUT: Duration = Duration::from_secs(2);
94
95const MISSING_LOCAL_BLOB_PUSH_ERROR: &str = "missing local blob";
96
97fn trim_transient_allocations() {
98 #[cfg(all(target_os = "linux", target_env = "gnu"))]
99 unsafe {
100 libc::malloc_trim(0);
104 }
105}
106
107fn decode_hex_pubkey(value: &str) -> Option<[u8; 32]> {
108 let bytes = hex::decode(value).ok()?;
109 <[u8; 32]>::try_from(bytes.as_slice()).ok()
110}
111
112#[derive(Debug, Clone)]
113pub struct NostrMirrorConfig {
114 pub relays: Vec<String>,
115 pub publish_relays: Vec<String>,
116 pub blossom_write_servers: Vec<String>,
117 pub max_follow_distance: u32,
118 pub overmute_threshold: f64,
119 pub author_batch_size: usize,
120 pub history_sync_author_chunk_size: usize,
121 pub history_sync_per_author_event_limit: usize,
122 pub missing_profile_backfill_batch_size: usize,
123 pub fetch_timeout: Duration,
124 pub relay_event_max_size: Option<u32>,
125 pub require_negentropy: bool,
126 pub kinds: Vec<u16>,
127 pub history_sync_on_start: bool,
128 pub history_sync_on_reconnect: bool,
129 pub full_text_note_history_follow_distance: Option<u32>,
130 pub full_text_note_history_max_relay_pages: usize,
131 pub published_event_tree_name: Option<String>,
132 pub published_profile_search_tree_name: Option<String>,
133 pub published_profiles_by_pubkey_tree_name: Option<String>,
134}
135
136impl Default for NostrMirrorConfig {
137 fn default() -> Self {
138 Self {
139 relays: Vec::new(),
140 publish_relays: Vec::new(),
141 blossom_write_servers: Vec::new(),
142 max_follow_distance: 2,
143 overmute_threshold: 1.0,
144 author_batch_size: 256,
145 history_sync_author_chunk_size: 5_000,
146 history_sync_per_author_event_limit: 256,
147 missing_profile_backfill_batch_size: 5_000,
148 fetch_timeout: Duration::from_secs(15),
149 relay_event_max_size: Some(SOCIALGRAPH_RELAY_EVENT_MAX_SIZE),
150 require_negentropy: false,
151 kinds: DEFAULT_HISTORY_KINDS.to_vec(),
152 history_sync_on_start: true,
153 history_sync_on_reconnect: true,
154 full_text_note_history_follow_distance: Some(
155 DEFAULT_FULL_TEXT_NOTE_HISTORY_FOLLOW_DISTANCE,
156 ),
157 full_text_note_history_max_relay_pages: DEFAULT_FULL_TEXT_NOTE_HISTORY_MAX_RELAY_PAGES,
158 published_event_tree_name: Some(DEFAULT_EVENT_TREE_NAME.to_string()),
159 published_profile_search_tree_name: Some(DEFAULT_PROFILE_SEARCH_TREE_NAME.to_string()),
160 published_profiles_by_pubkey_tree_name: Some(
161 DEFAULT_PROFILES_BY_PUBKEY_TREE_NAME.to_string(),
162 ),
163 }
164 }
165}
166
167#[derive(Debug, Default)]
168struct RootPublishState {
169 pending_root: Option<hashtree_core::Cid>,
170 last_changed_at: Option<Instant>,
171 dirty_since: Option<Instant>,
172 last_published_root: Option<hashtree_core::Cid>,
173 last_published_at: Option<Instant>,
174 last_published_created_at: Option<Timestamp>,
175 last_uploaded_root: Option<hashtree_core::Cid>,
176 last_uploaded_at: Option<Instant>,
177 upload_in_progress_root: Option<hashtree_core::Cid>,
178 last_upload_failed_at: Option<Instant>,
179 last_upload_error: Option<String>,
180 missing_blob_rebuild_required: bool,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184struct HistorySyncPlan {
185 relay_fetch_mode: RelayFetchMode,
186 author_batch_size: usize,
187 per_author_event_limit: usize,
188 relay_page_size: usize,
189 max_relay_pages: usize,
190}
191
192pub struct BackgroundNostrMirror {
193 config: NostrMirrorConfig,
194 store: Arc<HashtreeStore>,
195 graph_store: Arc<SocialGraphStore>,
196 client: Client,
197 publish_client: Option<Client>,
198 event_publish_state: Arc<Mutex<RootPublishState>>,
199 profile_search_publish_state: Arc<Mutex<RootPublishState>>,
200 profiles_by_pubkey_publish_state: Arc<Mutex<RootPublishState>>,
201 pending_live_events: Mutex<BTreeMap<String, Event>>,
202 missing_profile_cursor: Mutex<usize>,
203 history_sync_lock: AsyncMutex<()>,
204 shutdown_tx: watch::Sender<bool>,
205 shutdown_rx: watch::Receiver<bool>,
206}
207
208impl BackgroundNostrMirror {
209 pub async fn new(
210 config: NostrMirrorConfig,
211 store: Arc<HashtreeStore>,
212 graph_store: Arc<SocialGraphStore>,
213 publish_keys: Option<Keys>,
214 ) -> Result<Self> {
215 let client = if let Some(max_size) = config.relay_event_max_size {
216 let mut limits = RelayLimits::default();
217 limits.events.max_size = Some(max_size);
218 Client::builder()
219 .signer(Keys::generate())
220 .opts(ClientOptions::new().relay_limits(limits))
221 .build()
222 } else {
223 Client::new(Keys::generate())
224 };
225 for relay in &config.relays {
226 client
227 .add_relay(relay)
228 .await
229 .with_context(|| format!("add mirror relay {relay}"))?;
230 }
231 client.connect().await;
232
233 let publish_client = if let Some(keys) = publish_keys {
234 if config.publish_relays.is_empty() {
235 None
236 } else {
237 let client = Client::new(keys);
238 for relay in &config.publish_relays {
239 client
240 .add_relay(relay)
241 .await
242 .with_context(|| format!("add mirror publish relay {relay}"))?;
243 }
244 client.connect().await;
245 Some(client)
246 }
247 } else {
248 None
249 };
250
251 let (shutdown_tx, shutdown_rx) = watch::channel(false);
252 let event_publish_state = Self::root_publish_state_from_disk(
253 store.base_path(),
254 config.published_event_tree_name.as_deref(),
255 "event root",
256 );
257 let profile_search_publish_state = Self::root_publish_state_from_disk(
258 store.base_path(),
259 config.published_profile_search_tree_name.as_deref(),
260 "profile-search root",
261 );
262 let profiles_by_pubkey_publish_state = Self::root_publish_state_from_disk(
263 store.base_path(),
264 config.published_profiles_by_pubkey_tree_name.as_deref(),
265 "profiles-by-pubkey root",
266 );
267 Ok(Self {
268 config,
269 store,
270 graph_store,
271 client,
272 publish_client,
273 event_publish_state: Arc::new(Mutex::new(event_publish_state)),
274 profile_search_publish_state: Arc::new(Mutex::new(profile_search_publish_state)),
275 profiles_by_pubkey_publish_state: Arc::new(Mutex::new(
276 profiles_by_pubkey_publish_state,
277 )),
278 pending_live_events: Mutex::new(BTreeMap::new()),
279 missing_profile_cursor: Mutex::new(0),
280 history_sync_lock: AsyncMutex::new(()),
281 shutdown_tx,
282 shutdown_rx,
283 })
284 }
285
286 fn root_publish_state_from_disk(
287 base_path: &std::path::Path,
288 tree_name: Option<&str>,
289 log_label: &str,
290 ) -> RootPublishState {
291 let Some(tree_name) = tree_name else {
292 return RootPublishState::default();
293 };
294 let path = Self::uploaded_root_state_path(base_path, tree_name);
295 let Some(root) = Self::read_uploaded_root_state(&path, log_label) else {
296 return RootPublishState::default();
297 };
298
299 RootPublishState {
300 last_uploaded_root: Some(root),
301 ..RootPublishState::default()
302 }
303 }
304
305 fn uploaded_root_state_path(base_path: &std::path::Path, tree_name: &str) -> PathBuf {
306 let safe_tree_name = tree_name
307 .chars()
308 .map(|ch| {
309 if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
310 ch
311 } else {
312 '_'
313 }
314 })
315 .collect::<String>();
316 base_path
317 .join(MIRROR_UPLOAD_STATE_DIR)
318 .join(format!("{safe_tree_name}{MIRROR_UPLOADED_ROOT_SUFFIX}"))
319 }
320
321 fn read_uploaded_root_state(
322 path: &std::path::Path,
323 log_label: &str,
324 ) -> Option<hashtree_core::Cid> {
325 let raw = match std::fs::read_to_string(path) {
326 Ok(raw) => raw,
327 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None,
328 Err(err) => {
329 warn!(
330 "Nostr mirror failed to read uploaded {} state {}: {}",
331 log_label,
332 path.display(),
333 err
334 );
335 return None;
336 }
337 };
338 let trimmed = raw.trim();
339 if trimmed.is_empty() {
340 return None;
341 }
342 match hashtree_core::Cid::parse(trimmed) {
343 Ok(root) => Some(root),
344 Err(err) => {
345 warn!(
346 "Nostr mirror ignored invalid uploaded {} state {}: {}",
347 log_label,
348 path.display(),
349 err
350 );
351 None
352 }
353 }
354 }
355
356 fn write_uploaded_root_state(
357 path: &std::path::Path,
358 root: &hashtree_core::Cid,
359 log_label: &str,
360 ) -> Result<()> {
361 if let Some(parent) = path.parent() {
362 std::fs::create_dir_all(parent)
363 .with_context(|| format!("create {}", parent.display()))?;
364 }
365 std::fs::write(path, format!("{root}\n"))
366 .with_context(|| format!("write uploaded {log_label} state {}", path.display()))
367 }
368
369 pub fn shutdown(&self) {
370 let _ = self.shutdown_tx.send(true);
371 }
372
373 fn sync_publish_roots_from_store(&self) -> Result<()> {
374 self.note_public_events_root_change()?;
375 self.note_profile_search_root_change()?;
376 self.note_profiles_by_pubkey_root_change()?;
377 Ok(())
378 }
379
380 async fn publish_pending_roots(
381 &self,
382 force_event: bool,
383 force_profile_search: bool,
384 force_profiles_by_pubkey: bool,
385 ) -> (Result<()>, Result<()>, Result<()>) {
386 tokio::join!(
387 self.maybe_publish_event_root(force_event),
388 self.maybe_publish_profile_search_root(force_profile_search),
389 self.maybe_publish_profiles_by_pubkey_root(force_profiles_by_pubkey),
390 )
391 }
392
393 async fn publish_priority_roots(
394 &self,
395 force_event: bool,
396 force_profile_search: bool,
397 force_profiles_by_pubkey: bool,
398 ) -> (Result<()>, Result<()>, Result<()>) {
399 let (profile_search_result, profiles_by_pubkey_result) = tokio::join!(
400 async {
401 if force_profile_search {
402 self.maybe_publish_profile_search_root(true).await
403 } else {
404 Ok(())
405 }
406 },
407 async {
408 if force_profiles_by_pubkey {
409 self.maybe_publish_profiles_by_pubkey_root(true).await
410 } else {
411 Ok(())
412 }
413 },
414 );
415 let event_result = if force_event {
416 self.maybe_publish_event_root(true).await
417 } else {
418 Ok(())
419 };
420 (
421 event_result,
422 profile_search_result,
423 profiles_by_pubkey_result,
424 )
425 }
426
427 pub async fn run(self: Arc<Self>) -> Result<()> {
428 if self.config.relays.is_empty() || self.config.max_follow_distance == 0 {
429 return Ok(());
430 }
431
432 info!(
433 "Nostr mirror starting: relays={} max_follow_distance={} negentropy_only={} kinds={:?} history_sync_author_chunk_size={} history_sync_on_start={} history_sync_on_reconnect={}",
434 self.config.relays.len(),
435 self.config.max_follow_distance,
436 self.config.require_negentropy,
437 self.config.kinds,
438 self.config.history_sync_author_chunk_size.max(1),
439 self.config.history_sync_on_start,
440 self.config.history_sync_on_reconnect
441 );
442
443 tokio::time::sleep(MIRROR_STARTUP_DELAY).await;
444 tokio::time::sleep(MIRROR_CONNECT_SETTLE_DELAY).await;
445 let live_since = Timestamp::now();
446 self.sync_publish_roots_from_store()?;
447 let (event_result, profile_search_result, profiles_by_pubkey_result) =
448 self.publish_priority_roots(true, true, true).await;
449 if let Err(err) = event_result {
450 warn!(
451 "Nostr mirror event-root publish failed on startup: {:#}",
452 err
453 );
454 }
455 if let Err(err) = profile_search_result {
456 warn!(
457 "Nostr mirror profile-search publish failed on startup: {:#}",
458 err
459 );
460 }
461 if let Err(err) = profiles_by_pubkey_result {
462 warn!(
463 "Nostr mirror profiles-by-pubkey publish failed on startup: {:#}",
464 err
465 );
466 }
467
468 let initial_authors = self.collect_authors()?;
469 if initial_authors.is_empty() {
470 info!("Nostr mirror: no social-graph authors to mirror yet");
471 }
472
473 let mut subscribed_authors = HashSet::new();
474 self.subscribe_authors_since(&initial_authors, live_since, &mut subscribed_authors)
475 .await?;
476
477 if !initial_authors.is_empty() && self.config.history_sync_on_start {
478 self.spawn_startup_history_sync(initial_authors.clone());
479 }
480
481 let mut relay_statuses = self.capture_relay_statuses().await;
482 let mut last_reconnect_history_sync_at: Option<Instant> = None;
483 let mut last_missing_profile_backfill_at: Option<Instant> = None;
484 let mut notifications = self.client.notifications();
485 let mut shutdown_rx = self.shutdown_rx.clone();
486 let mut refresh_interval = tokio::time::interval(MIRROR_AUTHOR_REFRESH_INTERVAL);
487 let mut publish_interval = tokio::time::interval(MIRROR_ROOT_PUBLISH_DEBOUNCE);
488
489 loop {
490 tokio::select! {
491 _ = shutdown_rx.changed() => {
492 if *shutdown_rx.borrow() {
493 break;
494 }
495 }
496 _ = refresh_interval.tick() => {
497 let authors = self.collect_authors()?;
498 let mut reconnected_relay = None;
499 for (relay_url, status) in self.capture_relay_statuses().await {
500 let previous = relay_statuses.insert(relay_url.clone(), status);
501 if reconnected_relay.is_none()
502 && Self::should_history_sync_on_reconnect(
503 self.config.history_sync_on_reconnect,
504 previous,
505 status,
506 )
507 {
508 reconnected_relay = Some(relay_url);
509 }
510 }
511 if let Some(relay_url) = reconnected_relay {
512 if Self::should_run_reconnect_history_sync(
513 last_reconnect_history_sync_at.as_ref(),
514 ) && !authors.is_empty()
515 {
516 info!(
517 "Nostr mirror relay reconnected; running catch-up history sync: relay={} authors={} negentropy_only={}",
518 relay_url,
519 authors.len(),
520 self.config.require_negentropy
521 );
522 self.spawn_author_history_sync(
523 "relay reconnect catch-up",
524 authors.clone(),
525 false,
526 false,
527 );
528 last_reconnect_history_sync_at = Some(Instant::now());
529 }
530 }
531 let new_authors = authors
532 .iter()
533 .cloned()
534 .filter(|author| !subscribed_authors.contains(author))
535 .collect::<Vec<_>>();
536 if !new_authors.is_empty() {
537 debug!(
538 "Nostr mirror discovered {} newly reachable author(s)",
539 new_authors.len()
540 );
541 self.subscribe_authors_since(
542 &new_authors,
543 Timestamp::now(),
544 &mut subscribed_authors,
545 )
546 .await?;
547 self.spawn_author_history_sync(
548 "new-author catch-up",
549 new_authors.clone(),
550 true,
551 true,
552 );
553 }
554 if self.should_backfill_missing_profiles(last_missing_profile_backfill_at) {
555 let missing_profile_authors = self.collect_missing_profile_authors(
556 self.config.missing_profile_backfill_batch_size,
557 )?;
558 if !missing_profile_authors.is_empty() {
559 info!(
560 "Nostr mirror missing-profile backfill starting: authors={}",
561 missing_profile_authors.len()
562 );
563 self.spawn_missing_profile_backfill(missing_profile_authors);
564 last_missing_profile_backfill_at = Some(Instant::now());
565 }
566 }
567 }
568 _ = publish_interval.tick() => {
569 self.sync_publish_roots_from_store()?;
570 if let Err(err) = self.flush_live_events().await {
571 warn!("Nostr mirror live event flush failed: {:#}", err);
572 }
573 let (event_result, profile_search_result, profiles_by_pubkey_result) = self
574 .publish_pending_roots(false, false, false)
575 .await;
576 if let Err(err) = event_result {
577 warn!("Nostr mirror event-root publish failed: {:#}", err);
578 }
579 if let Err(err) = profile_search_result {
580 warn!("Nostr mirror profile-search publish failed: {:#}", err);
581 }
582 if let Err(err) = profiles_by_pubkey_result {
583 warn!("Nostr mirror profiles-by-pubkey publish failed: {:#}", err);
584 }
585 }
586 notification = notifications.recv() => {
587 match notification {
588 Ok(RelayPoolNotification::Event { event, .. }) => {
589 self.ingest_live_event(&event)?;
590 }
591 Ok(RelayPoolNotification::Shutdown) => break,
592 Ok(_) => {}
593 Err(err) => {
594 warn!("Nostr mirror notification error: {}", err);
595 break;
596 }
597 }
598 }
599 }
600 }
601
602 if let Err(err) = self.flush_live_events().await {
603 warn!(
604 "Nostr mirror live event flush failed during shutdown: {:#}",
605 err
606 );
607 }
608 if let Err(err) = self.sync_publish_roots_from_store() {
609 warn!(
610 "Nostr mirror root-state refresh failed during shutdown: {:#}",
611 err
612 );
613 }
614 let (event_result, profile_search_result, profiles_by_pubkey_result) =
615 self.publish_pending_roots(true, true, true).await;
616 if let Err(err) = event_result {
617 warn!(
618 "Nostr mirror event-root publish failed during shutdown: {:#}",
619 err
620 );
621 }
622 if let Err(err) = profile_search_result {
623 warn!(
624 "Nostr mirror profile-search publish failed during shutdown: {:#}",
625 err
626 );
627 }
628 if let Err(err) = profiles_by_pubkey_result {
629 warn!(
630 "Nostr mirror profiles-by-pubkey publish failed during shutdown: {:#}",
631 err
632 );
633 }
634 let _ = self.client.disconnect().await;
635 if let Some(client) = self.publish_client.as_ref() {
636 let _ = client.disconnect().await;
637 }
638 Ok(())
639 }
640
641 fn spawn_startup_history_sync(self: &Arc<Self>, initial_authors: Vec<String>) {
642 let mirror = Arc::clone(self);
643 tokio::task::spawn_blocking(move || {
644 let runtime = tokio::runtime::Builder::new_current_thread()
645 .enable_all()
646 .build()
647 .expect("build nostr mirror startup history sync runtime");
648 runtime.block_on(async move {
649 let _guard = mirror.history_sync_lock.lock().await;
650 if let Err(err) = mirror.run_startup_history_sync(initial_authors).await {
651 warn!("Nostr mirror startup history sync failed: {:#}", err);
652 }
653 });
654 });
655 }
656
657 async fn run_startup_history_sync(&self, initial_authors: Vec<String>) -> Result<()> {
658 self.history_sync_full_text_notes_for_reachable_authors()
659 .await?;
660 self.history_sync_authors(initial_authors).await?;
661 if self.should_backfill_missing_profiles(None) {
662 let missing_profile_authors = self
663 .collect_missing_profile_authors(self.config.missing_profile_backfill_batch_size)?;
664 if !missing_profile_authors.is_empty() {
665 info!(
666 "Nostr mirror missing-profile backfill starting: authors={}",
667 missing_profile_authors.len()
668 );
669 self.history_sync_authors_with_kinds(
670 missing_profile_authors,
671 &[Kind::Metadata.as_u16()],
672 )
673 .await?;
674 }
675 }
676 Ok(())
677 }
678
679 fn spawn_author_history_sync(
680 self: &Arc<Self>,
681 label: &'static str,
682 authors: Vec<String>,
683 include_full_text_notes: bool,
684 wait_for_existing_sync: bool,
685 ) {
686 let mirror = Arc::clone(self);
687 tokio::task::spawn_blocking(move || {
688 let runtime = tokio::runtime::Builder::new_current_thread()
689 .enable_all()
690 .build()
691 .expect("build nostr mirror author history sync runtime");
692 runtime.block_on(async move {
693 if wait_for_existing_sync {
694 let _guard = mirror.history_sync_lock.lock().await;
695 if let Err(err) = mirror
696 .run_author_history_sync(authors, include_full_text_notes)
697 .await
698 {
699 warn!("Nostr mirror {label} failed: {:#}", err);
700 }
701 return;
702 }
703
704 let Ok(_guard) = mirror.history_sync_lock.try_lock() else {
705 info!("Nostr mirror {label} skipped; another history sync is running");
706 return;
707 };
708 if let Err(err) = mirror
709 .run_author_history_sync(authors, include_full_text_notes)
710 .await
711 {
712 warn!("Nostr mirror {label} failed: {:#}", err);
713 }
714 });
715 });
716 }
717
718 async fn run_author_history_sync(
719 &self,
720 authors: Vec<String>,
721 include_full_text_notes: bool,
722 ) -> Result<()> {
723 if include_full_text_notes {
724 self.history_sync_full_text_notes_for_authors(authors.clone())
725 .await?;
726 }
727 self.history_sync_authors(authors).await
728 }
729
730 fn spawn_missing_profile_backfill(self: &Arc<Self>, authors: Vec<String>) {
731 let mirror = Arc::clone(self);
732 tokio::task::spawn_blocking(move || {
733 let runtime = tokio::runtime::Builder::new_current_thread()
734 .enable_all()
735 .build()
736 .expect("build nostr mirror missing profile runtime");
737 runtime.block_on(async move {
738 let Ok(_guard) = mirror.history_sync_lock.try_lock() else {
739 info!(
740 "Nostr mirror missing-profile backfill skipped; another history sync is running"
741 );
742 return;
743 };
744 if let Err(err) = mirror
745 .history_sync_authors_with_kinds(authors, &[Kind::Metadata.as_u16()])
746 .await
747 {
748 warn!("Nostr mirror missing-profile backfill failed: {:#}", err);
749 }
750 });
751 });
752 }
753
754 async fn capture_relay_statuses(&self) -> HashMap<String, RelayStatus> {
755 let mut statuses = HashMap::new();
756 for (relay_url, relay) in self.client.relays().await {
757 statuses.insert(relay_url.to_string(), relay.status());
758 }
759 statuses
760 }
761
762 async fn has_connected_publish_relay(&self) -> bool {
763 let Some(client) = self.publish_client.as_ref() else {
764 return false;
765 };
766 Self::client_has_connected_relay(client).await
767 }
768
769 async fn client_has_connected_relay(client: &Client) -> bool {
770 for (_relay_url, relay) in client.relays().await {
771 if relay.status() == RelayStatus::Connected {
772 return true;
773 }
774 }
775 false
776 }
777
778 fn collect_authors(&self) -> Result<Vec<String>> {
779 self.collect_authors_with_max_distance(self.config.max_follow_distance)
780 }
781
782 fn collect_authors_with_max_distance(&self, max_distance: u32) -> Result<Vec<String>> {
783 let mut authors = Vec::new();
784 let mut seen = HashSet::new();
785 for distance in 0..=max_distance {
786 for pubkey in socialgraph::SocialGraphBackend::users_by_follow_distance(
787 self.graph_store.as_ref(),
788 distance,
789 )
790 .with_context(|| format!("load social-graph distance {distance}"))?
791 {
792 if self
793 .graph_store
794 .is_overmuted_user(&pubkey, self.config.overmute_threshold)?
795 {
796 continue;
797 }
798 let hex = hex::encode(pubkey);
799 if seen.insert(hex.clone()) {
800 authors.push(hex);
801 }
802 }
803 }
804 Ok(authors)
805 }
806
807 async fn prioritize_full_text_note_history_authors(
808 &self,
809 authors: Vec<String>,
810 ) -> Result<Vec<String>> {
811 let Some(root) = self.graph_store.public_events_root()? else {
812 return Ok(authors);
813 };
814
815 let event_store = NostrEventStore::new(self.store.store_arc());
816 let mut prioritized = Vec::with_capacity(authors.len());
817 let mut sampled = 0usize;
818 for (index, author) in authors.into_iter().enumerate() {
819 let distance = match decode_hex_pubkey(&author) {
820 Some(pubkey) => self
821 .graph_store
822 .follow_distance(&pubkey)?
823 .unwrap_or(u32::MAX),
824 None => u32::MAX,
825 };
826 let indexed_text_sample = if distance <= FULL_TEXT_HISTORY_PRIORITY_MAX_DISTANCE {
827 sampled = sampled.saturating_add(1);
828 event_store
829 .list_by_author_and_kind(
830 Some(&root),
831 &author,
832 Kind::TextNote.as_u16() as u32,
833 ListEventsOptions {
834 limit: Some(FULL_TEXT_HISTORY_PRIORITY_SAMPLE_LIMIT),
835 ..ListEventsOptions::default()
836 },
837 )
838 .await
839 .with_context(|| {
840 format!("sample indexed text-note history for author {author}")
841 })?
842 .len()
843 } else {
844 FULL_TEXT_HISTORY_PRIORITY_SAMPLE_LIMIT
845 };
846 prioritized.push((distance, indexed_text_sample, index, author));
847 }
848
849 prioritized.sort_by(|left, right| {
850 left.0
851 .cmp(&right.0)
852 .then_with(|| left.1.cmp(&right.1))
853 .then_with(|| left.2.cmp(&right.2))
854 });
855 info!(
856 "Nostr mirror full text content history prioritized authors: authors={} sampled_distance_le_{}={}",
857 prioritized.len(),
858 FULL_TEXT_HISTORY_PRIORITY_MAX_DISTANCE,
859 sampled
860 );
861 Ok(prioritized
862 .into_iter()
863 .map(|(_, _, _, author)| author)
864 .collect())
865 }
866
867 fn full_text_note_history_follow_distance(&self) -> Option<u32> {
868 let distance = self.config.full_text_note_history_follow_distance?;
869 if self
870 .config
871 .kinds
872 .iter()
873 .any(|kind| *kind == Kind::TextNote.as_u16() || *kind == KIND_LONG_FORM_CONTENT)
874 {
875 Some(distance.min(self.config.max_follow_distance))
876 } else {
877 None
878 }
879 }
880
881 fn full_text_note_history_max_relay_pages(&self) -> Option<usize> {
882 Self::full_text_note_history_max_relay_pages_for_config(&self.config)
883 }
884
885 fn full_text_note_history_max_relay_pages_for_config(
886 config: &NostrMirrorConfig,
887 ) -> Option<usize> {
888 let pages = config.full_text_note_history_max_relay_pages;
889 if pages == 0 {
890 None
891 } else {
892 Some(pages)
893 }
894 }
895
896 fn is_text_content_history_kind(kind: u16) -> bool {
897 kind == Kind::TextNote.as_u16() || kind == KIND_LONG_FORM_CONTENT
898 }
899
900 fn history_sync_kinds_for_config(config: &NostrMirrorConfig) -> Vec<u16> {
901 let mut kinds = config.kinds.clone();
902 kinds.retain(|kind| !Self::is_text_content_history_kind(*kind));
903 kinds
904 }
905
906 fn collect_missing_profile_authors(&self, limit: usize) -> Result<Vec<String>> {
907 if limit == 0 {
908 return Ok(Vec::new());
909 }
910
911 let authors = self.collect_authors()?;
912 if authors.is_empty() {
913 return Ok(Vec::new());
914 }
915
916 let mut cursor = self
917 .missing_profile_cursor
918 .lock()
919 .expect("missing profile cursor");
920 let mut index = (*cursor).min(authors.len());
921 let mut scanned = 0usize;
922 let mut missing = Vec::new();
923
924 while scanned < authors.len() && missing.len() < limit {
925 let author = &authors[index];
926 if self.graph_store.latest_profile_event(author)?.is_none() {
927 missing.push(author.clone());
928 }
929 index += 1;
930 if index == authors.len() {
931 index = 0;
932 }
933 scanned += 1;
934 }
935
936 *cursor = index;
937 Ok(missing)
938 }
939
940 fn should_backfill_missing_profiles(&self, last_run: Option<Instant>) -> bool {
941 if self.config.missing_profile_backfill_batch_size == 0
942 || !self.config.kinds.contains(&Kind::Metadata.as_u16())
943 {
944 return false;
945 }
946 match last_run {
947 Some(last_run) => last_run.elapsed() >= MIRROR_MISSING_PROFILE_BACKFILL_INTERVAL,
948 None => true,
949 }
950 }
951
952 fn should_history_sync_on_reconnect(
953 history_sync_on_reconnect: bool,
954 previous: Option<RelayStatus>,
955 status: RelayStatus,
956 ) -> bool {
957 history_sync_on_reconnect
958 && status == RelayStatus::Connected
959 && matches!(
960 previous,
961 Some(
962 RelayStatus::Initialized
963 | RelayStatus::Pending
964 | RelayStatus::Connecting
965 | RelayStatus::Disconnected
966 | RelayStatus::Terminated
967 )
968 )
969 }
970
971 fn should_run_reconnect_history_sync(last_run: Option<&Instant>) -> bool {
972 match last_run {
973 None => true,
974 Some(last_run) => last_run.elapsed() >= MIRROR_RECONNECT_HISTORY_SYNC_COOLDOWN,
975 }
976 }
977
978 fn is_metadata_only_history_sync(kinds: &[u16]) -> bool {
979 !kinds.is_empty() && kinds.iter().all(|kind| *kind == Kind::Metadata.as_u16())
980 }
981
982 fn history_sync_kinds_affect_profile_or_graph(kinds: &[u16]) -> bool {
983 kinds.is_empty()
984 || kinds.iter().any(|kind| {
985 *kind == Kind::Metadata.as_u16()
986 || *kind == Kind::ContactList.as_u16()
987 || *kind == Kind::MuteList.as_u16()
988 })
989 }
990
991 fn history_sync_plan_for(
992 config: &NostrMirrorConfig,
993 authors: usize,
994 kinds: &[u16],
995 ) -> HistorySyncPlan {
996 let author_batch_size = config.author_batch_size.max(1);
997 let per_author_event_limit = config.history_sync_per_author_event_limit.max(1);
998 let relay_page_size = 1_000;
999 let max_relay_pages = 10;
1000
1001 if Self::is_metadata_only_history_sync(kinds) {
1002 return HistorySyncPlan {
1003 relay_fetch_mode: RelayFetchMode::AuthorBatches,
1004 author_batch_size: author_batch_size.min(METADATA_HISTORY_SYNC_AUTHOR_BATCH_SIZE),
1005 per_author_event_limit: METADATA_HISTORY_SYNC_PER_AUTHOR_EVENT_LIMIT,
1006 relay_page_size,
1007 max_relay_pages,
1008 };
1009 }
1010
1011 if authors > author_batch_size.saturating_mul(LARGE_HISTORY_SYNC_AUTHOR_MULTIPLIER) {
1012 return HistorySyncPlan {
1013 relay_fetch_mode: RelayFetchMode::GlobalRecent,
1014 author_batch_size,
1015 per_author_event_limit: per_author_event_limit
1016 .min(LARGE_HISTORY_SYNC_PER_AUTHOR_EVENT_LIMIT)
1017 .max(1),
1018 relay_page_size,
1019 max_relay_pages: LARGE_HISTORY_SYNC_MAX_RELAY_PAGES,
1020 };
1021 }
1022
1023 HistorySyncPlan {
1024 relay_fetch_mode: RelayFetchMode::AuthorBatches,
1025 author_batch_size,
1026 per_author_event_limit,
1027 relay_page_size,
1028 max_relay_pages,
1029 }
1030 }
1031
1032 fn history_sync_plan(&self, authors: usize, kinds: &[u16]) -> HistorySyncPlan {
1033 Self::history_sync_plan_for(&self.config, authors, kinds)
1034 }
1035
1036 fn history_sync_chunk_size_for_config(
1037 config: &NostrMirrorConfig,
1038 authors: usize,
1039 kinds: &[u16],
1040 full_author_history: bool,
1041 chunk_size_override: Option<usize>,
1042 ) -> usize {
1043 let configured_chunk_size = chunk_size_override
1044 .unwrap_or(config.history_sync_author_chunk_size)
1045 .max(1);
1046 if full_author_history {
1047 return 1;
1048 }
1049 if !full_author_history
1050 && Self::history_sync_plan_for(config, authors, kinds).relay_fetch_mode
1051 == RelayFetchMode::GlobalRecent
1052 {
1053 return authors.max(1);
1054 }
1055 configured_chunk_size
1056 }
1057
1058 async fn history_sync_authors(&self, authors: Vec<String>) -> Result<()> {
1059 let kinds = Self::history_sync_kinds_for_config(&self.config);
1060 if kinds.is_empty() {
1061 info!("Nostr mirror history sync skipped: no enabled history kinds");
1062 return Ok(());
1063 }
1064 self.history_sync_authors_with_kinds(authors, &kinds).await
1065 }
1066
1067 async fn history_sync_authors_with_kinds(
1068 &self,
1069 authors: Vec<String>,
1070 kinds: &[u16],
1071 ) -> Result<()> {
1072 self.history_sync_authors_with_kinds_and_mode(authors, kinds, false, None)
1073 .await
1074 }
1075
1076 async fn history_sync_full_text_notes_for_reachable_authors(&self) -> Result<()> {
1077 let Some(distance) = self.full_text_note_history_follow_distance() else {
1078 return Ok(());
1079 };
1080 if self.full_text_note_history_max_relay_pages().is_none() {
1081 info!("Nostr mirror full text content history sync skipped: max_relay_pages=0");
1082 return Ok(());
1083 }
1084 info!(
1085 "Nostr mirror full text content history author collection starting: max_follow_distance={distance}"
1086 );
1087 let authors = self
1088 .prioritize_full_text_note_history_authors(
1089 self.collect_authors_with_max_distance(distance)?,
1090 )
1091 .await?;
1092 let Some(max_relay_pages) = self.full_text_note_history_max_relay_pages() else {
1093 info!("Nostr mirror full text content history sync skipped: max_relay_pages=0");
1094 return Ok(());
1095 };
1096 self.history_sync_distance_filtered_full_text_notes(authors, distance, max_relay_pages)
1097 .await
1098 }
1099
1100 async fn history_sync_full_text_notes_for_authors(&self, authors: Vec<String>) -> Result<()> {
1101 let Some(distance) = self.full_text_note_history_follow_distance() else {
1102 return Ok(());
1103 };
1104 let Some(max_relay_pages) = self.full_text_note_history_max_relay_pages() else {
1105 info!("Nostr mirror full text content history sync skipped: max_relay_pages=0");
1106 return Ok(());
1107 };
1108 let mut close_authors = Vec::new();
1109 for author in authors {
1110 let Some(pubkey) = decode_hex_pubkey(&author) else {
1111 continue;
1112 };
1113 if self
1114 .graph_store
1115 .follow_distance(&pubkey)?
1116 .is_some_and(|actual_distance| actual_distance <= distance)
1117 {
1118 close_authors.push(author);
1119 }
1120 }
1121 if close_authors.is_empty() {
1122 return Ok(());
1123 }
1124 let close_authors = self
1125 .prioritize_full_text_note_history_authors(close_authors)
1126 .await?;
1127
1128 self.history_sync_distance_filtered_full_text_notes(
1129 close_authors,
1130 distance,
1131 max_relay_pages,
1132 )
1133 .await
1134 }
1135
1136 async fn history_sync_distance_filtered_full_text_notes(
1137 &self,
1138 close_authors: Vec<String>,
1139 distance: u32,
1140 max_relay_pages: usize,
1141 ) -> Result<()> {
1142 if close_authors.is_empty() {
1143 return Ok(());
1144 }
1145
1146 info!(
1147 "Nostr mirror full text content history sync starting: authors={} max_follow_distance={} max_relay_pages={}",
1148 close_authors.len(),
1149 distance,
1150 max_relay_pages
1151 );
1152 let kinds = [Kind::TextNote.as_u16(), KIND_LONG_FORM_CONTENT];
1153 self.history_sync_authors_with_kinds_and_mode(
1154 close_authors,
1155 &kinds,
1156 true,
1157 Some(max_relay_pages),
1158 )
1159 .await
1160 }
1161
1162 async fn history_sync_authors_with_kinds_and_mode(
1163 &self,
1164 authors: Vec<String>,
1165 kinds: &[u16],
1166 full_author_history: bool,
1167 max_relay_pages: Option<usize>,
1168 ) -> Result<()> {
1169 let update_profile_and_graph = Self::history_sync_kinds_affect_profile_or_graph(kinds);
1170 let chunk_size = Self::history_sync_chunk_size_for_config(
1171 &self.config,
1172 authors.len(),
1173 kinds,
1174 full_author_history,
1175 None,
1176 );
1177 self.history_sync_authors_chunked(
1178 authors,
1179 |current_root, author_chunk| async move {
1180 self.history_sync_author_chunk(
1181 current_root,
1182 author_chunk,
1183 kinds,
1184 full_author_history,
1185 max_relay_pages,
1186 )
1187 .await
1188 },
1189 update_profile_and_graph,
1190 Some(chunk_size),
1191 )
1192 .await
1193 }
1194
1195 async fn history_sync_authors_chunked<F, Fut>(
1196 &self,
1197 authors: Vec<String>,
1198 mut run_chunk: F,
1199 update_profile_and_graph: bool,
1200 chunk_size_override: Option<usize>,
1201 ) -> Result<()>
1202 where
1203 F: FnMut(Option<hashtree_core::Cid>, Vec<String>) -> Fut,
1204 Fut: std::future::Future<Output = Result<CrawlReport>>,
1205 {
1206 if authors.is_empty() {
1207 return Ok(());
1208 }
1209
1210 info!(
1211 "Nostr mirror history sync starting: authors={} relays={} negentropy_only={}",
1212 authors.len(),
1213 self.config.relays.len(),
1214 self.config.require_negentropy
1215 );
1216
1217 let mut current_root = self.graph_store.public_events_root_for_write()?;
1218 let mut last_error = None;
1219 let mut applied_chunks = 0usize;
1220 let mut failed_chunks = 0usize;
1221 let chunk_size = chunk_size_override
1222 .unwrap_or(self.config.history_sync_author_chunk_size)
1223 .max(1);
1224 let total_chunks = authors.len().div_ceil(chunk_size);
1225
1226 for (chunk_index, author_chunk) in authors.chunks(chunk_size).enumerate() {
1227 let author_chunk = author_chunk.to_vec();
1228 let author_count = author_chunk.len();
1229 info!(
1230 "Nostr mirror history sync chunk starting: chunk={}/{} authors={}",
1231 chunk_index + 1,
1232 total_chunks,
1233 author_count
1234 );
1235 let mut report = match run_chunk(current_root.clone(), author_chunk.clone()).await {
1236 Ok(report) => report,
1237 Err(err) => {
1238 failed_chunks = failed_chunks.saturating_add(1);
1239 warn!(
1240 "Nostr mirror history sync chunk failed: chunk={}/{} authors={} error={:#}",
1241 chunk_index + 1,
1242 total_chunks,
1243 author_count,
1244 err
1245 );
1246 last_error = Some(err);
1247 trim_transient_allocations();
1248 continue;
1249 }
1250 };
1251
1252 let latest_root = self.graph_store.public_events_root_for_write()?;
1253 if latest_root != current_root {
1254 info!(
1255 "Nostr mirror history sync root advanced while chunk was fetching; merging chunk into latest root: chunk={}/{} authors={} events_applied={}",
1256 chunk_index + 1,
1257 total_chunks,
1258 author_count,
1259 report.applied_events.len()
1260 );
1261 if report.applied_events.is_empty() {
1262 report.root = latest_root.clone();
1263 } else {
1264 let event_store = NostrEventStore::new(self.store.store_arc());
1265 report.root = event_store
1266 .build(latest_root.as_ref(), report.applied_events.clone())
1267 .await
1268 .context("merge history chunk into latest mirrored event root")?;
1269 }
1270 current_root = latest_root;
1271 }
1272
1273 if report.root != current_root {
1274 self.apply_history_root_with_options(
1275 report.root.as_ref(),
1276 update_profile_and_graph,
1277 true,
1278 Some(&report.applied_events),
1279 )
1280 .await?;
1281 current_root = report.root.clone();
1282 info!(
1283 "Nostr mirror history sync updated trusted root: chunk={}/{} authors_processed={} events_selected={} events_seen={}",
1284 chunk_index + 1,
1285 total_chunks,
1286 report.authors_processed,
1287 report.events_selected,
1288 report.events_seen
1289 );
1290 }
1291 applied_chunks = applied_chunks.saturating_add(1);
1292 trim_transient_allocations();
1293 }
1294
1295 if applied_chunks == 0 {
1296 return Err(last_error
1297 .unwrap_or_else(|| anyhow::anyhow!("mirror history sync made no progress"))
1298 .context("run mirror history sync"));
1299 }
1300 if failed_chunks > 0 {
1301 warn!(
1302 "Nostr mirror history sync completed with skipped chunks: applied_chunks={} failed_chunks={}",
1303 applied_chunks, failed_chunks
1304 );
1305 }
1306 Ok(())
1307 }
1308
1309 async fn history_sync_author_chunk(
1310 &self,
1311 current_root: Option<hashtree_core::Cid>,
1312 authors: Vec<String>,
1313 kinds: &[u16],
1314 full_author_history: bool,
1315 max_relay_pages: Option<usize>,
1316 ) -> Result<CrawlReport> {
1317 let mut last_error = None;
1318 let mut report = None;
1319 let mut plan = self.history_sync_plan(authors.len(), kinds);
1320 if full_author_history {
1321 plan.relay_fetch_mode = RelayFetchMode::AuthorBatches;
1322 plan.max_relay_pages = max_relay_pages.unwrap_or(plan.max_relay_pages);
1323 }
1324 for attempt in 0..3 {
1325 let mut last_logged_authors = 0usize;
1326 let bridge = NostrBridge::new(
1327 self.store.store_arc(),
1328 CrawlConfig {
1329 relays: self.config.relays.clone(),
1330 author_allowlist: Some(authors.clone()),
1331 max_live_bytes: None,
1332 max_events_seen: None,
1333 max_authors: None,
1334 max_follow_distance: None,
1335 author_batch_size: plan.author_batch_size,
1336 per_author_event_limit: plan.per_author_event_limit,
1337 per_author_live_bytes: None,
1338 fetch_timeout: self.config.fetch_timeout,
1339 kinds: Some(kinds.to_vec()),
1340 relay_fetch_mode: plan.relay_fetch_mode,
1341 require_negentropy: self.config.require_negentropy,
1342 relay_event_max_size: self.config.relay_event_max_size,
1343 relay_page_size: plan.relay_page_size,
1344 max_relay_pages: plan.max_relay_pages,
1345 full_author_history,
1346 },
1347 );
1348
1349 let crawl = bridge.crawl_with_progress(
1350 self.graph_store.as_ref(),
1351 current_root.as_ref(),
1352 |progress| {
1353 let log_interval = self.config.author_batch_size.saturating_mul(8).max(2_048);
1354 let should_log = progress.authors_processed == progress.authors_considered
1355 || progress.authors_processed == 0
1356 || progress
1357 .authors_processed
1358 .saturating_sub(last_logged_authors)
1359 >= log_interval;
1360 if should_log {
1361 last_logged_authors = progress.authors_processed;
1362 info!(
1363 "Nostr mirror history sync progress: authors_processed={}/{} events_selected={} events_seen={}",
1364 progress.authors_processed,
1365 progress.authors_considered,
1366 progress.events_selected,
1367 progress.events_seen
1368 );
1369 }
1370 },
1371 );
1372 let crawl_result: Result<CrawlReport> = if full_author_history {
1373 let timeout = self.config.fetch_timeout.saturating_mul(4);
1374 match tokio::time::timeout(timeout, crawl).await {
1375 Ok(result) => result.map_err(Into::into),
1376 Err(_) => Err(anyhow::anyhow!(
1377 "full author history crawl timed out after {:?} for {} author(s)",
1378 timeout,
1379 authors.len()
1380 )),
1381 }
1382 } else {
1383 crawl.await.map_err(Into::into)
1384 };
1385
1386 match crawl_result {
1387 Ok(next_report) => {
1388 report = Some(next_report);
1389 break;
1390 }
1391 Err(err) => {
1392 last_error = Some(err);
1393 if full_author_history {
1394 break;
1395 }
1396 if attempt < 2 {
1397 tokio::time::sleep(Duration::from_millis(500)).await;
1398 }
1399 }
1400 }
1401 }
1402 report
1403 .ok_or_else(|| last_error.expect("history sync retry captured error"))
1404 .context("run mirror history sync")
1405 }
1406
1407 #[cfg(test)]
1408 async fn apply_history_root(&self, root: Option<&hashtree_core::Cid>) -> Result<()> {
1409 self.apply_history_root_with_options(root, true, true, None)
1410 .await
1411 }
1412
1413 async fn apply_history_root_with_options(
1414 &self,
1415 root: Option<&hashtree_core::Cid>,
1416 update_profile_and_graph: bool,
1417 publish_roots: bool,
1418 applied_events: Option<&[hashtree_nostr::StoredNostrEvent]>,
1419 ) -> Result<()> {
1420 self.graph_store.write_public_events_root(root)?;
1421 let Some(root) = root else {
1422 return Ok(());
1423 };
1424
1425 self.note_public_events_root_change()?;
1426 if update_profile_and_graph {
1427 let events = match applied_events {
1428 Some(events) => events
1429 .iter()
1430 .cloned()
1431 .map(socialgraph::stored_event_to_nostr_event)
1432 .collect::<Result<Vec<_>>>()?,
1433 None => {
1434 let event_store = NostrEventStore::new(self.store.store_arc());
1435 event_store
1436 .list_recent_lossy(Some(root), ListEventsOptions::default())
1437 .await
1438 .context("list trusted mirrored events")?
1439 .into_iter()
1440 .map(socialgraph::stored_event_to_nostr_event)
1441 .collect::<Result<Vec<_>>>()?
1442 }
1443 };
1444
1445 socialgraph::ingest_graph_parsed_events(self.graph_store.as_ref(), &events)
1446 .context("sync mirrored social graph state")?;
1447 if applied_events.is_some() {
1448 self.graph_store
1449 .sync_profile_index_for_events(&events)
1450 .context("update mirrored profile search index")?;
1451 } else {
1452 self.graph_store
1453 .rebuild_profile_index_for_events(&events)
1454 .context("rebuild mirrored profile search index")?;
1455 }
1456 self.note_profile_search_root_change()?;
1457 self.note_profiles_by_pubkey_root_change()?;
1458 }
1459 if !publish_roots {
1460 return Ok(());
1461 }
1462 let (event_result, profile_search_result, profiles_by_pubkey_result) = self
1463 .publish_priority_roots(true, update_profile_and_graph, update_profile_and_graph)
1464 .await;
1465 if let Err(err) = event_result {
1466 warn!(
1467 "Nostr mirror event-root publish failed after root update: {:#}",
1468 err
1469 );
1470 }
1471 if let Err(err) = profile_search_result {
1472 warn!(
1473 "Nostr mirror profile-search publish failed after root update: {:#}",
1474 err
1475 );
1476 }
1477 if let Err(err) = profiles_by_pubkey_result {
1478 warn!(
1479 "Nostr mirror profiles-by-pubkey publish failed after root update: {:#}",
1480 err
1481 );
1482 }
1483 Ok(())
1484 }
1485
1486 async fn subscribe_authors_since(
1487 &self,
1488 authors: &[String],
1489 since: Timestamp,
1490 subscribed_authors: &mut HashSet<String>,
1491 ) -> Result<()> {
1492 let new_authors = authors
1493 .iter()
1494 .filter(|author| !subscribed_authors.contains(*author))
1495 .cloned()
1496 .collect::<Vec<_>>();
1497 if new_authors.is_empty() {
1498 return Ok(());
1499 }
1500
1501 for chunk in new_authors.chunks(self.config.author_batch_size.max(1)) {
1502 let pubkeys = chunk
1503 .iter()
1504 .filter_map(|author| PublicKey::from_hex(author).ok())
1505 .collect::<Vec<_>>();
1506 if pubkeys.is_empty() {
1507 continue;
1508 }
1509
1510 let filter = Filter::new()
1511 .authors(pubkeys)
1512 .kinds(self.config.kinds.iter().copied().map(Kind::from))
1513 .since(since);
1514
1515 if let Err(err) = self.client.subscribe(filter, None).await {
1516 warn!(
1517 "Nostr mirror author subscription failed: authors={} error={:#}",
1518 chunk.len(),
1519 err
1520 );
1521 continue;
1522 }
1523 subscribed_authors.extend(chunk.iter().cloned());
1524 }
1525 Ok(())
1526 }
1527
1528 fn ingest_live_event(&self, event: &Event) -> Result<()> {
1529 self.pending_live_events
1530 .lock()
1531 .expect("pending live events")
1532 .insert(event.id.to_hex(), event.clone());
1533 Ok(())
1534 }
1535
1536 async fn flush_live_events(&self) -> Result<()> {
1537 let pending = {
1538 let mut pending = self
1539 .pending_live_events
1540 .lock()
1541 .expect("pending live events");
1542 if pending.is_empty() {
1543 return Ok(());
1544 }
1545 std::mem::take(&mut *pending)
1546 };
1547 let events = pending.into_values().collect::<Vec<_>>();
1548 let event_count = events.len();
1549 let previous_event_root = self.graph_store.public_events_root()?;
1550 let previous_profile_search_root = self.graph_store.profile_search_root()?;
1551 let previous_profiles_by_pubkey_root = self.graph_store.profiles_by_pubkey_root()?;
1552
1553 socialgraph::ingest_parsed_events_with_storage_class(
1554 self.graph_store.as_ref(),
1555 &events,
1556 socialgraph::EventStorageClass::Public,
1557 )
1558 .context("ingest live mirrored event batch")?;
1559
1560 let next_event_root = self.graph_store.public_events_root()?;
1561 let next_profile_search_root = self.graph_store.profile_search_root()?;
1562 let next_profiles_by_pubkey_root = self.graph_store.profiles_by_pubkey_root()?;
1563 let event_root_changed = next_event_root != previous_event_root;
1564 let profile_search_root_changed = next_profile_search_root != previous_profile_search_root;
1565 let profiles_by_pubkey_root_changed =
1566 next_profiles_by_pubkey_root != previous_profiles_by_pubkey_root;
1567
1568 if event_root_changed {
1569 self.note_public_events_root_change()?;
1570 }
1571 if profile_search_root_changed {
1572 self.note_profile_search_root_change()?;
1573 }
1574 if profiles_by_pubkey_root_changed {
1575 self.note_profiles_by_pubkey_root_change()?;
1576 }
1577 if profile_search_root_changed {
1578 self.maybe_publish_profile_search_root(false).await?;
1579 }
1580 if profiles_by_pubkey_root_changed {
1581 self.maybe_publish_profiles_by_pubkey_root(false).await?;
1582 }
1583 if event_root_changed {
1584 self.maybe_publish_event_root(false).await?;
1585 }
1586 info!(
1587 "Nostr mirror flushed live events: events={} event_root_changed={} profile_search_root_changed={} profiles_by_pubkey_root_changed={}",
1588 event_count,
1589 event_root_changed,
1590 profile_search_root_changed,
1591 profiles_by_pubkey_root_changed
1592 );
1593 Ok(())
1594 }
1595
1596 fn note_public_events_root_change(&self) -> Result<()> {
1597 let root = self.graph_store.public_events_root()?;
1598 Self::note_root_change(
1599 self.config.published_event_tree_name.as_deref(),
1600 &self.event_publish_state,
1601 root,
1602 )
1603 }
1604
1605 fn note_profile_search_root_change(&self) -> Result<()> {
1606 let root = self.graph_store.profile_search_root()?;
1607 Self::note_root_change(
1608 self.config.published_profile_search_tree_name.as_deref(),
1609 &self.profile_search_publish_state,
1610 root,
1611 )
1612 }
1613
1614 fn note_profiles_by_pubkey_root_change(&self) -> Result<()> {
1615 let root = self.graph_store.profiles_by_pubkey_root()?;
1616 Self::note_root_change(
1617 self.config
1618 .published_profiles_by_pubkey_tree_name
1619 .as_deref(),
1620 &self.profiles_by_pubkey_publish_state,
1621 root,
1622 )
1623 }
1624
1625 fn note_root_change(
1626 tree_name: Option<&str>,
1627 publish_state: &Arc<Mutex<RootPublishState>>,
1628 root: Option<hashtree_core::Cid>,
1629 ) -> Result<()> {
1630 let Some(_tree_name) = tree_name else {
1631 return Ok(());
1632 };
1633
1634 let mut state = publish_state.lock().expect("root publish state");
1635 let now = Instant::now();
1636
1637 if state.pending_root == root {
1638 return Ok(());
1639 }
1640
1641 state.pending_root = root;
1642 state.last_upload_failed_at = None;
1643 state.last_upload_error = None;
1644 state.last_changed_at = Some(now);
1645 if state.dirty_since.is_none() {
1646 state.dirty_since = Some(now);
1647 }
1648 Ok(())
1649 }
1650
1651 async fn maybe_publish_event_root(&self, force: bool) -> Result<()> {
1652 if self.take_missing_blob_event_upload_error() {
1653 return self.rebuild_event_indexes_after_missing_blobs(force).await;
1654 }
1655 let result = self
1656 .maybe_publish_root(
1657 self.config.published_event_tree_name.as_deref(),
1658 &self.event_publish_state,
1659 "event root",
1660 force,
1661 false,
1662 )
1663 .await;
1664 let Err(error) = result else {
1665 if self.take_missing_blob_event_upload_error() {
1666 return self.rebuild_event_indexes_after_missing_blobs(force).await;
1667 }
1668 return Ok(());
1669 };
1670 if !is_missing_local_blob_push_error(&error) {
1671 return Err(error);
1672 }
1673
1674 self.rebuild_event_indexes_after_missing_blobs(force).await
1675 }
1676
1677 fn take_missing_blob_event_upload_error(&self) -> bool {
1678 let mut state = self
1679 .event_publish_state
1680 .lock()
1681 .expect("event root publish state");
1682 if state.upload_in_progress_root.is_some() {
1683 return false;
1684 }
1685 if !state.missing_blob_rebuild_required {
1686 return false;
1687 }
1688 state.missing_blob_rebuild_required = false;
1689 state.last_upload_error = None;
1690 state.last_upload_failed_at = None;
1691 true
1692 }
1693
1694 async fn rebuild_event_indexes_after_missing_blobs(&self, force: bool) -> Result<()> {
1695 warn!(
1696 "Nostr mirror event root DAG references missing local blobs; rebuilding event indexes from stored events"
1697 );
1698 let (public_count, ambient_count) = self
1699 .graph_store
1700 .rebuild_event_indexes_from_stored_events_async()
1701 .await
1702 .context("rebuild event indexes after missing event blobs")?;
1703 info!(
1704 "Nostr mirror rebuilt event indexes after missing blobs: public={} ambient={}",
1705 public_count, ambient_count
1706 );
1707 self.sync_publish_roots_from_store()?;
1708
1709 self.maybe_publish_root(
1710 self.config.published_event_tree_name.as_deref(),
1711 &self.event_publish_state,
1712 "event root",
1713 force,
1714 false,
1715 )
1716 .await
1717 }
1718
1719 async fn maybe_publish_profile_search_root(&self, force: bool) -> Result<()> {
1720 self.maybe_publish_root(
1721 self.config.published_profile_search_tree_name.as_deref(),
1722 &self.profile_search_publish_state,
1723 "profile search root",
1724 force,
1725 true,
1726 )
1727 .await
1728 }
1729
1730 async fn maybe_publish_profiles_by_pubkey_root(&self, force: bool) -> Result<()> {
1731 self.maybe_publish_root(
1732 self.config
1733 .published_profiles_by_pubkey_tree_name
1734 .as_deref(),
1735 &self.profiles_by_pubkey_publish_state,
1736 "profiles-by-pubkey root",
1737 force,
1738 true,
1739 )
1740 .await
1741 }
1742
1743 async fn maybe_publish_root(
1744 &self,
1745 tree_name: Option<&str>,
1746 publish_state: &Arc<Mutex<RootPublishState>>,
1747 log_label: &str,
1748 force: bool,
1749 publish_before_upload_ready_on_force: bool,
1750 ) -> Result<()> {
1751 let Some(tree_name) = tree_name else {
1752 return Ok(());
1753 };
1754
1755 let pending_root = {
1756 let state = publish_state.lock().expect("root publish state");
1757 let Some(pending_root) = state.pending_root.clone() else {
1758 return Ok(());
1759 };
1760
1761 let now = Instant::now();
1762 let debounce_ready = state.last_changed_at.is_some_and(|changed_at| {
1763 now.duration_since(changed_at) >= MIRROR_ROOT_PUBLISH_DEBOUNCE
1764 });
1765 let stale_ready = state.dirty_since.is_some_and(|dirty_since| {
1766 now.duration_since(dirty_since) >= MIRROR_ROOT_PUBLISH_MAX_STALENESS
1767 });
1768 if !force && !debounce_ready && !stale_ready {
1769 return Ok(());
1770 }
1771
1772 pending_root
1773 };
1774
1775 let upload_started = self.maybe_start_background_root_upload(
1776 tree_name,
1777 &pending_root,
1778 publish_state,
1779 log_label,
1780 );
1781 let upload_required = !self.config.blossom_write_servers.is_empty();
1782 let (upload_ready, publish_root) = {
1783 let state = publish_state.lock().expect("root publish state");
1784 let upload_ready =
1785 !upload_required || state.last_uploaded_root.as_ref() == Some(&pending_root);
1786 let publish_root = if upload_ready {
1787 Some(pending_root.clone())
1788 } else {
1789 state.last_uploaded_root.clone().filter(|uploaded_root| {
1790 state.last_published_root.as_ref() != Some(uploaded_root)
1791 })
1792 };
1793 (upload_ready, publish_root)
1794 };
1795 let publish_before_upload_ready =
1796 force && upload_required && !upload_ready && publish_before_upload_ready_on_force;
1797 let publish_root = if let Some(publish_root) = publish_root {
1798 publish_root
1799 } else if publish_before_upload_ready {
1800 pending_root.clone()
1801 } else {
1802 if upload_started {
1803 info!(
1804 "Nostr mirror uploading {} DAG before publish: tree={} hash={}",
1805 log_label,
1806 tree_name,
1807 hex::encode(pending_root.hash),
1808 );
1809 }
1810 return Ok(());
1811 };
1812
1813 let mut successful_relays = Vec::new();
1814 let mut failed_relays = Vec::new();
1815 let mut published_now = false;
1816 let publish_required =
1817 self.publish_client.is_some() && !self.config.publish_relays.is_empty();
1818 if publish_required {
1819 let Some(publish_client) = self.publish_client.as_ref() else {
1820 unreachable!("publish_required implies publish_client");
1821 };
1822 if !self.has_connected_publish_relay().await {
1823 return Ok(());
1824 }
1825 if publish_before_upload_ready {
1826 info!(
1827 "Nostr mirror publishing {} before Blossom upload completes: tree={} hash={}",
1828 log_label,
1829 tree_name,
1830 hex::encode(pending_root.hash),
1831 );
1832 } else if !upload_ready {
1833 info!(
1834 "Nostr mirror publishing uploaded {} while newer root is still uploading: tree={} published_hash={} pending_hash={}",
1835 log_label,
1836 tree_name,
1837 hex::encode(publish_root.hash),
1838 hex::encode(pending_root.hash),
1839 );
1840 }
1841
1842 let already_published = {
1843 let state = publish_state.lock().expect("root publish state");
1844 state.last_published_root.as_ref() == Some(&publish_root)
1845 };
1846 if !already_published {
1847 let publish_relays = self.config.publish_relays.clone();
1848 let latest_known_created_at = {
1849 let state = publish_state.lock().expect("root publish state");
1850 state.last_published_created_at
1851 };
1852 let publish_created_at =
1853 next_replaceable_created_at(Timestamp::now(), latest_known_created_at);
1854 let event = publish_client
1855 .sign_event_builder(Self::build_public_root_event(
1856 tree_name,
1857 &publish_root,
1858 publish_created_at,
1859 ))
1860 .await
1861 .with_context(|| format!("sign {log_label} event"))?;
1862 let publish_result = self
1863 .publish_root_event_to_relays(publish_client, &publish_relays, &event)
1864 .await
1865 .with_context(|| format!("publish {log_label} event"))?;
1866 successful_relays = publish_result.0;
1867 failed_relays = publish_result.1;
1868 if successful_relays.is_empty() {
1869 let failure_summary = if failed_relays.is_empty() {
1870 "no publish relays accepted the event".to_string()
1871 } else {
1872 failed_relays.join("; ")
1873 };
1874 anyhow::bail!("no publish relays accepted the event ({failure_summary})");
1875 }
1876
1877 let mut state = publish_state.lock().expect("root publish state");
1878 if state.pending_root.as_ref() == Some(&pending_root)
1879 || state.last_uploaded_root.as_ref() == Some(&publish_root)
1880 || publish_before_upload_ready
1881 {
1882 state.last_published_root = Some(publish_root.clone());
1883 state.last_published_at = Some(Instant::now());
1884 state.last_published_created_at = Some(event.created_at);
1885 }
1886 published_now = true;
1887 }
1888 }
1889
1890 {
1891 let mut state = publish_state.lock().expect("root publish state");
1892 if state.pending_root.as_ref() == Some(&pending_root) {
1893 let upload_satisfied = self.config.blossom_write_servers.is_empty()
1894 || state.last_uploaded_root.as_ref() == Some(&pending_root);
1895 let publish_satisfied =
1896 !publish_required || state.last_published_root.as_ref() == Some(&pending_root);
1897 if upload_satisfied && publish_satisfied {
1898 state.dirty_since = None;
1899 }
1900 }
1901 }
1902
1903 if published_now {
1904 info!(
1905 "Nostr mirror published {}: tree={} hash={} relays={:?}",
1906 log_label,
1907 tree_name,
1908 hex::encode(publish_root.hash),
1909 successful_relays,
1910 );
1911 }
1912 if !failed_relays.is_empty() {
1913 warn!(
1914 "Nostr mirror publish had relay failures: tree={} failures={:?}",
1915 tree_name, failed_relays
1916 );
1917 }
1918 Ok(())
1919 }
1920
1921 fn maybe_start_background_root_upload(
1922 &self,
1923 tree_name: &str,
1924 pending_root: &hashtree_core::Cid,
1925 publish_state: &Arc<Mutex<RootPublishState>>,
1926 log_label: &str,
1927 ) -> bool {
1928 if self.config.blossom_write_servers.is_empty() {
1929 return false;
1930 }
1931
1932 let previous_uploaded_root = {
1933 let mut state = publish_state.lock().expect("root publish state");
1934 if state.last_uploaded_root.as_ref() == Some(pending_root)
1935 || state.upload_in_progress_root.is_some()
1936 {
1937 return false;
1938 }
1939 if state
1940 .last_upload_failed_at
1941 .is_some_and(|failed_at| failed_at.elapsed() < MIRROR_ROOT_UPLOAD_RETRY_INTERVAL)
1942 {
1943 return false;
1944 }
1945 state.upload_in_progress_root = Some(pending_root.clone());
1946 state.last_uploaded_root.clone()
1947 };
1948
1949 let store = Arc::clone(&self.store);
1950 let upload_state_path = Self::uploaded_root_state_path(store.base_path(), tree_name);
1951 let servers = self.config.blossom_write_servers.clone();
1952 let root = pending_root.clone();
1953 let publish_state = Arc::clone(publish_state);
1954 let log_label = log_label.to_string();
1955 tokio::task::spawn_blocking(move || {
1956 let runtime = tokio::runtime::Builder::new_current_thread()
1957 .enable_all()
1958 .build()
1959 .expect("build nostr mirror root upload runtime");
1960 runtime.block_on(async move {
1961 let result = background_blossom_push_incremental_with_store(
1962 store,
1963 root.clone(),
1964 previous_uploaded_root,
1965 &servers,
1966 )
1967 .await;
1968 let mut state = publish_state.lock().expect("root publish state");
1969 if state.upload_in_progress_root.as_ref() == Some(&root) {
1970 state.upload_in_progress_root = None;
1971 }
1972 match result {
1973 Ok(()) => {
1974 if let Err(err) =
1975 Self::write_uploaded_root_state(&upload_state_path, &root, &log_label)
1976 {
1977 warn!("Nostr mirror failed to persist uploaded root state: {err:#}");
1978 }
1979 state.last_uploaded_root = Some(root.clone());
1980 state.last_uploaded_at = Some(Instant::now());
1981 if state.pending_root.as_ref() == Some(&root) {
1982 state.last_upload_failed_at = None;
1983 state.last_upload_error = None;
1984 state.missing_blob_rebuild_required = false;
1985 }
1986 info!(
1987 "Nostr mirror uploaded {} DAG to Blossom: hash={}",
1988 log_label,
1989 hex::encode(root.hash)
1990 );
1991 }
1992 Err(err) => {
1993 if state.pending_root.as_ref() == Some(&root) {
1994 state.last_upload_failed_at = Some(Instant::now());
1995 state.last_upload_error = Some(format!("{err:#}"));
1996 }
1997 if is_missing_local_blob_message(&format!("{err:#}")) {
1998 state.missing_blob_rebuild_required = true;
1999 }
2000 warn!(
2001 "Nostr mirror {} DAG upload failed: hash={} error={:#}",
2002 log_label,
2003 hex::encode(root.hash),
2004 err
2005 );
2006 }
2007 }
2008 });
2009 trim_transient_allocations();
2010 });
2011
2012 true
2013 }
2014
2015 async fn publish_root_event_to_relays(
2016 &self,
2017 publish_client: &Client,
2018 relays: &[String],
2019 event: &Event,
2020 ) -> Result<(Vec<String>, Vec<String>)> {
2021 let primary_send = Self::send_root_event_to_relays(publish_client, relays, event);
2022 let (mut successful_relays, mut failed_relays) =
2023 match tokio::time::timeout(MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT, primary_send).await {
2024 Ok(result) => result,
2025 Err(_) => (
2026 Vec::new(),
2027 vec![format!(
2028 "publish relays: primary publish timed out after {:?}",
2029 MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT
2030 )],
2031 ),
2032 };
2033
2034 if successful_relays.is_empty() {
2035 let (retry_successes, retry_failures) = self
2036 .publish_root_event_with_fresh_client(relays, event)
2037 .await;
2038 successful_relays.extend(retry_successes);
2039 failed_relays.extend(retry_failures);
2040 }
2041
2042 Ok((successful_relays, failed_relays))
2043 }
2044
2045 async fn send_root_event_to_relays(
2046 publish_client: &Client,
2047 relays: &[String],
2048 event: &Event,
2049 ) -> (Vec<String>, Vec<String>) {
2050 let mut successful_relays = Vec::new();
2051 let mut failed_relays = Vec::new();
2052
2053 match publish_client
2054 .send_event_to(relays.iter().map(|relay| relay.as_str()), event)
2055 .await
2056 {
2057 Ok(output) => {
2058 for relay in relays {
2059 let relay_url = relay.trim_end_matches('/');
2060 if output
2061 .success
2062 .iter()
2063 .any(|url| url.as_str().trim_end_matches('/') == relay_url)
2064 {
2065 successful_relays.push(relay.clone());
2066 }
2067 }
2068 failed_relays.extend(
2069 output
2070 .failed
2071 .into_iter()
2072 .map(|(url, reason)| format!("{url}: {reason}")),
2073 );
2074 }
2075 Err(err) => {
2076 failed_relays.push(format!("publish relays: {err}"));
2077 }
2078 }
2079
2080 (successful_relays, failed_relays)
2081 }
2082
2083 async fn publish_root_event_with_fresh_client(
2084 &self,
2085 relays: &[String],
2086 event: &Event,
2087 ) -> (Vec<String>, Vec<String>) {
2088 let client = Client::new(Keys::generate());
2089 let mut setup_failures = Vec::new();
2090 for relay in relays {
2091 if let Err(err) = client.add_relay(relay).await {
2092 setup_failures.push(format!("{relay}: add relay failed: {err}"));
2093 }
2094 }
2095
2096 client.connect().await;
2097 let publish = Self::send_root_event_to_relays(&client, relays, event);
2098 let retry_timeout = self
2099 .config
2100 .fetch_timeout
2101 .min(MIRROR_ROOT_PUBLISH_RETRY_TIMEOUT);
2102 let result = tokio::time::timeout(retry_timeout, publish).await;
2103 let _ = client.disconnect().await;
2104
2105 match result {
2106 Ok((successful_relays, mut failed_relays)) => {
2107 failed_relays.extend(setup_failures);
2108 (successful_relays, failed_relays)
2109 }
2110 Err(_) => {
2111 setup_failures.push(format!(
2112 "fresh publish client timed out after {:?}",
2113 retry_timeout
2114 ));
2115 (Vec::new(), setup_failures)
2116 }
2117 }
2118 }
2119
2120 fn build_public_root_event(
2121 tree_name: &str,
2122 cid: &hashtree_core::Cid,
2123 created_at: Timestamp,
2124 ) -> EventBuilder {
2125 let mut tags = vec![
2126 Tag::identifier(tree_name.to_string()),
2127 Tag::custom(
2128 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)),
2129 vec!["hashtree"],
2130 ),
2131 Tag::custom(TagKind::Custom("hash".into()), vec![hex::encode(cid.hash)]),
2132 ];
2133 if let Some(key) = cid.key {
2134 tags.push(Tag::custom(
2135 TagKind::Custom("key".into()),
2136 vec![hex::encode(key)],
2137 ));
2138 }
2139
2140 EventBuilder::new(Kind::Custom(HASHTREE_ROOT_KIND as u16), "")
2141 .tags(tags)
2142 .custom_created_at(created_at)
2143 }
2144}
2145
2146fn is_missing_local_blob_push_error(error: &anyhow::Error) -> bool {
2147 error
2148 .chain()
2149 .any(|cause| cause.to_string().contains(MISSING_LOCAL_BLOB_PUSH_ERROR))
2150}
2151
2152fn is_missing_local_blob_message(message: &str) -> bool {
2153 message.contains(MISSING_LOCAL_BLOB_PUSH_ERROR)
2154}
2155
2156fn next_replaceable_created_at(now: Timestamp, latest_existing: Option<Timestamp>) -> Timestamp {
2157 match latest_existing {
2158 Some(latest) if latest >= now => Timestamp::from_secs(latest.as_secs().saturating_add(1)),
2159 _ => now,
2160 }
2161}
2162
2163#[cfg(test)]
2164mod tests;