tuitbot_server/routes/
targets.rs1use std::sync::Arc;
4
5use axum::extract::{Path, Query, State};
6use axum::Json;
7use serde::Deserialize;
8use serde_json::{json, Value};
9use tuitbot_core::storage::target_accounts;
10
11use crate::error::ApiError;
12use crate::state::AppState;
13
14pub async fn list_targets(State(state): State<Arc<AppState>>) -> Result<Json<Value>, ApiError> {
16 let accounts = target_accounts::get_enriched_target_accounts(&state.db).await?;
17 Ok(Json(json!(accounts)))
18}
19
20#[derive(Deserialize)]
22pub struct AddTargetRequest {
23 pub username: String,
25}
26
27pub async fn add_target(
29 State(state): State<Arc<AppState>>,
30 Json(body): Json<AddTargetRequest>,
31) -> Result<Json<Value>, ApiError> {
32 let username = body.username.trim().trim_start_matches('@');
33
34 if username.is_empty() {
35 return Err(ApiError::BadRequest("username is required".to_string()));
36 }
37
38 if let Some(existing) =
40 target_accounts::get_target_account_by_username(&state.db, username).await?
41 {
42 if existing.status == "active" {
43 return Err(ApiError::Conflict(format!(
44 "target account @{username} already exists"
45 )));
46 }
47 }
48
49 target_accounts::upsert_target_account(&state.db, username, username).await?;
52
53 Ok(Json(
54 json!({"status": "added", "username": username.to_string()}),
55 ))
56}
57
58pub async fn remove_target(
60 State(state): State<Arc<AppState>>,
61 Path(username): Path<String>,
62) -> Result<Json<Value>, ApiError> {
63 let removed = target_accounts::deactivate_target_account(&state.db, &username).await?;
64
65 if !removed {
66 return Err(ApiError::NotFound(format!(
67 "active target account @{username} not found"
68 )));
69 }
70
71 Ok(Json(json!({"status": "removed", "username": username})))
72}
73
74#[derive(Deserialize)]
76pub struct TimelineQuery {
77 pub limit: Option<i64>,
79}
80
81pub async fn target_timeline(
83 State(state): State<Arc<AppState>>,
84 Path(username): Path<String>,
85 Query(params): Query<TimelineQuery>,
86) -> Result<Json<Value>, ApiError> {
87 let limit = params.limit.unwrap_or(50).min(200);
88 let items = target_accounts::get_target_timeline(&state.db, &username, limit).await?;
89 Ok(Json(json!(items)))
90}
91
92pub async fn target_stats(
94 State(state): State<Arc<AppState>>,
95 Path(username): Path<String>,
96) -> Result<Json<Value>, ApiError> {
97 let stats = target_accounts::get_target_stats(&state.db, &username).await?;
98
99 match stats {
100 Some(s) => Ok(Json(json!(s))),
101 None => Err(ApiError::NotFound(format!(
102 "active target account @{username} not found"
103 ))),
104 }
105}