Skip to main content

tuitbot_server/routes/
scraper_session.rs

1//! Scraper session endpoints for importing/managing browser cookie sessions.
2
3use std::sync::Arc;
4
5use axum::extract::State;
6use axum::Json;
7use serde::Deserialize;
8use serde_json::Value;
9use tuitbot_core::storage::accounts::account_scraper_session_path;
10use tuitbot_core::x_api::ScraperSession;
11
12use crate::account::AccountContext;
13use crate::error::ApiError;
14use crate::state::AppState;
15
16/// Request body for importing a browser session.
17#[derive(Deserialize)]
18pub struct ImportSessionRequest {
19    /// The `auth_token` cookie value from the browser.
20    pub auth_token: String,
21    /// The `ct0` cookie value (CSRF token) from the browser.
22    pub ct0: String,
23    /// Optional X username for display purposes.
24    #[serde(default)]
25    pub username: Option<String>,
26}
27
28/// `GET /api/settings/scraper-session` — check if a browser session exists.
29pub async fn get_scraper_session(
30    State(state): State<Arc<AppState>>,
31    ctx: AccountContext,
32) -> Result<Json<Value>, ApiError> {
33    let session_path = account_scraper_session_path(&state.data_dir, &ctx.account_id);
34    let session = ScraperSession::load(&session_path)
35        .map_err(|e| ApiError::Internal(format!("failed to read session: {e}")))?;
36
37    match session {
38        Some(s) => Ok(Json(serde_json::json!({
39            "exists": true,
40            "username": s.username,
41            "created_at": s.created_at,
42        }))),
43        None => Ok(Json(serde_json::json!({
44            "exists": false,
45        }))),
46    }
47}
48
49/// `POST /api/settings/scraper-session` — import browser cookies.
50pub async fn import_scraper_session(
51    State(state): State<Arc<AppState>>,
52    ctx: AccountContext,
53    Json(body): Json<ImportSessionRequest>,
54) -> Result<Json<Value>, ApiError> {
55    if body.auth_token.trim().is_empty() || body.ct0.trim().is_empty() {
56        return Err(ApiError::BadRequest(
57            "auth_token and ct0 are required".to_string(),
58        ));
59    }
60
61    let session = ScraperSession {
62        auth_token: body.auth_token.trim().to_string(),
63        ct0: body.ct0.trim().to_string(),
64        username: body.username,
65        created_at: Some(chrono::Utc::now().to_rfc3339()),
66    };
67
68    let session_path = account_scraper_session_path(&state.data_dir, &ctx.account_id);
69
70    // Ensure the parent directory exists for non-default accounts.
71    if let Some(parent) = session_path.parent() {
72        std::fs::create_dir_all(parent)
73            .map_err(|e| ApiError::Internal(format!("failed to create session directory: {e}")))?;
74    }
75
76    session
77        .save(&session_path)
78        .map_err(|e| ApiError::Internal(format!("failed to save session: {e}")))?;
79
80    tracing::info!(account_id = %ctx.account_id, "Browser session imported successfully");
81
82    Ok(Json(serde_json::json!({
83        "status": "imported",
84        "username": session.username,
85        "created_at": session.created_at,
86    })))
87}
88
89/// `DELETE /api/settings/scraper-session` — remove the browser session.
90pub async fn delete_scraper_session(
91    State(state): State<Arc<AppState>>,
92    ctx: AccountContext,
93) -> Result<Json<Value>, ApiError> {
94    let session_path = account_scraper_session_path(&state.data_dir, &ctx.account_id);
95    let deleted = ScraperSession::delete(&session_path)
96        .map_err(|e| ApiError::Internal(format!("failed to delete session: {e}")))?;
97
98    Ok(Json(serde_json::json!({
99        "deleted": deleted,
100    })))
101}