tuitbot_server/routes/content/
drafts.rs1use std::sync::Arc;
4
5use axum::extract::{Path, State};
6use axum::Json;
7use serde::Deserialize;
8use serde_json::{json, Value};
9use tuitbot_core::content::{tweet_weighted_len, MAX_TWEET_CHARS};
10use tuitbot_core::storage::{approval_queue, scheduled_content};
11
12use crate::account::{require_mutate, AccountContext};
13use crate::error::ApiError;
14use crate::state::AppState;
15
16#[derive(Deserialize)]
17pub struct CreateDraftRequest {
18 pub content_type: String,
19 pub content: String,
20 #[serde(default = "default_source")]
21 pub source: String,
22}
23
24fn default_source() -> String {
25 "manual".to_string()
26}
27
28pub async fn list_drafts(
29 State(state): State<Arc<AppState>>,
30 ctx: AccountContext,
31) -> Result<Json<Vec<scheduled_content::ScheduledContent>>, ApiError> {
32 let drafts = scheduled_content::list_drafts_for(&state.db, &ctx.account_id)
33 .await
34 .map_err(ApiError::Storage)?;
35 Ok(Json(drafts))
36}
37
38pub async fn create_draft(
39 State(state): State<Arc<AppState>>,
40 ctx: AccountContext,
41 Json(body): Json<CreateDraftRequest>,
42) -> Result<Json<Value>, ApiError> {
43 require_mutate(&ctx)?;
44
45 if body.content.trim().is_empty() {
47 return Err(ApiError::BadRequest(
48 "content must not be empty".to_string(),
49 ));
50 }
51
52 if body.content_type == "tweet"
53 && !tuitbot_core::content::validate_tweet_length(&body.content, MAX_TWEET_CHARS)
54 {
55 return Err(ApiError::BadRequest(format!(
56 "Tweet exceeds {} characters (weighted length: {})",
57 MAX_TWEET_CHARS,
58 tweet_weighted_len(&body.content)
59 )));
60 }
61
62 let id = scheduled_content::insert_draft_for(
63 &state.db,
64 &ctx.account_id,
65 &body.content_type,
66 &body.content,
67 &body.source,
68 )
69 .await
70 .map_err(ApiError::Storage)?;
71
72 Ok(Json(json!({ "id": id, "status": "draft" })))
73}
74
75#[derive(Deserialize)]
76pub struct EditDraftRequest {
77 pub content: String,
78}
79
80pub async fn edit_draft(
81 State(state): State<Arc<AppState>>,
82 ctx: AccountContext,
83 Path(id): Path<i64>,
84 Json(body): Json<EditDraftRequest>,
85) -> Result<Json<Value>, ApiError> {
86 require_mutate(&ctx)?;
87
88 if body.content.trim().is_empty() {
89 return Err(ApiError::BadRequest(
90 "content must not be empty".to_string(),
91 ));
92 }
93
94 scheduled_content::update_draft_for(&state.db, &ctx.account_id, id, &body.content)
95 .await
96 .map_err(ApiError::Storage)?;
97
98 Ok(Json(json!({ "id": id, "status": "draft" })))
99}
100
101pub async fn delete_draft(
102 State(state): State<Arc<AppState>>,
103 ctx: AccountContext,
104 Path(id): Path<i64>,
105) -> Result<Json<Value>, ApiError> {
106 require_mutate(&ctx)?;
107
108 scheduled_content::delete_draft_for(&state.db, &ctx.account_id, id)
109 .await
110 .map_err(ApiError::Storage)?;
111
112 Ok(Json(json!({ "id": id, "status": "cancelled" })))
113}
114
115#[derive(Deserialize)]
116pub struct ScheduleDraftRequest {
117 pub scheduled_for: String,
118}
119
120pub async fn schedule_draft(
121 State(state): State<Arc<AppState>>,
122 ctx: AccountContext,
123 Path(id): Path<i64>,
124 Json(body): Json<ScheduleDraftRequest>,
125) -> Result<Json<Value>, ApiError> {
126 require_mutate(&ctx)?;
127
128 scheduled_content::schedule_draft_for(&state.db, &ctx.account_id, id, &body.scheduled_for)
129 .await
130 .map_err(ApiError::Storage)?;
131
132 Ok(Json(
133 json!({ "id": id, "status": "scheduled", "scheduled_for": body.scheduled_for }),
134 ))
135}
136
137pub async fn publish_draft(
138 State(state): State<Arc<AppState>>,
139 ctx: AccountContext,
140 Path(id): Path<i64>,
141) -> Result<Json<Value>, ApiError> {
142 require_mutate(&ctx)?;
143
144 let item = scheduled_content::get_by_id_for(&state.db, &ctx.account_id, id)
146 .await
147 .map_err(ApiError::Storage)?
148 .ok_or_else(|| ApiError::NotFound(format!("Draft {id} not found")))?;
149
150 if item.status != "draft" {
151 return Err(ApiError::BadRequest(format!(
152 "Item is in '{}' status, not 'draft'",
153 item.status
154 )));
155 }
156
157 let queue_id = approval_queue::enqueue_for(
159 &state.db,
160 &ctx.account_id,
161 &item.content_type,
162 "", "", &item.content,
165 "", "", 0.0, "[]",
169 )
170 .await
171 .map_err(ApiError::Storage)?;
172
173 approval_queue::update_status_for(&state.db, &ctx.account_id, queue_id, "approved")
175 .await
176 .map_err(ApiError::Storage)?;
177
178 scheduled_content::update_status_for(&state.db, &ctx.account_id, id, "posted", None)
180 .await
181 .map_err(ApiError::Storage)?;
182
183 Ok(Json(
184 json!({ "id": id, "approval_queue_id": queue_id, "status": "queued_for_posting" }),
185 ))
186}