use crate::models::{ChatMessage, ChatRequest, ChatSession};
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default)]
pub struct FetchOptions {
pub limit: Option<usize>,
pub after: Option<DateTime<Utc>>,
pub before: Option<DateTime<Utc>>,
pub include_archived: bool,
pub session_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudConversation {
pub id: String,
pub title: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>,
pub model: Option<String>,
pub messages: Vec<CloudMessage>,
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudMessage {
pub id: Option<String>,
pub role: String,
pub content: String,
pub timestamp: Option<DateTime<Utc>>,
pub model: Option<String>,
}
impl CloudConversation {
pub fn to_chat_session(&self, provider_name: &str) -> ChatSession {
use uuid::Uuid;
let mut requests = Vec::new();
let mut i = 0;
while i < self.messages.len() {
let msg = &self.messages[i];
if msg.role == "user" {
let mut request = ChatRequest {
timestamp: msg.timestamp.map(|t| t.timestamp_millis()),
message: Some(ChatMessage {
text: Some(msg.content.clone()),
parts: None,
}),
response: None,
variable_data: None,
request_id: Some(msg.id.clone().unwrap_or_else(|| Uuid::new_v4().to_string())),
response_id: None,
model_id: self.model.clone(),
agent: None,
result: None,
followups: None,
is_canceled: Some(false),
content_references: None,
code_citations: None,
response_markdown_info: None,
source_session: Some(format!("{}:{}", provider_name, self.id)),
model_state: None,
time_spent_waiting: None,
};
if i + 1 < self.messages.len() && self.messages[i + 1].role == "assistant" {
let assistant_msg = &self.messages[i + 1];
request.response = Some(serde_json::json!({
"text": assistant_msg.content,
"result": {
"metadata": {
"provider": provider_name,
"model": assistant_msg.model.clone().or_else(|| self.model.clone())
}
}
}));
request.response_id = Some(
assistant_msg
.id
.clone()
.unwrap_or_else(|| Uuid::new_v4().to_string()),
);
i += 1;
}
requests.push(request);
} else if msg.role == "system" {
}
i += 1;
}
ChatSession {
version: 3,
session_id: Some(format!("{}:{}", provider_name, self.id)),
creation_date: self.created_at.timestamp_millis(),
last_message_date: self
.updated_at
.unwrap_or(self.created_at)
.timestamp_millis(),
is_imported: true,
initial_location: "panel".to_string(),
custom_title: self.title.clone(),
requester_username: None,
requester_avatar_icon_uri: None,
responder_username: Some(provider_name.to_string()),
responder_avatar_icon_uri: None,
requests,
}
}
}
pub trait CloudProvider: Send + Sync {
fn name(&self) -> &'static str;
fn api_base_url(&self) -> &str;
fn is_authenticated(&self) -> bool;
fn set_credentials(&mut self, api_key: Option<String>, session_token: Option<String>);
fn list_conversations(&self, options: &FetchOptions) -> Result<Vec<CloudConversation>>;
fn fetch_conversation(&self, id: &str) -> Result<CloudConversation>;
fn fetch_all_conversations(&self, options: &FetchOptions) -> Result<Vec<ChatSession>> {
let conversations = self.list_conversations(options)?;
let mut sessions = Vec::new();
for conv in conversations {
if !conv.messages.is_empty() {
sessions.push(conv.to_chat_session(self.name()));
} else {
match self.fetch_conversation(&conv.id) {
Ok(full_conv) => sessions.push(full_conv.to_chat_session(self.name())),
Err(e) => {
eprintln!("Warning: Failed to fetch conversation {}: {}", conv.id, e);
}
}
}
}
Ok(sessions)
}
fn api_key_env_var(&self) -> &'static str;
fn load_api_key_from_env(&self) -> Option<String> {
std::env::var(self.api_key_env_var()).ok()
}
}
#[derive(Debug, Clone)]
pub struct HttpClientConfig {
pub timeout_secs: u64,
pub user_agent: String,
pub accept_invalid_certs: bool,
}
impl Default for HttpClientConfig {
fn default() -> Self {
Self {
timeout_secs: 30,
user_agent: format!("csm/{}", env!("CARGO_PKG_VERSION")),
accept_invalid_certs: false,
}
}
}
pub fn build_http_client(config: &HttpClientConfig) -> Result<reqwest::blocking::Client> {
use std::time::Duration;
reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(config.timeout_secs))
.user_agent(&config.user_agent)
.danger_accept_invalid_certs(config.accept_invalid_certs)
.build()
.map_err(|e| anyhow::anyhow!("Failed to build HTTP client: {}", e))
}