tuitbot_server/routes/
content.rs1use std::sync::Arc;
4
5use axum::extract::{Query, State};
6use axum::Json;
7use serde::Deserialize;
8use serde_json::{json, Value};
9use tuitbot_core::config::Config;
10use tuitbot_core::storage::{approval_queue, threads};
11
12use crate::error::ApiError;
13use crate::state::AppState;
14use crate::ws::WsEvent;
15
16#[derive(Deserialize)]
18pub struct TweetsQuery {
19 #[serde(default = "default_tweet_limit")]
21 pub limit: u32,
22}
23
24fn default_tweet_limit() -> u32 {
25 50
26}
27
28#[derive(Deserialize)]
30pub struct ThreadsQuery {
31 #[serde(default = "default_thread_limit")]
33 pub limit: u32,
34}
35
36fn default_thread_limit() -> u32 {
37 20
38}
39
40pub async fn list_tweets(
42 State(state): State<Arc<AppState>>,
43 Query(params): Query<TweetsQuery>,
44) -> Result<Json<Value>, ApiError> {
45 let tweets = threads::get_recent_original_tweets(&state.db, params.limit).await?;
46 Ok(Json(json!(tweets)))
47}
48
49pub async fn list_threads(
51 State(state): State<Arc<AppState>>,
52 Query(params): Query<ThreadsQuery>,
53) -> Result<Json<Value>, ApiError> {
54 let threads = threads::get_recent_threads(&state.db, params.limit).await?;
55 Ok(Json(json!(threads)))
56}
57
58#[derive(Deserialize)]
60pub struct ComposeTweetRequest {
61 pub text: String,
63 pub scheduled_for: Option<String>,
65}
66
67pub async fn compose_tweet(
69 State(state): State<Arc<AppState>>,
70 Json(body): Json<ComposeTweetRequest>,
71) -> Result<Json<Value>, ApiError> {
72 let text = body.text.trim();
73 if text.is_empty() {
74 return Err(ApiError::BadRequest("text is required".to_string()));
75 }
76
77 let approval_mode = read_approval_mode(&state)?;
79
80 if approval_mode {
81 let id = approval_queue::enqueue(
82 &state.db, "tweet", "", "", text, "", "", 0.0,
87 )
88 .await?;
89
90 let _ = state.event_tx.send(WsEvent::ApprovalQueued {
91 id,
92 action_type: "tweet".to_string(),
93 content: text.to_string(),
94 });
95
96 Ok(Json(json!({
97 "status": "queued_for_approval",
98 "id": id,
99 })))
100 } else {
101 Ok(Json(json!({
103 "status": "accepted",
104 "text": text,
105 "scheduled_for": body.scheduled_for,
106 })))
107 }
108}
109
110#[derive(Deserialize)]
112pub struct ComposeThreadRequest {
113 pub tweets: Vec<String>,
115 pub scheduled_for: Option<String>,
117}
118
119pub async fn compose_thread(
121 State(state): State<Arc<AppState>>,
122 Json(body): Json<ComposeThreadRequest>,
123) -> Result<Json<Value>, ApiError> {
124 if body.tweets.is_empty() {
125 return Err(ApiError::BadRequest(
126 "tweets array must not be empty".to_string(),
127 ));
128 }
129
130 let approval_mode = read_approval_mode(&state)?;
131 let combined = body.tweets.join("\n---\n");
132
133 if approval_mode {
134 let id =
135 approval_queue::enqueue(&state.db, "thread", "", "", &combined, "", "", 0.0).await?;
136
137 let _ = state.event_tx.send(WsEvent::ApprovalQueued {
138 id,
139 action_type: "thread".to_string(),
140 content: combined,
141 });
142
143 Ok(Json(json!({
144 "status": "queued_for_approval",
145 "id": id,
146 })))
147 } else {
148 Ok(Json(json!({
149 "status": "accepted",
150 "tweet_count": body.tweets.len(),
151 "scheduled_for": body.scheduled_for,
152 })))
153 }
154}
155
156fn read_approval_mode(state: &AppState) -> Result<bool, ApiError> {
158 let contents = std::fs::read_to_string(&state.config_path).unwrap_or_default();
159 let config: Config = toml::from_str(&contents).unwrap_or_default();
160 Ok(config.approval_mode)
161}