Skip to main content

gemini_chat_api/
utils.rs

1//! Utility functions for cookie loading and file upload.
2
3use crate::enums::{upload_headers, Endpoint};
4use crate::error::{Error, Result};
5use reqwest::Client;
6use serde::Deserialize;
7use std::collections::HashMap;
8use std::path::Path;
9
10/// Cookie entry from browser export JSON format.
11#[derive(Debug, Deserialize)]
12struct CookieEntry {
13    name: String,
14    value: String,
15}
16
17/// Loads authentication cookies from a JSON file.
18///
19/// The file should be in the browser cookie export format:
20/// ```json
21/// [
22///   { "name": "__Secure-1PSID", "value": "..." },
23///   { "name": "__Secure-1PSIDTS", "value": "..." }
24/// ]
25/// ```
26///
27/// Cookie names are matched case-insensitively.
28///
29/// # Arguments
30/// * `cookie_path` - Path to the JSON cookie file
31///
32/// # Returns
33/// A tuple of (secure_1psid, secure_1psidts) values
34///
35/// # Errors
36/// Returns an error if the file is not found, invalid JSON, or missing required cookies.
37pub fn load_cookies(cookie_path: &str) -> Result<(String, String)> {
38    let path = Path::new(cookie_path);
39    if !path.exists() {
40        return Err(Error::Cookie(format!(
41            "Cookie file not found at path: {}",
42            cookie_path
43        )));
44    }
45
46    let content = std::fs::read_to_string(path)?;
47    let cookies: Vec<CookieEntry> = serde_json::from_str(&content)
48        .map_err(|e| Error::Cookie(format!("Invalid JSON format in cookie file: {}", e)))?;
49
50    let mut secure_1psid: Option<String> = None;
51    let mut secure_1psidts: Option<String> = None;
52
53    for cookie in cookies {
54        match cookie.name.to_uppercase().as_str() {
55            "__SECURE-1PSID" => secure_1psid = Some(cookie.value),
56            "__SECURE-1PSIDTS" => secure_1psidts = Some(cookie.value),
57            _ => {}
58        }
59    }
60
61    match (secure_1psid, secure_1psidts) {
62        (Some(psid), Some(psidts)) => Ok((psid, psidts)),
63        (None, _) => Err(Error::Cookie(
64            "Required cookie __Secure-1PSID not found".to_string(),
65        )),
66        (_, None) => Err(Error::Cookie(
67            "Required cookie __Secure-1PSIDTS not found".to_string(),
68        )),
69    }
70}
71
72/// Uploads a file to the Gemini upload endpoint and returns the raw response text.
73///
74/// # Arguments
75/// * `file_data` - The file content as bytes
76/// * `proxy` - Optional proxy URL
77///
78/// # Returns
79/// The file identifier string from the server
80///
81/// # Errors
82/// Returns `Error::Upload` if the request fails, the response status is not
83/// successful, or the response body cannot be read.
84///
85/// There is no retry or polling behavior. External failures (upload endpoint
86/// availability or connectivity issues) are returned as errors.
87pub async fn upload_file(file_data: &[u8], proxy: Option<&str>) -> Result<String> {
88    let mut builder = Client::builder();
89
90    if let Some(proxy_url) = proxy {
91        builder = builder
92            .proxy(reqwest::Proxy::all(proxy_url).map_err(|e| Error::Upload(e.to_string()))?);
93    }
94
95    let client = builder.build().map_err(|e| Error::Upload(e.to_string()))?;
96
97    // Create multipart form with the file
98    let part = reqwest::multipart::Part::bytes(file_data.to_vec()).file_name("file");
99    let form = reqwest::multipart::Form::new().part("file", part);
100
101    let response: reqwest::Response = client
102        .post(Endpoint::Upload.url())
103        .headers(upload_headers())
104        .multipart(form)
105        .send()
106        .await
107        .map_err(|e| Error::Upload(e.to_string()))?;
108
109    if !response.status().is_success() {
110        return Err(Error::Upload(format!(
111            "Upload failed with status: {}",
112            response.status()
113        )));
114    }
115
116    let text = response
117        .text()
118        .await
119        .map_err(|e| Error::Upload(e.to_string()))?;
120    Ok(text)
121}
122
123/// Returns a simple cookie map suitable for manual `reqwest` usage.
124///
125/// This helper does not perform any I/O and does not validate the values.
126pub fn cookies_to_map(secure_1psid: &str, secure_1psidts: &str) -> HashMap<String, String> {
127    let mut map = HashMap::new();
128    map.insert("__Secure-1PSID".to_string(), secure_1psid.to_string());
129    map.insert("__Secure-1PSIDTS".to_string(), secure_1psidts.to_string());
130    map
131}