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};
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 crate::db::scoped(async move {
104 let session = crate::db::current_session();
105 let mut stats = VolleyStats::default();
106 let Some(my_pk) = state::my_public_key() else {
107 return (Vec::new(), stats);
108 };
109
110 let mut by_community: Vec<(CommunityId, Vec<PaintTarget>)> = Vec::new();
112 for t in targets {
113 match by_community.iter_mut().find(|(id, _)| *id == t.community_id) {
114 Some((_, v)) => v.push(t),
115 None => by_community.push((t.community_id, vec![t])),
116 }
117 }
118
119 let mut relay_sets: Vec<Vec<String>> = Vec::new();
120 let mut jobs: Vec<Job> = Vec::new();
121 let mut plane_index: HashMap<PublicKey, usize> = HashMap::new();
122 for (cid, ts) in by_community {
123 let cid_hex = crate::simd::hex::bytes_to_hex_32(&cid.0);
124 if crate::db::community::get_community_dissolved(&cid_hex).unwrap_or(false) {
125 continue;
126 }
127 let Ok(Some(community)) = crate::db::community::load_community_v2(&cid) else {
128 continue;
129 };
130 let mut sorted = community.relays.clone();
131 sorted.sort();
132 let relay_set = match relay_sets.iter().position(|r| {
133 let mut s = r.clone();
134 s.sort();
135 s == sorted
136 }) {
137 Some(i) => i,
138 None => {
139 relay_sets.push(community.relays.clone());
140 relay_sets.len() - 1
141 }
142 };
143 for t in ts {
144 let ch_id = ChannelId(crate::simd::hex::hex_to_bytes_32(&t.channel_hex));
145 let Some(ch) = community.channel(&ch_id) else { continue };
146 let ch_hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
151 let (group, epoch) = if ch.private {
152 let mut best: Option<(crate::community::Epoch, [u8; 32])> =
153 ch.key.map(|k| (ch.epoch, k));
154 for (ep, k) in
155 crate::db::community::held_epoch_keys(&cid_hex, &ch_hex).unwrap_or_default()
156 {
157 if k == community.community_root {
159 continue;
160 }
161 if best.map_or(true, |(be, _)| ep.0 > be.0) {
162 best = Some((ep, k));
163 }
164 }
165 let Some((ep, key)) = best else { continue };
166 (channel_group_key(&key, &ch_id, ep), ep)
167 } else {
168 let mut best = (community.root_epoch, community.community_root);
169 for (ep, k) in crate::db::community::held_epoch_keys(
170 &cid_hex,
171 crate::community::SERVER_ROOT_SCOPE_HEX,
172 )
173 .unwrap_or_default()
174 {
175 if ep.0 > best.0 .0 {
176 best = (ep, k);
177 }
178 }
179 (channel_group_key(&best.1, &ch_id, best.0), best.0)
180 };
181 if plane_index.contains_key(&group.pk()) {
184 continue;
185 }
186 plane_index.insert(group.pk(), jobs.len());
187 jobs.push(Job {
188 channel_hex: t.channel_hex,
189 channel_id: ch_id,
190 group,
191 epoch,
192 since: t.since,
193 relay_set,
194 });
195 }
196 }
197 if jobs.is_empty() {
198 return (Vec::new(), stats);
199 }
200
201 let mut by_set: HashMap<usize, Vec<usize>> = HashMap::new();
205 for (i, j) in jobs.iter().enumerate() {
206 by_set.entry(j.relay_set).or_default().push(i);
207 }
208 let batch_start = std::time::Instant::now();
209 crate::community::v2::streamauth::register(jobs.iter().map(|j| j.group.keys().clone()));
214 let shared = LiveTransport::warm_client(
215 relay_sets.iter().flat_map(|r| r.iter().cloned()).collect::<Vec<_>>().as_slice(),
216 std::time::Duration::from_secs(4),
217 )
218 .await
219 .ok();
220
221 let fetch_budget = crate::relay_request_timeout(std::time::Duration::from_secs(4));
225 let mut fetches = FuturesUnordered::new();
226 for (set, idxs) in by_set {
227 let chunks: Vec<Vec<Filter>> = idxs
228 .chunks(BATCH_FILTERS)
229 .map(|chunk| {
230 chunk
231 .iter()
232 .map(|&i| {
233 let j = &jobs[i];
234 Query {
235 kinds: vec![stream::KIND_WRAP],
236 authors: vec![j.group.pk_hex()],
237 since: j.since,
238 limit: Some(50),
239 ..Default::default()
240 }
241 .to_filter()
242 })
243 .collect()
244 })
245 .collect();
246 let relays = relay_sets[set].clone();
247 let client = shared.clone();
248 fetches.push(async move {
249 let Some(client) = client else {
250 return (set, Vec::new(), Vec::new(), false);
251 };
252 let live =
253 connected_targets(&client, &relays, std::time::Duration::from_millis(2500)).await;
254 if live.is_empty() {
255 return (set, live, Vec::new(), false);
256 }
257 let gating_live: Vec<String> = live
261 .iter()
262 .filter(|r| relay_auth_gating(r))
263 .cloned()
264 .collect();
265 if !gating_live.is_empty() {
266 crate::community::v2::streamauth::prime_auth(&client, &gating_live).await;
267 }
268 let mut evs: Vec<Event> = Vec::new();
269 let client_ref = &client;
274 let mut per = futures_util::stream::iter(chunks.iter().flat_map(|chunk| {
275 live.iter().map(move |r| {
276 let c = client_ref.clone();
277 let f = chunk.clone();
278 let r = r.clone();
279 async move {
280 let res = crate::community::transport::fetch_relay_eose_filters(
281 &c, &r, f, fetch_budget,
282 )
283 .await;
284 (r, res)
285 }
286 })
287 }).collect::<Vec<_>>())
288 .buffer_unordered(6);
289 let mut ok_chunks: HashMap<String, usize> = HashMap::new();
295 while let Some((r, res)) = per.next().await {
296 if let Ok(batch) = res {
297 *ok_chunks.entry(r).or_insert(0) += 1;
298 evs.extend(batch);
299 }
300 }
301 let covered = !live.is_empty()
302 && live
303 .iter()
304 .all(|r| ok_chunks.get(r).copied().unwrap_or(0) == chunks.len());
305 (set, live, evs, covered)
306 });
307 }
308
309 let mut live_by_set: HashMap<usize, Vec<String>> = HashMap::new();
314 let mut covered_sets: HashSet<usize> = HashSet::new();
315 let mut seen_wraps: HashSet<EventId> = HashSet::new();
316 let mut ingested: HashSet<usize> = HashSet::new();
317 let mut painted: Vec<(String, usize)> = Vec::new();
318 while let Some((set, live, evs, covered)) = fetches.next().await {
319 if session.stopped() {
323 break;
324 }
325 live_by_set.insert(set, live);
326 if covered {
327 covered_sets.insert(set);
328 }
329 let mut pages: HashMap<usize, Vec<FetchedEvent>> = HashMap::new();
330 for wrap in evs {
331 if !seen_wraps.insert(wrap.id) {
332 continue;
333 }
334 let Some(&job_idx) = plane_index.get(&wrap.pubkey) else { continue };
335 let j = &jobs[job_idx];
336 if let Ok(event) = chat::open_chat_event(&wrap, &j.group, &j.channel_id, j.epoch) {
337 stats.batch_events += 1;
338 pages.entry(job_idx).or_default().push(FetchedEvent { event, epoch: j.epoch });
339 }
340 }
341 for (job_idx, mut page) in pages {
342 page.sort_by_key(|f| f.event.opened().at_ms);
343 let new = crate::VectorCore::v2_ingest_chat_page(
344 &jobs[job_idx].channel_hex,
345 my_pk,
346 session.clone(),
347 page,
348 )
349 .await;
350 ingested.insert(job_idx);
351 if new > 0 {
352 painted.push((jobs[job_idx].channel_hex.clone(), new));
353 }
354 }
355 }
356 stats.batch_ms = batch_start.elapsed().as_millis();
357 let fallback_start = std::time::Instant::now();
358
359 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(4));
366 let gating: HashSet<String> = live_by_set
368 .values()
369 .flat_map(|v| v.iter())
370 .filter(|r| relay_auth_gating(r))
371 .cloned()
372 .collect();
373 let missed: Vec<(usize, nostr_sdk::prelude::Keys, Query, Vec<String>)> = jobs
374 .iter()
375 .enumerate()
376 .filter(|(idx, j)| {
377 if ingested.contains(idx) {
378 return false;
379 }
380 if covered_sets.contains(&j.relay_set) {
383 return false;
384 }
385 let live = live_by_set.get(&j.relay_set).map(Vec::as_slice).unwrap_or(&[]);
388 if live.is_empty() {
389 return false;
390 }
391 if live.iter().all(|r| gating.contains(r.as_str())) {
393 return true;
394 }
395 const RECENT_SECS: u64 = 7 * 24 * 3600;
400 let now = std::time::SystemTime::now()
401 .duration_since(std::time::UNIX_EPOCH)
402 .map(|d| d.as_secs())
403 .unwrap_or(0);
404 j.since.is_some_and(|s| now.saturating_sub(s) < RECENT_SECS)
405 && live.iter().any(|r| gating.contains(r.as_str()))
406 })
407 .map(|(idx, j)| {
408 let q = Query {
409 kinds: vec![stream::KIND_WRAP],
410 authors: vec![j.group.pk_hex()],
411 since: j.since,
412 limit: Some(50),
413 evidence: Evidence::Fast,
416 ..Default::default()
417 };
418 let live = live_by_set.get(&j.relay_set).map(Vec::as_slice).unwrap_or(&[]);
421 let gate_targets: Vec<String> =
422 live.iter().filter(|r| gating.contains(r.as_str())).cloned().collect();
423 let targets = if gate_targets.is_empty() { live.to_vec() } else { gate_targets };
424 (idx, j.group.keys().clone(), q, targets)
425 })
426 .collect();
427 let fallback_pages: Vec<(usize, Vec<Event>)> = futures_util::stream::iter(missed)
428 .map(|(idx, keys, q, relays)| {
429 let t = &transport;
430 async move { (idx, t.fetch_plane(&keys, &q, &relays).await.unwrap_or_default()) }
431 })
432 .buffer_unordered(24)
433 .collect()
434 .await;
435 let mut fb_pages: HashMap<usize, Vec<FetchedEvent>> = HashMap::new();
436 for (job_idx, evs) in fallback_pages {
437 if session.stopped() {
438 break;
439 }
440 let j = &jobs[job_idx];
441 for wrap in evs {
442 if !seen_wraps.insert(wrap.id) {
443 continue;
444 }
445 if wrap.pubkey != j.group.pk() {
446 continue;
447 }
448 if let Ok(event) = chat::open_chat_event(&wrap, &j.group, &j.channel_id, j.epoch) {
449 stats.fallback_events += 1;
450 fb_pages.entry(job_idx).or_default().push(FetchedEvent { event, epoch: j.epoch });
451 }
452 }
453 }
454 for (job_idx, mut page) in fb_pages {
455 page.sort_by_key(|f| f.event.opened().at_ms);
456 let new = crate::VectorCore::v2_ingest_chat_page(
457 &jobs[job_idx].channel_hex,
458 my_pk,
459 session.clone(),
460 page,
461 )
462 .await;
463 if new > 0 {
464 painted.push((jobs[job_idx].channel_hex.clone(), new));
465 }
466 }
467 stats.fallback_ms = fallback_start.elapsed().as_millis();
468 (painted, stats)
469 })
470 .await
471}