tuitbot_server/routes/approval/
export.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::approval_queue;
10
11use crate::account::AccountContext;
12use crate::error::ApiError;
13use crate::state::AppState;
14
15#[derive(Deserialize)]
17pub struct ExportQuery {
18 #[serde(default = "default_csv")]
20 pub format: String,
21 #[serde(default = "default_export_status")]
23 pub status: String,
24 #[serde(rename = "type")]
26 pub action_type: Option<String>,
27}
28
29fn default_csv() -> String {
30 "csv".to_string()
31}
32
33fn default_export_status() -> String {
34 "pending,approved,rejected,posted".to_string()
35}
36
37pub async fn export_items(
39 State(state): State<Arc<AppState>>,
40 ctx: AccountContext,
41 Query(params): Query<ExportQuery>,
42) -> Result<axum::response::Response, ApiError> {
43 use axum::response::IntoResponse;
44
45 let statuses: Vec<&str> = params.status.split(',').map(|s| s.trim()).collect();
46 let action_type = params.action_type.as_deref();
47
48 let items =
49 approval_queue::get_by_statuses_for(&state.db, &ctx.account_id, &statuses, action_type)
50 .await?;
51
52 if params.format == "json" {
53 let body = serde_json::to_string(&items).unwrap_or_else(|_| "[]".to_string());
54 Ok((
55 [
56 (
57 axum::http::header::CONTENT_TYPE,
58 "application/json; charset=utf-8",
59 ),
60 (
61 axum::http::header::CONTENT_DISPOSITION,
62 "attachment; filename=\"approval_export.json\"",
63 ),
64 ],
65 body,
66 )
67 .into_response())
68 } else {
69 let mut csv = String::from(
70 "id,action_type,target_author,generated_content,topic,score,status,reviewed_by,review_notes,created_at\n",
71 );
72 for item in &items {
73 csv.push_str(&format!(
74 "{},{},{},{},{},{},{},{},{},{}\n",
75 item.id,
76 escape_csv(&item.action_type),
77 escape_csv(&item.target_author),
78 escape_csv(&item.generated_content),
79 escape_csv(&item.topic),
80 item.score,
81 escape_csv(&item.status),
82 escape_csv(item.reviewed_by.as_deref().unwrap_or("")),
83 escape_csv(item.review_notes.as_deref().unwrap_or("")),
84 escape_csv(&item.created_at),
85 ));
86 }
87 Ok((
88 [
89 (axum::http::header::CONTENT_TYPE, "text/csv; charset=utf-8"),
90 (
91 axum::http::header::CONTENT_DISPOSITION,
92 "attachment; filename=\"approval_export.csv\"",
93 ),
94 ],
95 csv,
96 )
97 .into_response())
98 }
99}
100
101pub(super) fn escape_csv(value: &str) -> String {
103 if value.contains(',') || value.contains('"') || value.contains('\n') {
104 format!("\"{}\"", value.replace('"', "\"\""))
105 } else {
106 value.to_string()
107 }
108}
109
110pub async fn get_edit_history(
112 State(state): State<Arc<AppState>>,
113 _ctx: AccountContext,
114 Path(id): Path<i64>,
115) -> Result<Json<Value>, ApiError> {
116 let history = approval_queue::get_edit_history(&state.db, id).await?;
118 Ok(Json(json!(history)))
119}