vector_core/community/v2/
volley.rs1use std::collections::{HashMap, HashSet};
19
20use futures_util::stream::{FuturesUnordered, StreamExt};
21use nostr_sdk::prelude::*;
22
23use crate::community::transport::{Evidence, LiveTransport, Query, Transport};
24use crate::community::v2::derive::{channel_group_key, GroupKey};
25use crate::community::v2::service::FetchedEvent;
26use crate::community::v2::{chat, stream};
27use crate::community::{ChannelId, CommunityId, Epoch};
28use crate::state::{self, SessionGuard};
29
30pub struct PaintTarget {
33 pub community_id: CommunityId,
34 pub channel_hex: String,
35 pub since: Option<u64>,
36}
37
38const BATCH_FILTERS: usize = 20;
40
41struct Job {
42 channel_hex: String,
43 channel_id: ChannelId,
44 group: GroupKey,
45 epoch: Epoch,
46 since: Option<u64>,
47 relay_set: usize,
48}
49
50fn relay_auth_gating(url: &str) -> bool {
53 crate::db::get_sql_setting(format!(
54 "auth_gate:{}",
55 crate::inbox_relays::normalize_relay_url(url)
56 ))
57 .ok()
58 .flatten()
59 .is_some()
60}
61
62async fn connected_targets(
66 client: &Client,
67 relays: &[String],
68 allowance: std::time::Duration,
69) -> Vec<String> {
70 let deadline = tokio::time::Instant::now() + allowance;
71 loop {
72 let pool = client.relays().await;
73 let up: Vec<String> = relays
74 .iter()
75 .filter(|r| {
76 RelayUrl::parse(r)
77 .ok()
78 .and_then(|u| pool.get(&u).map(|rl| rl.status() == RelayStatus::Connected))
79 .unwrap_or(false)
80 })
81 .cloned()
82 .collect();
83 if !up.is_empty() || tokio::time::Instant::now() >= deadline {
84 return up;
85 }
86 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
87 }
88}
89
90#[derive(Default)]
93pub struct VolleyStats {
94 pub batch_ms: u128,
95 pub fallback_ms: u128,
96 pub batch_events: usize,
97 pub fallback_events: usize,
98}
99
100pub async fn paint_all(targets: Vec<PaintTarget>) -> (Vec<(String, usize)>, VolleyStats) {
103 let session = SessionGuard::capture();
104 let mut stats = VolleyStats::default();
105 let Some(my_pk) = state::my_public_key() else {
106 return (Vec::new(), stats);
107 };
108
109 let mut by_community: Vec<(CommunityId, Vec<PaintTarget>)> = Vec::new();
111 for t in targets {
112 match by_community.iter_mut().find(|(id, _)| *id == t.community_id) {
113 Some((_, v)) => v.push(t),
114 None => by_community.push((t.community_id, vec![t])),
115 }
116 }
117
118 let mut relay_sets: Vec<Vec<String>> = Vec::new();
119 let mut jobs: Vec<Job> = Vec::new();
120 let mut plane_index: HashMap<PublicKey, usize> = HashMap::new();
121 for (cid, ts) in by_community {
122 let cid_hex = crate::simd::hex::bytes_to_hex_32(&cid.0);
123 if crate::db::community::get_community_dissolved(&cid_hex).unwrap_or(false) {
124 continue;
125 }
126 let Ok(Some(community)) = crate::db::community::load_community_v2(&cid) else {
127 continue;
128 };
129 let mut sorted = community.relays.clone();
130 sorted.sort();
131 let relay_set = match relay_sets.iter().position(|r| {
132 let mut s = r.clone();
133 s.sort();
134 s == sorted
135 }) {
136 Some(i) => i,
137 None => {
138 relay_sets.push(community.relays.clone());
139 relay_sets.len() - 1
140 }
141 };
142 for t in ts {
143 let ch_id = ChannelId(crate::simd::hex::hex_to_bytes_32(&t.channel_hex));
144 let Some(ch) = community.channel(&ch_id) else { continue };
145 let ch_hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
150 let (group, epoch) = if ch.private {
151 let mut best: Option<(crate::community::Epoch, [u8; 32])> =
152 ch.key.map(|k| (ch.epoch, k));
153 for (ep, k) in
154 crate::db::community::held_epoch_keys(&cid_hex, &ch_hex).unwrap_or_default()
155 {
156 if k == community.community_root {
158 continue;
159 }
160 if best.map_or(true, |(be, _)| ep.0 > be.0) {
161 best = Some((ep, k));
162 }
163 }
164 let Some((ep, key)) = best else { continue };
165 (channel_group_key(&key, &ch_id, ep), ep)
166 } else {
167 let mut best = (community.root_epoch, community.community_root);
168 for (ep, k) in crate::db::community::held_epoch_keys(
169 &cid_hex,
170 crate::community::SERVER_ROOT_SCOPE_HEX,
171 )
172 .unwrap_or_default()
173 {
174 if ep.0 > best.0 .0 {
175 best = (ep, k);
176 }
177 }
178 (channel_group_key(&best.1, &ch_id, best.0), best.0)
179 };
180 if plane_index.contains_key(&group.pk()) {
183 continue;
184 }
185 plane_index.insert(group.pk(), jobs.len());
186 jobs.push(Job {
187 channel_hex: t.channel_hex,
188 channel_id: ch_id,
189 group,
190 epoch,
191 since: t.since,
192 relay_set,
193 });
194 }
195 }
196 if jobs.is_empty() {
197 return (Vec::new(), stats);
198 }
199
200 let mut by_set: HashMap<usize, Vec<usize>> = HashMap::new();
204 for (i, j) in jobs.iter().enumerate() {
205 by_set.entry(j.relay_set).or_default().push(i);
206 }
207 let batch_start = std::time::Instant::now();
208 crate::community::v2::streamauth::register(jobs.iter().map(|j| j.group.keys().clone()));
213 let shared = LiveTransport::warm_client(
214 relay_sets.iter().flat_map(|r| r.iter().cloned()).collect::<Vec<_>>().as_slice(),
215 std::time::Duration::from_secs(4),
216 )
217 .await
218 .ok();
219
220 let fetch_budget = crate::relay_request_timeout(std::time::Duration::from_secs(4));
224 let mut fetches = FuturesUnordered::new();
225 for (set, idxs) in by_set {
226 let chunks: Vec<Vec<Filter>> = idxs
227 .chunks(BATCH_FILTERS)
228 .map(|chunk| {
229 chunk
230 .iter()
231 .map(|&i| {
232 let j = &jobs[i];
233 Query {
234 kinds: vec![stream::KIND_WRAP],
235 authors: vec![j.group.pk_hex()],
236 since: j.since,
237 limit: Some(50),
238 ..Default::default()
239 }
240 .to_filter()
241 })
242 .collect()
243 })
244 .collect();
245 let relays = relay_sets[set].clone();
246 let client = shared.clone();
247 fetches.push(async move {
248 let Some(client) = client else {
249 return (set, Vec::new(), Vec::new(), false);
250 };
251 let live =
252 connected_targets(&client, &relays, std::time::Duration::from_millis(2500)).await;
253 if live.is_empty() {
254 return (set, live, Vec::new(), false);
255 }
256 let gating_live: Vec<String> = live
260 .iter()
261 .filter(|r| relay_auth_gating(r))
262 .cloned()
263 .collect();
264 if !gating_live.is_empty() {
265 crate::community::v2::streamauth::prime_auth(&client, &gating_live).await;
266 }
267 let mut evs: Vec<Event> = Vec::new();
268 let client_ref = &client;
273 let mut per = futures_util::stream::iter(chunks.iter().flat_map(|chunk| {
274 live.iter().map(move |r| {
275 let c = client_ref.clone();
276 let f = chunk.clone();
277 let r = r.clone();
278 async move {
279 let res = crate::community::transport::fetch_relay_eose_filters(
280 &c, &r, f, fetch_budget,
281 )
282 .await;
283 (r, res)
284 }
285 })
286 }).collect::<Vec<_>>())
287 .buffer_unordered(6);
288 let mut ok_chunks: HashMap<String, usize> = HashMap::new();
294 while let Some((r, res)) = per.next().await {
295 if let Ok(batch) = res {
296 *ok_chunks.entry(r).or_insert(0) += 1;
297 evs.extend(batch);
298 }
299 }
300 let covered = !live.is_empty()
301 && live
302 .iter()
303 .all(|r| ok_chunks.get(r).copied().unwrap_or(0) == chunks.len());
304 (set, live, evs, covered)
305 });
306 }
307
308 let mut live_by_set: HashMap<usize, Vec<String>> = HashMap::new();
313 let mut covered_sets: HashSet<usize> = HashSet::new();
314 let mut seen_wraps: HashSet<EventId> = HashSet::new();
315 let mut ingested: HashSet<usize> = HashSet::new();
316 let mut painted: Vec<(String, usize)> = Vec::new();
317 while let Some((set, live, evs, covered)) = fetches.next().await {
318 live_by_set.insert(set, live);
319 if covered {
320 covered_sets.insert(set);
321 }
322 let mut pages: HashMap<usize, Vec<FetchedEvent>> = HashMap::new();
323 for wrap in evs {
324 if !seen_wraps.insert(wrap.id) {
325 continue;
326 }
327 let Some(&job_idx) = plane_index.get(&wrap.pubkey) else { continue };
328 let j = &jobs[job_idx];
329 if let Ok(event) = chat::open_chat_event(&wrap, &j.group, &j.channel_id, j.epoch) {
330 stats.batch_events += 1;
331 pages.entry(job_idx).or_default().push(FetchedEvent { event, epoch: j.epoch });
332 }
333 }
334 for (job_idx, mut page) in pages {
335 if !session.is_valid() {
336 return (painted, stats);
337 }
338 page.sort_by_key(|f| f.event.opened().at_ms);
339 let new = crate::VectorCore::v2_ingest_chat_page(
340 &jobs[job_idx].channel_hex,
341 my_pk,
342 session,
343 page,
344 )
345 .await;
346 ingested.insert(job_idx);
347 if new > 0 {
348 painted.push((jobs[job_idx].channel_hex.clone(), new));
349 }
350 }
351 }
352 stats.batch_ms = batch_start.elapsed().as_millis();
353 let fallback_start = std::time::Instant::now();
354
355 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(4));
362 let gating: HashSet<String> = live_by_set
364 .values()
365 .flat_map(|v| v.iter())
366 .filter(|r| relay_auth_gating(r))
367 .cloned()
368 .collect();
369 let missed: Vec<(usize, nostr_sdk::prelude::Keys, Query, Vec<String>)> = jobs
370 .iter()
371 .enumerate()
372 .filter(|(idx, j)| {
373 if ingested.contains(idx) {
374 return false;
375 }
376 if covered_sets.contains(&j.relay_set) {
379 return false;
380 }
381 let live = live_by_set.get(&j.relay_set).map(Vec::as_slice).unwrap_or(&[]);
384 if live.is_empty() {
385 return false;
386 }
387 if live.iter().all(|r| gating.contains(r.as_str())) {
389 return true;
390 }
391 const RECENT_SECS: u64 = 7 * 24 * 3600;
396 let now = std::time::SystemTime::now()
397 .duration_since(std::time::UNIX_EPOCH)
398 .map(|d| d.as_secs())
399 .unwrap_or(0);
400 j.since.is_some_and(|s| now.saturating_sub(s) < RECENT_SECS)
401 && live.iter().any(|r| gating.contains(r.as_str()))
402 })
403 .map(|(idx, j)| {
404 let q = Query {
405 kinds: vec![stream::KIND_WRAP],
406 authors: vec![j.group.pk_hex()],
407 since: j.since,
408 limit: Some(50),
409 evidence: Evidence::Fast,
412 ..Default::default()
413 };
414 let live = live_by_set.get(&j.relay_set).map(Vec::as_slice).unwrap_or(&[]);
417 let gate_targets: Vec<String> =
418 live.iter().filter(|r| gating.contains(r.as_str())).cloned().collect();
419 let targets = if gate_targets.is_empty() { live.to_vec() } else { gate_targets };
420 (idx, j.group.keys().clone(), q, targets)
421 })
422 .collect();
423 let fallback_pages: Vec<(usize, Vec<Event>)> = futures_util::stream::iter(missed)
424 .map(|(idx, keys, q, relays)| {
425 let t = &transport;
426 async move { (idx, t.fetch_plane(&keys, &q, &relays).await.unwrap_or_default()) }
427 })
428 .buffer_unordered(24)
429 .collect()
430 .await;
431 let mut fb_pages: HashMap<usize, Vec<FetchedEvent>> = HashMap::new();
432 for (job_idx, evs) in fallback_pages {
433 let j = &jobs[job_idx];
434 for wrap in evs {
435 if !seen_wraps.insert(wrap.id) {
436 continue;
437 }
438 if wrap.pubkey != j.group.pk() {
439 continue;
440 }
441 if let Ok(event) = chat::open_chat_event(&wrap, &j.group, &j.channel_id, j.epoch) {
442 stats.fallback_events += 1;
443 fb_pages.entry(job_idx).or_default().push(FetchedEvent { event, epoch: j.epoch });
444 }
445 }
446 }
447 for (job_idx, mut page) in fb_pages {
448 if !session.is_valid() {
449 break;
450 }
451 page.sort_by_key(|f| f.event.opened().at_ms);
452 let new = crate::VectorCore::v2_ingest_chat_page(
453 &jobs[job_idx].channel_hex,
454 my_pk,
455 session,
456 page,
457 )
458 .await;
459 if new > 0 {
460 painted.push((jobs[job_idx].channel_hex.clone(), new));
461 }
462 }
463 stats.fallback_ms = fallback_start.elapsed().as_millis();
464 (painted, stats)
465}