webpuppet/
config.rs

1//! Configuration for webpuppet browser automation.
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::time::Duration;
6
7/// Main configuration for WebPuppet.
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
9pub struct Config {
10    /// Browser configuration.
11    pub browser: BrowserConfig,
12    /// Provider-specific settings.
13    pub providers: ProvidersConfig,
14    /// Session management settings.
15    pub session: SessionConfig,
16    /// Rate limiting settings.
17    pub rate_limit: RateLimitConfig,
18}
19
20/// Browser-specific configuration.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct BrowserConfig {
23    /// Run browser in headless mode.
24    pub headless: bool,
25    /// Path to browser executable (auto-detect if None).
26    pub executable_path: Option<PathBuf>,
27    /// User data directory for profiles.
28    pub user_data_dir: Option<PathBuf>,
29    /// Browser window width.
30    pub window_width: u32,
31    /// Browser window height.
32    pub window_height: u32,
33    /// Additional browser arguments.
34    pub args: Vec<String>,
35    /// Request timeout.
36    #[serde(with = "humantime_serde")]
37    pub timeout: Duration,
38    /// Enable devtools (debug mode).
39    pub devtools: bool,
40    /// Sandbox mode (disable for containers).
41    pub sandbox: bool,
42    /// Dual-head mode: launches a visible monitoring window alongside headless automation.
43    pub dual_head: bool,
44}
45
46impl Default for BrowserConfig {
47    fn default() -> Self {
48        Self {
49            headless: true,
50            executable_path: None,
51            user_data_dir: None,
52            window_width: 1920,
53            window_height: 1080,
54            args: vec![
55                "--disable-gpu".into(),
56                "--disable-dev-shm-usage".into(),
57                "--no-first-run".into(),
58            ],
59            timeout: Duration::from_secs(60),
60            devtools: false,
61            sandbox: true,
62            dual_head: false,
63        }
64    }
65}
66
67/// Provider-specific configurations.
68#[derive(Debug, Clone, Serialize, Deserialize, Default)]
69pub struct ProvidersConfig {
70    /// Grok (X.ai) configuration.
71    #[cfg(feature = "grok")]
72    pub grok: GrokConfig,
73    /// Claude (Anthropic) configuration.
74    #[cfg(feature = "claude")]
75    pub claude: ClaudeConfig,
76    /// Gemini (Google) configuration.
77    #[cfg(feature = "gemini")]
78    pub gemini: GeminiConfig,
79}
80
81/// Grok-specific settings.
82#[cfg(feature = "grok")]
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct GrokConfig {
85    /// Login URL.
86    pub login_url: String,
87    /// Chat URL.
88    pub chat_url: String,
89    /// CSS selector for input field.
90    pub input_selector: String,
91    /// CSS selector for submit button.
92    pub submit_selector: String,
93    /// CSS selector for response container.
94    pub response_selector: String,
95    /// CSS selector to wait for page ready.
96    pub ready_selector: String,
97    /// CSS selector for file input (if supported).
98    pub file_input_selector: Option<String>,
99    /// Model variant to use.
100    pub model: String,
101}
102
103#[cfg(feature = "grok")]
104impl Default for GrokConfig {
105    fn default() -> Self {
106        Self {
107            login_url: "https://x.com/i/grok".into(),
108            chat_url: "https://x.com/i/grok".into(),
109            input_selector: r#"textarea[data-testid="grokInput"]"#.into(),
110            submit_selector: r#"button[data-testid="grokSend"]"#.into(),
111            response_selector: r#"div[data-testid="grokResponse"]"#.into(),
112            ready_selector: r#"textarea[data-testid="grokInput"]"#.into(),
113            file_input_selector: Some(r#"input[type="file"]"#.into()),
114            model: "grok-2".into(),
115        }
116    }
117}
118
119/// Claude-specific settings.
120#[cfg(feature = "claude")]
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ClaudeConfig {
123    /// Login URL.
124    pub login_url: String,
125    /// Chat URL.
126    pub chat_url: String,
127    /// CSS selector for input field.
128    pub input_selector: String,
129    /// CSS selector for submit button.
130    pub submit_selector: String,
131    /// CSS selector for response container.
132    pub response_selector: String,
133    /// CSS selector to wait for page ready.
134    pub ready_selector: String,
135    /// CSS selector for file input.
136    pub file_input_selector: Option<String>,
137    /// Organization (if applicable).
138    pub organization: Option<String>,
139}
140
141#[cfg(feature = "claude")]
142impl Default for ClaudeConfig {
143    fn default() -> Self {
144        Self {
145            login_url: "https://claude.ai/login".into(),
146            chat_url: "https://claude.ai/new".into(),
147            input_selector: r#"div[contenteditable="true"]"#.into(),
148            submit_selector: r#"button[aria-label="Send message"]"#.into(),
149            response_selector: r#"div.prose"#.into(),
150            ready_selector: r#"div[contenteditable="true"]"#.into(),
151            file_input_selector: Some(r#"input[type="file"]"#.into()),
152            organization: None,
153        }
154    }
155}
156
157/// Gemini-specific settings.
158#[cfg(feature = "gemini")]
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct GeminiConfig {
161    /// Login URL.
162    pub login_url: String,
163    /// Chat URL.
164    pub chat_url: String,
165    /// CSS selector for input field.
166    pub input_selector: String,
167    /// CSS selector for submit button.
168    pub submit_selector: String,
169    /// CSS selector for response container.
170    pub response_selector: String,
171    /// CSS selector to wait for page ready.
172    pub ready_selector: String,
173    /// CSS selector for file input.
174    pub file_input_selector: Option<String>,
175    /// Google account to use.
176    pub google_account: Option<String>,
177}
178
179#[cfg(feature = "gemini")]
180impl Default for GeminiConfig {
181    fn default() -> Self {
182        Self {
183            login_url: "https://gemini.google.com".into(),
184            chat_url: "https://gemini.google.com/app".into(),
185            input_selector: r#"rich-textarea"#.into(),
186            submit_selector: r#"button[aria-label="Send message"]"#.into(),
187            response_selector: r#"message-content"#.into(),
188            ready_selector: r#"rich-textarea"#.into(),
189            file_input_selector: Some(r#"input[type="file"]"#.into()),
190            google_account: None,
191        }
192    }
193}
194
195/// ChatGPT (OpenAI) configuration.
196#[cfg(feature = "chatgpt")]
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct ChatGptConfig {
199    /// Login URL.
200    pub login_url: String,
201    /// Chat URL.
202    pub chat_url: String,
203    /// CSS selector for input field.
204    pub input_selector: String,
205    /// CSS selector for submit button.
206    pub submit_selector: String,
207    /// CSS selector for response container.
208    pub response_selector: String,
209    /// CSS selector to wait for page ready.
210    pub ready_selector: String,
211    /// CSS selector for file input.
212    pub file_input_selector: Option<String>,
213    /// Model to use (gpt-4o, gpt-4, etc).
214    pub model: String,
215}
216
217#[cfg(feature = "chatgpt")]
218impl Default for ChatGptConfig {
219    fn default() -> Self {
220        Self {
221            login_url: "https://chat.openai.com".into(),
222            chat_url: "https://chat.openai.com".into(),
223            input_selector: r#"textarea[data-id="root"]"#.into(),
224            submit_selector: r#"button[data-testid="send-button"]"#.into(),
225            response_selector: r#"div[data-message-author-role="assistant"]"#.into(),
226            ready_selector: r#"textarea[data-id="root"]"#.into(),
227            file_input_selector: Some(r#"input[type="file"]"#.into()),
228            model: "gpt-4o".into(),
229        }
230    }
231}
232
233/// Perplexity AI configuration.
234#[cfg(feature = "perplexity")]
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct PerplexityConfig {
237    /// Login URL.
238    pub login_url: String,
239    /// Chat URL.
240    pub chat_url: String,
241    /// CSS selector for input field.
242    pub input_selector: String,
243    /// CSS selector for submit button.
244    pub submit_selector: String,
245    /// CSS selector for response container.
246    pub response_selector: String,
247    /// CSS selector to wait for page ready.
248    pub ready_selector: String,
249    /// CSS selector for file input.
250    pub file_input_selector: Option<String>,
251}
252
253#[cfg(feature = "perplexity")]
254impl Default for PerplexityConfig {
255    fn default() -> Self {
256        Self {
257            login_url: "https://www.perplexity.ai".into(),
258            chat_url: "https://www.perplexity.ai".into(),
259            input_selector: r#"textarea[placeholder*="Ask"]"#.into(),
260            submit_selector: r#"button[aria-label="Submit query"]"#.into(),
261            response_selector: r#"div.prose"#.into(),
262            ready_selector: r#"textarea[placeholder*="Ask"]"#.into(),
263            file_input_selector: Some(r#"input[type="file"]"#.into()),
264        }
265    }
266}
267
268/// NotebookLM (Google) configuration.
269#[cfg(feature = "notebooklm")]
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct NotebookLmConfig {
272    /// Login URL.
273    pub login_url: String,
274    /// Chat URL.
275    pub chat_url: String,
276    /// CSS selector for input field.
277    pub input_selector: String,
278    /// CSS selector for submit button.
279    pub submit_selector: String,
280    /// CSS selector for response container.
281    pub response_selector: String,
282    /// CSS selector to wait for page ready.
283    pub ready_selector: String,
284    /// CSS selector for file input.
285    pub file_input_selector: Option<String>,
286}
287
288#[cfg(feature = "notebooklm")]
289impl Default for NotebookLmConfig {
290    fn default() -> Self {
291        Self {
292            login_url: "https://notebooklm.google.com".into(),
293            chat_url: "https://notebooklm.google.com".into(),
294            input_selector: r#"textarea[aria-label*="Ask"]"#.into(),
295            submit_selector: r#"button[aria-label="Send"]"#.into(),
296            response_selector: r#"div.response-content"#.into(),
297            ready_selector: r#"textarea[aria-label*="Ask"]"#.into(),
298            file_input_selector: Some(r#"button[aria-label="Add source"]"#.into()),
299        }
300    }
301}
302
303/// Session management configuration.
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct SessionConfig {
306    /// Directory for session storage.
307    pub storage_dir: Option<PathBuf>,
308    /// Session timeout before re-auth.
309    #[serde(with = "humantime_serde")]
310    pub timeout: Duration,
311    /// Keep cookies between sessions.
312    pub persist_cookies: bool,
313    /// Encrypt stored session data.
314    pub encrypt_storage: bool,
315}
316
317impl Default for SessionConfig {
318    fn default() -> Self {
319        Self {
320            storage_dir: None,
321            timeout: Duration::from_secs(3600 * 24), // 24 hours
322            persist_cookies: true,
323            encrypt_storage: true,
324        }
325    }
326}
327
328/// Rate limiting configuration.
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct RateLimitConfig {
331    /// Minimum delay between requests.
332    #[serde(with = "humantime_serde")]
333    pub min_delay: Duration,
334    /// Maximum delay between requests.
335    #[serde(with = "humantime_serde")]
336    pub max_delay: Duration,
337    /// Requests per minute limit.
338    pub requests_per_minute: u32,
339    /// Add human-like delays.
340    pub humanize: bool,
341    /// Jitter percentage for delays (0-100).
342    pub jitter_percent: u8,
343}
344
345impl Default for RateLimitConfig {
346    fn default() -> Self {
347        Self {
348            min_delay: Duration::from_secs(2),
349            max_delay: Duration::from_secs(10),
350            requests_per_minute: 20,
351            humanize: true,
352            jitter_percent: 30,
353        }
354    }
355}
356
357impl Config {
358    /// Load configuration from file.
359    pub fn from_file(path: &std::path::Path) -> crate::Result<Self> {
360        let content = std::fs::read_to_string(path)?;
361        toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
362    }
363
364    /// Save configuration to file.
365    pub fn save(&self, path: &std::path::Path) -> crate::Result<()> {
366        let content =
367            toml::to_string_pretty(self).map_err(|e| crate::Error::Config(e.to_string()))?;
368        std::fs::write(path, content)?;
369        Ok(())
370    }
371
372    /// Create a builder for configuration.
373    pub fn builder() -> ConfigBuilder {
374        ConfigBuilder::default()
375    }
376}
377
378/// Builder for Config.
379#[derive(Debug, Default)]
380pub struct ConfigBuilder {
381    config: Config,
382}
383
384impl ConfigBuilder {
385    /// Set headless mode.
386    pub fn headless(mut self, headless: bool) -> Self {
387        self.config.browser.headless = headless;
388        self
389    }
390
391    /// Set browser executable path.
392    pub fn executable_path(mut self, path: PathBuf) -> Self {
393        self.config.browser.executable_path = Some(path);
394        self
395    }
396
397    /// Set user data directory.
398    pub fn user_data_dir(mut self, path: PathBuf) -> Self {
399        self.config.browser.user_data_dir = Some(path);
400        self
401    }
402
403    /// Set request timeout.
404    pub fn timeout(mut self, timeout: Duration) -> Self {
405        self.config.browser.timeout = timeout;
406        self
407    }
408
409    /// Enable devtools.
410    pub fn devtools(mut self, enabled: bool) -> Self {
411        self.config.browser.devtools = enabled;
412        self
413    }
414
415    /// Disable sandbox (for containers).
416    pub fn no_sandbox(mut self) -> Self {
417        self.config.browser.sandbox = false;
418        self.config.browser.args.push("--no-sandbox".into());
419        self
420    }
421
422    /// Set session storage directory.
423    pub fn session_dir(mut self, path: PathBuf) -> Self {
424        self.config.session.storage_dir = Some(path);
425        self
426    }
427
428    /// Set rate limit.
429    pub fn rate_limit(mut self, requests_per_minute: u32) -> Self {
430        self.config.rate_limit.requests_per_minute = requests_per_minute;
431        self
432    }
433
434    /// Build the configuration.
435    pub fn build(self) -> Config {
436        self.config
437    }
438}