antibot_rs/
debug_replay.rs1use crate::types::Solution;
5use serde::Serialize;
6use std::path::PathBuf;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9use tracing::warn;
10
11#[derive(Debug, Clone)]
12pub struct DebugConfig {
13 pub directory: PathBuf,
15 pub include_metadata: bool,
17}
18
19impl DebugConfig {
20 pub fn new(directory: impl Into<PathBuf>) -> Self {
21 Self {
22 directory: directory.into(),
23 include_metadata: true,
24 }
25 }
26}
27
28#[derive(Clone)]
29pub(crate) struct DebugSink {
30 config: DebugConfig,
31 counter: Arc<AtomicU64>,
32}
33
34impl DebugSink {
35 pub fn new(config: DebugConfig) -> Self {
36 Self {
37 config,
38 counter: Arc::new(AtomicU64::new(0)),
39 }
40 }
41
42 pub async fn write(&self, url: &str, solution: &Solution) {
45 if let Err(e) = self.write_inner(url, solution).await {
46 warn!("debug sink write failed: {}", e);
47 }
48 }
49
50 async fn write_inner(&self, url: &str, solution: &Solution) -> std::io::Result<()> {
51 tokio::fs::create_dir_all(&self.config.directory).await?;
52
53 let n = self.counter.fetch_add(1, Ordering::Relaxed);
54 let stem = format!("{:06}_{}", n, slug_from_url(url));
55 let html_path = self.config.directory.join(format!("{}.html", stem));
56 let meta_path = self.config.directory.join(format!("{}.json", stem));
57
58 if let Some(html) = &solution.response {
59 tokio::fs::write(&html_path, html.as_bytes()).await?;
60 }
61
62 if self.config.include_metadata {
63 let meta = ReplayMetadata {
64 url: url.to_string(),
65 status: solution.status,
66 user_agent: solution.user_agent.clone(),
67 cookies: solution
68 .cookies
69 .iter()
70 .map(|c| ReplayCookie {
71 name: c.name.clone(),
72 value: c.value.clone(),
73 domain: c.domain.clone(),
74 path: c.path.clone(),
75 })
76 .collect(),
77 source: source_label(solution),
78 solved_at: chrono_unix(&solution.solved_at),
79 };
80 let json = serde_json::to_vec_pretty(&meta).unwrap_or_default();
81 tokio::fs::write(&meta_path, json).await?;
82 }
83
84 Ok(())
85 }
86
87}
88
89#[derive(Serialize)]
90struct ReplayMetadata {
91 url: String,
92 status: u16,
93 user_agent: String,
94 cookies: Vec<ReplayCookie>,
95 source: &'static str,
96 solved_at: u64,
97}
98
99#[derive(Serialize)]
100struct ReplayCookie {
101 name: String,
102 value: String,
103 domain: String,
104 path: String,
105}
106
107fn source_label(solution: &Solution) -> &'static str {
108 match solution.source {
109 crate::types::SolutionSource::Fresh => "fresh",
110 crate::types::SolutionSource::Cached { .. } => "cached",
111 }
112}
113
114fn chrono_unix(t: &std::time::SystemTime) -> u64 {
115 t.duration_since(std::time::UNIX_EPOCH)
116 .map(|d| d.as_secs())
117 .unwrap_or(0)
118}
119
120fn slug_from_url(url: &str) -> String {
121 let mut s = String::with_capacity(url.len().min(80));
122 for c in url.chars().take(80) {
123 if c.is_ascii_alphanumeric() {
124 s.push(c);
125 } else {
126 s.push('_');
127 }
128 }
129 if s.is_empty() {
130 "page".to_string()
131 } else {
132 s
133 }
134}