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::account::{require_mutate, AccountContext};
12use crate::error::ApiError;
13use crate::state::AppState;
14
15pub async fn list_targets(
17 State(state): State<Arc<AppState>>,
18 ctx: AccountContext,
19) -> Result<Json<Value>, ApiError> {
20 let accounts =
21 target_accounts::get_enriched_target_accounts_for(&state.db, &ctx.account_id).await?;
22 Ok(Json(json!(accounts)))
23}
24
25#[derive(Deserialize)]
27pub struct AddTargetRequest {
28 pub username: String,
30}
31
32pub async fn add_target(
34 State(state): State<Arc<AppState>>,
35 ctx: AccountContext,
36 Json(body): Json<AddTargetRequest>,
37) -> Result<Json<Value>, ApiError> {
38 require_mutate(&ctx)?;
39
40 let username = body.username.trim().trim_start_matches('@');
41
42 if username.is_empty() {
43 return Err(ApiError::BadRequest("username is required".to_string()));
44 }
45
46 if let Some(existing) =
48 target_accounts::get_target_account_by_username_for(&state.db, &ctx.account_id, username)
49 .await?
50 {
51 if existing.status == "active" {
52 return Err(ApiError::Conflict(format!(
53 "target account @{username} already exists"
54 )));
55 }
56 }
57
58 target_accounts::upsert_target_account_for(&state.db, &ctx.account_id, username, username)
61 .await?;
62
63 Ok(Json(
64 json!({"status": "added", "username": username.to_string()}),
65 ))
66}
67
68pub async fn remove_target(
70 State(state): State<Arc<AppState>>,
71 ctx: AccountContext,
72 Path(username): Path<String>,
73) -> Result<Json<Value>, ApiError> {
74 require_mutate(&ctx)?;
75
76 let removed =
77 target_accounts::deactivate_target_account_for(&state.db, &ctx.account_id, &username)
78 .await?;
79
80 if !removed {
81 return Err(ApiError::NotFound(format!(
82 "active target account @{username} not found"
83 )));
84 }
85
86 Ok(Json(json!({"status": "removed", "username": username})))
87}
88
89#[derive(Deserialize)]
91pub struct TimelineQuery {
92 pub limit: Option<i64>,
94}
95
96pub async fn target_timeline(
98 State(state): State<Arc<AppState>>,
99 ctx: AccountContext,
100 Path(username): Path<String>,
101 Query(params): Query<TimelineQuery>,
102) -> Result<Json<Value>, ApiError> {
103 let limit = params.limit.unwrap_or(50).min(200);
104 let items =
105 target_accounts::get_target_timeline_for(&state.db, &ctx.account_id, &username, limit)
106 .await?;
107 Ok(Json(json!(items)))
108}
109
110pub async fn target_stats(
112 State(state): State<Arc<AppState>>,
113 ctx: AccountContext,
114 Path(username): Path<String>,
115) -> Result<Json<Value>, ApiError> {
116 let stats =
117 target_accounts::get_target_stats_for(&state.db, &ctx.account_id, &username).await?;
118
119 match stats {
120 Some(s) => Ok(Json(json!(s))),
121 None => Err(ApiError::NotFound(format!(
122 "active target account @{username} not found"
123 ))),
124 }
125}