tuitbot_server/routes/content/
calendar.rs1use std::sync::Arc;
4
5use axum::extract::{Query, State};
6use axum::Json;
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use tuitbot_core::storage::{approval_queue, replies, scheduled_content, threads};
10
11use crate::account::AccountContext;
12use crate::error::ApiError;
13use crate::state::AppState;
14
15use super::read_config;
16
17#[derive(Debug, Serialize)]
19pub struct CalendarItem {
20 pub id: i64,
21 pub content_type: String,
22 pub content: String,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub target_author: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub topic: Option<String>,
27 pub timestamp: String,
28 pub status: String,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub performance_score: Option<f64>,
31 pub source: String,
32}
33
34#[derive(Deserialize)]
36pub struct CalendarQuery {
37 pub from: String,
39 pub to: String,
41}
42
43pub async fn calendar(
45 State(state): State<Arc<AppState>>,
46 ctx: AccountContext,
47 Query(params): Query<CalendarQuery>,
48) -> Result<Json<Value>, ApiError> {
49 let from = ¶ms.from;
50 let to = ¶ms.to;
51
52 let mut items: Vec<CalendarItem> = Vec::new();
53
54 let tweets = threads::get_tweets_in_range_for(&state.db, &ctx.account_id, from, to).await?;
56 for t in tweets {
57 items.push(CalendarItem {
58 id: t.id,
59 content_type: "tweet".to_string(),
60 content: t.content,
61 target_author: None,
62 topic: t.topic,
63 timestamp: t.created_at,
64 status: t.status,
65 performance_score: None,
66 source: "autonomous".to_string(),
67 });
68 }
69
70 let thread_list =
72 threads::get_threads_in_range_for(&state.db, &ctx.account_id, from, to).await?;
73 for t in thread_list {
74 items.push(CalendarItem {
75 id: t.id,
76 content_type: "thread".to_string(),
77 content: t.topic.clone(),
78 target_author: None,
79 topic: Some(t.topic),
80 timestamp: t.created_at,
81 status: t.status,
82 performance_score: None,
83 source: "autonomous".to_string(),
84 });
85 }
86
87 let reply_list =
89 replies::get_replies_in_range_for(&state.db, &ctx.account_id, from, to).await?;
90 for r in reply_list {
91 items.push(CalendarItem {
92 id: r.id,
93 content_type: "reply".to_string(),
94 content: r.reply_content,
95 target_author: Some(r.target_tweet_id),
96 topic: None,
97 timestamp: r.created_at,
98 status: r.status,
99 performance_score: None,
100 source: "autonomous".to_string(),
101 });
102 }
103
104 let pending =
106 approval_queue::get_by_statuses_for(&state.db, &ctx.account_id, &["pending"], None).await?;
107 for a in pending {
108 if a.created_at >= *from && a.created_at <= *to {
110 items.push(CalendarItem {
111 id: a.id,
112 content_type: a.action_type,
113 content: a.generated_content,
114 target_author: if a.target_author.is_empty() {
115 None
116 } else {
117 Some(a.target_author)
118 },
119 topic: if a.topic.is_empty() {
120 None
121 } else {
122 Some(a.topic)
123 },
124 timestamp: a.created_at,
125 status: "pending".to_string(),
126 performance_score: None,
127 source: "approval".to_string(),
128 });
129 }
130 }
131
132 let scheduled =
134 scheduled_content::get_in_range_for(&state.db, &ctx.account_id, from, to).await?;
135 for s in scheduled {
136 items.push(CalendarItem {
137 id: s.id,
138 content_type: s.content_type,
139 content: s.content,
140 target_author: None,
141 topic: None,
142 timestamp: s.scheduled_for.unwrap_or(s.created_at),
143 status: s.status,
144 performance_score: None,
145 source: "manual".to_string(),
146 });
147 }
148
149 items.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
151
152 Ok(Json(json!(items)))
153}
154
155pub async fn schedule(
157 State(state): State<Arc<AppState>>,
158 _ctx: AccountContext,
159) -> Result<Json<Value>, ApiError> {
160 let config = read_config(&state)?;
161
162 Ok(Json(json!({
163 "timezone": config.schedule.timezone,
164 "active_hours": {
165 "start": config.schedule.active_hours_start,
166 "end": config.schedule.active_hours_end,
167 },
168 "preferred_times": config.schedule.preferred_times,
169 "preferred_times_override": config.schedule.preferred_times_override,
170 "thread_day": config.schedule.thread_preferred_day,
171 "thread_time": config.schedule.thread_preferred_time,
172 })))
173}