use crate::enums::{gemini_headers, rotate_cookies_headers, Endpoint, Model};
use crate::error::{Error, Result};
use crate::utils::upload_file;
use rand::Rng;
use regex::Regex;
use reqwest::cookie::Jar;
use reqwest::{Client, Url};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
const SNLM0E_PATTERN: &str = r#"["']SNlM0e["']\s*:\s*["']([^"']+)["']"#;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatResponse {
pub content: String,
pub conversation_id: String,
pub response_id: String,
pub factuality_queries: Option<Value>,
pub text_query: String,
pub choices: Vec<Choice>,
pub error: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Choice {
pub id: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedConversation {
pub conversation_name: String,
#[serde(rename = "_reqid")]
pub reqid: u32,
pub conversation_id: String,
pub response_id: String,
pub choice_id: String,
#[serde(rename = "SNlM0e")]
pub snlm0e: String,
pub model_name: String,
pub timestamp: String,
}
pub struct AsyncChatbot {
client: Client,
snlm0e: String,
conversation_id: String,
response_id: String,
choice_id: String,
reqid: u32,
secure_1psidts: String,
model: Model,
proxy: Option<String>,
}
impl AsyncChatbot {
pub async fn new(
secure_1psid: &str,
secure_1psidts: &str,
model: Model,
proxy: Option<&str>,
timeout: u64,
) -> Result<Self> {
if secure_1psid.is_empty() {
return Err(Error::Authentication(
"__Secure-1PSID cookie is required".to_string(),
));
}
let jar = Jar::default();
let url: Url = "https://gemini.google.com".parse().unwrap();
jar.add_cookie_str(
&format!(
"__Secure-1PSID={}; Domain=.google.com; Path=/; Secure; SameSite=None",
secure_1psid
),
&url,
);
jar.add_cookie_str(
&format!(
"__Secure-1PSIDTS={}; Domain=.google.com; Path=/; Secure; SameSite=None",
secure_1psidts
),
&url,
);
let mut headers = gemini_headers();
if let Some(model_headers) = model.headers() {
headers.extend(model_headers);
}
let mut builder = Client::builder()
.cookie_provider(Arc::new(jar))
.default_headers(headers)
.timeout(Duration::from_secs(timeout));
if let Some(proxy_url) = proxy {
builder = builder.proxy(reqwest::Proxy::all(proxy_url)?);
}
let client = builder.build()?;
let mut chatbot = Self {
client,
snlm0e: String::new(),
conversation_id: String::new(),
response_id: String::new(),
choice_id: String::new(),
reqid: rand::thread_rng().gen_range(1000000..9999999),
secure_1psidts: secure_1psidts.to_string(),
model,
proxy: proxy.map(|s| s.to_string()),
};
chatbot.snlm0e = chatbot.get_snlm0e().await?;
Ok(chatbot)
}
async fn get_snlm0e(&mut self) -> Result<String> {
if self.secure_1psidts.is_empty() {
let _ = self.rotate_cookies().await;
}
let response = self.client.get(Endpoint::Init.url()).send().await?;
let status = response.status();
let text = response.text().await?;
if !status.is_success() {
if status.as_u16() == 401 || status.as_u16() == 403 {
return Err(Error::Authentication(format!(
"Authentication failed (status {}). Check cookies.",
status
)));
}
return Err(Error::Parse(format!("HTTP error: {}", status)));
}
if text.contains("\"identifier-shown\"")
|| text.contains("SignIn?continue")
|| text.contains("Sign in - Google Accounts")
{
return Err(Error::Authentication(
"Authentication failed. Cookies might be invalid or expired.".to_string(),
));
}
let re = Regex::new(SNLM0E_PATTERN).unwrap();
match re.captures(&text) {
Some(caps) => Ok(caps.get(1).unwrap().as_str().to_string()),
None => {
if text.contains("429") {
Err(Error::Parse(
"SNlM0e not found. Rate limit likely exceeded.".to_string(),
))
} else {
Err(Error::Parse(
"SNlM0e value not found in response. Check cookie validity.".to_string(),
))
}
}
}
}
async fn rotate_cookies(&mut self) -> Result<Option<String>> {
let response = self
.client
.post(Endpoint::RotateCookies.url())
.headers(rotate_cookies_headers())
.body(r#"[000,"-0000000000000000000"]"#)
.send()
.await?;
if !response.status().is_success() {
return Ok(None);
}
for cookie in response.cookies() {
if cookie.name() == "__Secure-1PSIDTS" {
let new_value = cookie.value().to_string();
self.secure_1psidts = new_value.clone();
return Ok(Some(new_value));
}
}
Ok(None)
}
pub async fn ask(&mut self, message: &str, image: Option<&[u8]>) -> Result<ChatResponse> {
if self.snlm0e.is_empty() {
return Err(Error::NotInitialized(
"AsyncChatbot not properly initialized. SNlM0e is missing.".to_string(),
));
}
let image_upload_id = if let Some(img_data) = image {
Some(upload_file(img_data, self.proxy.as_deref()).await?)
} else {
None
};
let message_struct: Value = if let Some(ref upload_id) = image_upload_id {
serde_json::json!([
[message],
[[[upload_id, 1]]],
[&self.conversation_id, &self.response_id, &self.choice_id]
])
} else {
serde_json::json!([
[message],
null,
[&self.conversation_id, &self.response_id, &self.choice_id]
])
};
let freq_value = serde_json::json!([null, serde_json::to_string(&message_struct)?]);
let params = [
("bl", "boq_assistant-bard-web-server_20240625.13_p0"),
("_reqid", &self.reqid.to_string()),
("rt", "c"),
];
let form_data = [
("f.req", serde_json::to_string(&freq_value)?),
("at", self.snlm0e.clone()),
];
let response = self
.client
.post(Endpoint::Generate.url())
.query(¶ms)
.form(&form_data)
.send()
.await?;
if !response.status().is_success() {
return Err(Error::Network(response.error_for_status().unwrap_err()));
}
let text = response.text().await?;
self.parse_response(&text)
}
fn parse_response(&mut self, text: &str) -> Result<ChatResponse> {
let lines: Vec<&str> = text.lines().collect();
if lines.len() < 3 {
return Err(Error::Parse(format!(
"Unexpected response format. Content: {}...",
&text[..text.len().min(200)]
)));
}
let mut body: Option<Value> = None;
for line in &lines {
if line.is_empty() || *line == ")]}" {
continue;
}
let mut clean_line = *line;
if clean_line.starts_with(")]}") {
clean_line = clean_line.get(4..).unwrap_or("").trim();
}
if !clean_line.starts_with('[') {
continue;
}
if let Ok(response_json) = serde_json::from_str::<Value>(clean_line) {
if let Some(arr) = response_json.as_array() {
for part in arr {
if let Some(part_arr) = part.as_array() {
if part_arr.len() > 2
&& part_arr.first().and_then(|v| v.as_str()) == Some("wrb.fr")
{
if let Some(inner_str) = part_arr.get(2).and_then(|v| v.as_str()) {
if let Ok(main_part) = serde_json::from_str::<Value>(inner_str)
{
if main_part
.as_array()
.map(|a| a.len() > 4 && !a[4].is_null())
.unwrap_or(false)
{
body = Some(main_part);
break;
}
}
}
}
}
}
}
if body.is_some() {
break;
}
}
}
let body = body.ok_or_else(|| {
Error::Parse("Failed to parse response body. No valid data found.".to_string())
})?;
let body_arr = body.as_array().unwrap();
let content = body_arr
.get(4)
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let conversation_id = body_arr
.get(1)
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|v| v.as_str())
.unwrap_or(&self.conversation_id)
.to_string();
let response_id = body_arr
.get(1)
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_str())
.unwrap_or(&self.response_id)
.to_string();
let factuality_queries = body_arr.get(3).cloned();
let text_query = body_arr
.get(2)
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let mut choices = Vec::new();
if let Some(candidates) = body_arr.get(4).and_then(|v| v.as_array()) {
for candidate in candidates {
if let Some(cand_arr) = candidate.as_array() {
if cand_arr.len() > 1 {
let id = cand_arr
.first()
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let choice_content = cand_arr
.get(1)
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
choices.push(Choice {
id,
content: choice_content,
});
}
}
}
}
let choice_id = choices
.first()
.map(|c| c.id.clone())
.unwrap_or_else(|| self.choice_id.clone());
self.conversation_id = conversation_id.clone();
self.response_id = response_id.clone();
self.choice_id = choice_id;
self.reqid += rand::thread_rng().gen_range(1000..9000);
Ok(ChatResponse {
content,
conversation_id,
response_id,
factuality_queries,
text_query,
choices,
error: false,
})
}
pub async fn save_conversation(&self, file_path: &str, conversation_name: &str) -> Result<()> {
let mut conversations = self.load_conversations(file_path).await?;
let conversation_data = SavedConversation {
conversation_name: conversation_name.to_string(),
reqid: self.reqid,
conversation_id: self.conversation_id.clone(),
response_id: self.response_id.clone(),
choice_id: self.choice_id.clone(),
snlm0e: self.snlm0e.clone(),
model_name: self.model.name().to_string(),
timestamp: chrono_now(),
};
let mut found = false;
for conv in &mut conversations {
if conv.conversation_name == conversation_name {
*conv = conversation_data.clone();
found = true;
break;
}
}
if !found {
conversations.push(conversation_data);
}
if let Some(parent) = Path::new(file_path).parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(&conversations)?;
std::fs::write(file_path, json)?;
Ok(())
}
pub async fn load_conversations(&self, file_path: &str) -> Result<Vec<SavedConversation>> {
if !Path::new(file_path).exists() {
return Ok(Vec::new());
}
let content = std::fs::read_to_string(file_path)?;
let conversations: Vec<SavedConversation> = serde_json::from_str(&content)?;
Ok(conversations)
}
pub async fn load_conversation(
&mut self,
file_path: &str,
conversation_name: &str,
) -> Result<bool> {
let conversations = self.load_conversations(file_path).await?;
for conv in conversations {
if conv.conversation_name == conversation_name {
self.reqid = conv.reqid;
self.conversation_id = conv.conversation_id;
self.response_id = conv.response_id;
self.choice_id = conv.choice_id;
self.snlm0e = conv.snlm0e;
if let Some(model) = Model::from_name(&conv.model_name) {
self.model = model;
}
return Ok(true);
}
}
Ok(false)
}
pub fn conversation_id(&self) -> &str {
&self.conversation_id
}
pub fn model(&self) -> &Model {
&self.model
}
pub fn reset(&mut self) {
self.conversation_id.clear();
self.response_id.clear();
self.choice_id.clear();
self.reqid = rand::thread_rng().gen_range(1000000..9999999);
}
}
fn chrono_now() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
format!("{}", duration.as_secs())
}