use std::sync::Arc;
use axum::extract::{Query, State};
use axum::Json;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tuitbot_core::storage::{approval_queue, replies, scheduled_content, threads};
use crate::account::AccountContext;
use crate::error::ApiError;
use crate::state::AppState;
use super::read_effective_config;
#[derive(Debug, Serialize)]
pub struct CalendarItem {
pub id: i64,
pub content_type: String,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub topic: Option<String>,
pub timestamp: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub performance_score: Option<f64>,
pub source: String,
}
#[derive(Deserialize)]
pub struct CalendarQuery {
pub from: String,
pub to: String,
}
pub async fn calendar(
State(state): State<Arc<AppState>>,
ctx: AccountContext,
Query(params): Query<CalendarQuery>,
) -> Result<Json<Value>, ApiError> {
let from = ¶ms.from;
let to = ¶ms.to;
let mut items: Vec<CalendarItem> = Vec::new();
let tweets = threads::get_tweets_in_range_for(&state.db, &ctx.account_id, from, to).await?;
for t in tweets {
items.push(CalendarItem {
id: t.id,
content_type: "tweet".to_string(),
content: t.content,
target_author: None,
topic: t.topic,
timestamp: t.created_at,
status: t.status,
performance_score: None,
source: "autonomous".to_string(),
});
}
let thread_list =
threads::get_threads_in_range_for(&state.db, &ctx.account_id, from, to).await?;
for t in thread_list {
items.push(CalendarItem {
id: t.id,
content_type: "thread".to_string(),
content: t.topic.clone(),
target_author: None,
topic: Some(t.topic),
timestamp: t.created_at,
status: t.status,
performance_score: None,
source: "autonomous".to_string(),
});
}
let reply_list =
replies::get_replies_in_range_for(&state.db, &ctx.account_id, from, to).await?;
for r in reply_list {
items.push(CalendarItem {
id: r.id,
content_type: "reply".to_string(),
content: r.reply_content,
target_author: Some(r.target_tweet_id),
topic: None,
timestamp: r.created_at,
status: r.status,
performance_score: None,
source: "autonomous".to_string(),
});
}
let pending =
approval_queue::get_by_statuses_for(&state.db, &ctx.account_id, &["pending"], None).await?;
for a in pending {
if a.created_at >= *from && a.created_at <= *to {
items.push(CalendarItem {
id: a.id,
content_type: a.action_type,
content: a.generated_content,
target_author: if a.target_author.is_empty() {
None
} else {
Some(a.target_author)
},
topic: if a.topic.is_empty() {
None
} else {
Some(a.topic)
},
timestamp: a.created_at,
status: "pending".to_string(),
performance_score: None,
source: "approval".to_string(),
});
}
}
let scheduled =
scheduled_content::get_in_range_for(&state.db, &ctx.account_id, from, to).await?;
for s in scheduled {
items.push(CalendarItem {
id: s.id,
content_type: s.content_type,
content: s.content,
target_author: None,
topic: None,
timestamp: s.scheduled_for.unwrap_or(s.created_at),
status: s.status,
performance_score: None,
source: "manual".to_string(),
});
}
items.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
Ok(Json(json!(items)))
}
pub async fn schedule(
State(state): State<Arc<AppState>>,
ctx: AccountContext,
) -> Result<Json<Value>, ApiError> {
let config = read_effective_config(&state, &ctx.account_id).await?;
Ok(Json(json!({
"timezone": config.schedule.timezone,
"active_hours": {
"start": config.schedule.active_hours_start,
"end": config.schedule.active_hours_end,
},
"preferred_times": config.schedule.preferred_times,
"preferred_times_override": config.schedule.preferred_times_override,
"thread_day": config.schedule.thread_preferred_day,
"thread_time": config.schedule.thread_preferred_time,
})))
}