use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
pub browser: BrowserConfig,
pub providers: ProvidersConfig,
pub session: SessionConfig,
pub rate_limit: RateLimitConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserConfig {
pub headless: bool,
pub executable_path: Option<PathBuf>,
pub user_data_dir: Option<PathBuf>,
pub window_width: u32,
pub window_height: u32,
pub args: Vec<String>,
#[serde(with = "humantime_serde")]
pub timeout: Duration,
pub devtools: bool,
pub sandbox: bool,
pub dual_head: bool,
}
impl Default for BrowserConfig {
fn default() -> Self {
Self {
headless: true,
executable_path: None,
user_data_dir: None,
window_width: 1920,
window_height: 1080,
args: vec![
"--disable-gpu".into(),
"--disable-dev-shm-usage".into(),
"--no-first-run".into(),
],
timeout: Duration::from_secs(60),
devtools: false,
sandbox: true,
dual_head: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProvidersConfig {
#[cfg(feature = "grok")]
pub grok: GrokConfig,
#[cfg(feature = "claude")]
pub claude: ClaudeConfig,
#[cfg(feature = "gemini")]
pub gemini: GeminiConfig,
}
#[cfg(feature = "grok")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrokConfig {
pub login_url: String,
pub chat_url: String,
pub input_selector: String,
pub submit_selector: String,
pub response_selector: String,
pub ready_selector: String,
pub file_input_selector: Option<String>,
pub model: String,
}
#[cfg(feature = "grok")]
impl Default for GrokConfig {
fn default() -> Self {
Self {
login_url: "https://x.com/i/grok".into(),
chat_url: "https://x.com/i/grok".into(),
input_selector: r#"textarea[data-testid="grokInput"]"#.into(),
submit_selector: r#"button[data-testid="grokSend"]"#.into(),
response_selector: r#"div[data-testid="grokResponse"]"#.into(),
ready_selector: r#"textarea[data-testid="grokInput"]"#.into(),
file_input_selector: Some(r#"input[type="file"]"#.into()),
model: "grok-2".into(),
}
}
}
#[cfg(feature = "claude")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeConfig {
pub login_url: String,
pub chat_url: String,
pub input_selector: String,
pub submit_selector: String,
pub response_selector: String,
pub ready_selector: String,
pub file_input_selector: Option<String>,
pub organization: Option<String>,
}
#[cfg(feature = "claude")]
impl Default for ClaudeConfig {
fn default() -> Self {
Self {
login_url: "https://claude.ai/login".into(),
chat_url: "https://claude.ai/new".into(),
input_selector: r#"div[contenteditable="true"]"#.into(),
submit_selector: r#"button[aria-label="Send message"]"#.into(),
response_selector: r#"div.prose"#.into(),
ready_selector: r#"div[contenteditable="true"]"#.into(),
file_input_selector: Some(r#"input[type="file"]"#.into()),
organization: None,
}
}
}
#[cfg(feature = "gemini")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeminiConfig {
pub login_url: String,
pub chat_url: String,
pub input_selector: String,
pub submit_selector: String,
pub response_selector: String,
pub ready_selector: String,
pub file_input_selector: Option<String>,
pub google_account: Option<String>,
}
#[cfg(feature = "gemini")]
impl Default for GeminiConfig {
fn default() -> Self {
Self {
login_url: "https://gemini.google.com".into(),
chat_url: "https://gemini.google.com/app".into(),
input_selector: r#"rich-textarea"#.into(),
submit_selector: r#"button[aria-label="Send message"]"#.into(),
response_selector: r#"message-content"#.into(),
ready_selector: r#"rich-textarea"#.into(),
file_input_selector: Some(r#"input[type="file"]"#.into()),
google_account: None,
}
}
}
#[cfg(feature = "chatgpt")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatGptConfig {
pub login_url: String,
pub chat_url: String,
pub input_selector: String,
pub submit_selector: String,
pub response_selector: String,
pub ready_selector: String,
pub file_input_selector: Option<String>,
pub model: String,
}
#[cfg(feature = "chatgpt")]
impl Default for ChatGptConfig {
fn default() -> Self {
Self {
login_url: "https://chat.openai.com".into(),
chat_url: "https://chat.openai.com".into(),
input_selector: r#"textarea[data-id="root"]"#.into(),
submit_selector: r#"button[data-testid="send-button"]"#.into(),
response_selector: r#"div[data-message-author-role="assistant"]"#.into(),
ready_selector: r#"textarea[data-id="root"]"#.into(),
file_input_selector: Some(r#"input[type="file"]"#.into()),
model: "gpt-4o".into(),
}
}
}
#[cfg(feature = "perplexity")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerplexityConfig {
pub login_url: String,
pub chat_url: String,
pub input_selector: String,
pub submit_selector: String,
pub response_selector: String,
pub ready_selector: String,
pub file_input_selector: Option<String>,
}
#[cfg(feature = "perplexity")]
impl Default for PerplexityConfig {
fn default() -> Self {
Self {
login_url: "https://www.perplexity.ai".into(),
chat_url: "https://www.perplexity.ai".into(),
input_selector: r#"textarea[placeholder*="Ask"]"#.into(),
submit_selector: r#"button[aria-label="Submit query"]"#.into(),
response_selector: r#"div.prose"#.into(),
ready_selector: r#"textarea[placeholder*="Ask"]"#.into(),
file_input_selector: Some(r#"input[type="file"]"#.into()),
}
}
}
#[cfg(feature = "notebooklm")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotebookLmConfig {
pub login_url: String,
pub chat_url: String,
pub input_selector: String,
pub submit_selector: String,
pub response_selector: String,
pub ready_selector: String,
pub file_input_selector: Option<String>,
}
#[cfg(feature = "notebooklm")]
impl Default for NotebookLmConfig {
fn default() -> Self {
Self {
login_url: "https://notebooklm.google.com".into(),
chat_url: "https://notebooklm.google.com".into(),
input_selector: r#"textarea[aria-label*="Ask"]"#.into(),
submit_selector: r#"button[aria-label="Send"]"#.into(),
response_selector: r#"div.response-content"#.into(),
ready_selector: r#"textarea[aria-label*="Ask"]"#.into(),
file_input_selector: Some(r#"button[aria-label="Add source"]"#.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionConfig {
pub storage_dir: Option<PathBuf>,
#[serde(with = "humantime_serde")]
pub timeout: Duration,
pub persist_cookies: bool,
pub encrypt_storage: bool,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
storage_dir: None,
timeout: Duration::from_secs(3600 * 24), persist_cookies: true,
encrypt_storage: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
#[serde(with = "humantime_serde")]
pub min_delay: Duration,
#[serde(with = "humantime_serde")]
pub max_delay: Duration,
pub requests_per_minute: u32,
pub humanize: bool,
pub jitter_percent: u8,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
min_delay: Duration::from_secs(2),
max_delay: Duration::from_secs(10),
requests_per_minute: 20,
humanize: true,
jitter_percent: 30,
}
}
}
impl Config {
pub fn from_file(path: &std::path::Path) -> crate::Result<Self> {
let content = std::fs::read_to_string(path)?;
toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
}
pub fn save(&self, path: &std::path::Path) -> crate::Result<()> {
let content =
toml::to_string_pretty(self).map_err(|e| crate::Error::Config(e.to_string()))?;
std::fs::write(path, content)?;
Ok(())
}
pub fn builder() -> ConfigBuilder {
ConfigBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
pub fn headless(mut self, headless: bool) -> Self {
self.config.browser.headless = headless;
self
}
pub fn executable_path(mut self, path: PathBuf) -> Self {
self.config.browser.executable_path = Some(path);
self
}
pub fn user_data_dir(mut self, path: PathBuf) -> Self {
self.config.browser.user_data_dir = Some(path);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.browser.timeout = timeout;
self
}
pub fn devtools(mut self, enabled: bool) -> Self {
self.config.browser.devtools = enabled;
self
}
pub fn no_sandbox(mut self) -> Self {
self.config.browser.sandbox = false;
self.config.browser.args.push("--no-sandbox".into());
self
}
pub fn session_dir(mut self, path: PathBuf) -> Self {
self.config.session.storage_dir = Some(path);
self
}
pub fn rate_limit(mut self, requests_per_minute: u32) -> Self {
self.config.rate_limit.requests_per_minute = requests_per_minute;
self
}
pub fn build(self) -> Config {
self.config
}
}