1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::time::Duration;
6
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
9pub struct Config {
10 pub browser: BrowserConfig,
12 pub providers: ProvidersConfig,
14 pub session: SessionConfig,
16 pub rate_limit: RateLimitConfig,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct BrowserConfig {
23 pub headless: bool,
25 pub executable_path: Option<PathBuf>,
27 pub user_data_dir: Option<PathBuf>,
29 pub window_width: u32,
31 pub window_height: u32,
33 pub args: Vec<String>,
35 #[serde(with = "humantime_serde")]
37 pub timeout: Duration,
38 pub devtools: bool,
40 pub sandbox: bool,
42 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
69pub struct ProvidersConfig {
70 #[cfg(feature = "grok")]
72 pub grok: GrokConfig,
73 #[cfg(feature = "claude")]
75 pub claude: ClaudeConfig,
76 #[cfg(feature = "gemini")]
78 pub gemini: GeminiConfig,
79}
80
81#[cfg(feature = "grok")]
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct GrokConfig {
85 pub login_url: String,
87 pub chat_url: String,
89 pub input_selector: String,
91 pub submit_selector: String,
93 pub response_selector: String,
95 pub ready_selector: String,
97 pub file_input_selector: Option<String>,
99 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#[cfg(feature = "claude")]
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ClaudeConfig {
123 pub login_url: String,
125 pub chat_url: String,
127 pub input_selector: String,
129 pub submit_selector: String,
131 pub response_selector: String,
133 pub ready_selector: String,
135 pub file_input_selector: Option<String>,
137 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#[cfg(feature = "gemini")]
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct GeminiConfig {
161 pub login_url: String,
163 pub chat_url: String,
165 pub input_selector: String,
167 pub submit_selector: String,
169 pub response_selector: String,
171 pub ready_selector: String,
173 pub file_input_selector: Option<String>,
175 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#[cfg(feature = "chatgpt")]
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct ChatGptConfig {
199 pub login_url: String,
201 pub chat_url: String,
203 pub input_selector: String,
205 pub submit_selector: String,
207 pub response_selector: String,
209 pub ready_selector: String,
211 pub file_input_selector: Option<String>,
213 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#[cfg(feature = "perplexity")]
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct PerplexityConfig {
237 pub login_url: String,
239 pub chat_url: String,
241 pub input_selector: String,
243 pub submit_selector: String,
245 pub response_selector: String,
247 pub ready_selector: String,
249 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#[cfg(feature = "notebooklm")]
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct NotebookLmConfig {
272 pub login_url: String,
274 pub chat_url: String,
276 pub input_selector: String,
278 pub submit_selector: String,
280 pub response_selector: String,
282 pub ready_selector: String,
284 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#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct SessionConfig {
306 pub storage_dir: Option<PathBuf>,
308 #[serde(with = "humantime_serde")]
310 pub timeout: Duration,
311 pub persist_cookies: bool,
313 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), persist_cookies: true,
323 encrypt_storage: true,
324 }
325 }
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct RateLimitConfig {
331 #[serde(with = "humantime_serde")]
333 pub min_delay: Duration,
334 #[serde(with = "humantime_serde")]
336 pub max_delay: Duration,
337 pub requests_per_minute: u32,
339 pub humanize: bool,
341 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 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 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 pub fn builder() -> ConfigBuilder {
374 ConfigBuilder::default()
375 }
376}
377
378#[derive(Debug, Default)]
380pub struct ConfigBuilder {
381 config: Config,
382}
383
384impl ConfigBuilder {
385 pub fn headless(mut self, headless: bool) -> Self {
387 self.config.browser.headless = headless;
388 self
389 }
390
391 pub fn executable_path(mut self, path: PathBuf) -> Self {
393 self.config.browser.executable_path = Some(path);
394 self
395 }
396
397 pub fn user_data_dir(mut self, path: PathBuf) -> Self {
399 self.config.browser.user_data_dir = Some(path);
400 self
401 }
402
403 pub fn timeout(mut self, timeout: Duration) -> Self {
405 self.config.browser.timeout = timeout;
406 self
407 }
408
409 pub fn devtools(mut self, enabled: bool) -> Self {
411 self.config.browser.devtools = enabled;
412 self
413 }
414
415 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 pub fn session_dir(mut self, path: PathBuf) -> Self {
424 self.config.session.storage_dir = Some(path);
425 self
426 }
427
428 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 pub fn build(self) -> Config {
436 self.config
437 }
438}