tuitbot_server/routes/
analytics.rs1use std::sync::Arc;
4
5use axum::extract::{Query, State};
6use axum::Json;
7use serde::Deserialize;
8use serde_json::{json, Value};
9use tuitbot_core::storage::analytics;
10
11use crate::account::AccountContext;
12use crate::error::ApiError;
13use crate::state::AppState;
14
15#[derive(Deserialize)]
17pub struct FollowersQuery {
18 #[serde(default = "default_days")]
20 pub days: u32,
21}
22
23fn default_days() -> u32 {
24 7
25}
26
27#[derive(Deserialize)]
29pub struct TopicsQuery {
30 #[serde(default = "default_topic_limit")]
32 pub limit: u32,
33}
34
35fn default_topic_limit() -> u32 {
36 20
37}
38
39#[derive(Deserialize)]
41pub struct RecentPerformanceQuery {
42 #[serde(default = "default_recent_limit")]
44 pub limit: u32,
45}
46
47fn default_recent_limit() -> u32 {
48 20
49}
50
51pub async fn followers(
53 State(state): State<Arc<AppState>>,
54 ctx: AccountContext,
55 Query(params): Query<FollowersQuery>,
56) -> Result<Json<Value>, ApiError> {
57 let snapshots =
58 analytics::get_follower_snapshots_for(&state.db, &ctx.account_id, params.days).await?;
59 Ok(Json(json!(snapshots)))
60}
61
62pub async fn performance(
64 State(state): State<Arc<AppState>>,
65 ctx: AccountContext,
66) -> Result<Json<Value>, ApiError> {
67 let avg_reply = analytics::get_avg_reply_engagement_for(&state.db, &ctx.account_id).await?;
68 let avg_tweet = analytics::get_avg_tweet_engagement_for(&state.db, &ctx.account_id).await?;
69 let (reply_count, tweet_count) =
70 analytics::get_performance_counts_for(&state.db, &ctx.account_id).await?;
71
72 Ok(Json(json!({
73 "avg_reply_engagement": avg_reply,
74 "avg_tweet_engagement": avg_tweet,
75 "measured_replies": reply_count,
76 "measured_tweets": tweet_count,
77 })))
78}
79
80pub async fn topics(
82 State(state): State<Arc<AppState>>,
83 ctx: AccountContext,
84 Query(params): Query<TopicsQuery>,
85) -> Result<Json<Value>, ApiError> {
86 let scores = analytics::get_top_topics_for(&state.db, &ctx.account_id, params.limit).await?;
87 Ok(Json(json!(scores)))
88}
89
90pub async fn summary(
92 State(state): State<Arc<AppState>>,
93 ctx: AccountContext,
94) -> Result<Json<Value>, ApiError> {
95 let data = analytics::get_analytics_summary_for(&state.db, &ctx.account_id).await?;
96 Ok(Json(json!(data)))
97}
98
99pub async fn recent_performance(
101 State(state): State<Arc<AppState>>,
102 ctx: AccountContext,
103 Query(params): Query<RecentPerformanceQuery>,
104) -> Result<Json<Value>, ApiError> {
105 let items =
106 analytics::get_recent_performance_items_for(&state.db, &ctx.account_id, params.limit)
107 .await?;
108 Ok(Json(json!(items)))
109}
110
111#[derive(Deserialize)]
113pub struct EngagementRateQuery {
114 #[serde(default = "default_engagement_limit")]
116 pub limit: u32,
117}
118
119fn default_engagement_limit() -> u32 {
120 20
121}
122
123pub async fn engagement_rate(
125 State(state): State<Arc<AppState>>,
126 ctx: AccountContext,
127 Query(params): Query<EngagementRateQuery>,
128) -> Result<Json<Value>, ApiError> {
129 let metrics =
130 analytics::get_engagement_rate_for(&state.db, &ctx.account_id, params.limit).await?;
131 Ok(Json(json!(metrics)))
132}
133
134#[derive(Deserialize)]
136pub struct ReachQuery {
137 #[serde(default = "default_reach_days")]
139 pub window: u32,
140}
141
142fn default_reach_days() -> u32 {
143 7
144}
145
146pub async fn reach(
148 State(state): State<Arc<AppState>>,
149 ctx: AccountContext,
150 Query(params): Query<ReachQuery>,
151) -> Result<Json<Value>, ApiError> {
152 let snapshots = analytics::get_reach_for(&state.db, &ctx.account_id, params.window).await?;
153 Ok(Json(json!(snapshots)))
154}
155
156#[derive(Deserialize)]
158pub struct FollowerGrowthQuery {
159 #[serde(default = "default_growth_days")]
161 pub window: u32,
162}
163
164fn default_growth_days() -> u32 {
165 30
166}
167
168pub async fn follower_growth(
170 State(state): State<Arc<AppState>>,
171 ctx: AccountContext,
172 Query(params): Query<FollowerGrowthQuery>,
173) -> Result<Json<Value>, ApiError> {
174 let snapshots =
175 analytics::get_follower_growth_for(&state.db, &ctx.account_id, params.window).await?;
176 Ok(Json(json!(snapshots)))
177}
178
179pub async fn best_times(
181 State(state): State<Arc<AppState>>,
182 ctx: AccountContext,
183) -> Result<Json<Value>, ApiError> {
184 let slots = analytics::get_best_times_for(&state.db, &ctx.account_id).await?;
185 Ok(Json(json!(slots)))
186}
187
188pub async fn heatmap(
190 State(state): State<Arc<AppState>>,
191 ctx: AccountContext,
192) -> Result<Json<Value>, ApiError> {
193 let grid = analytics::get_heatmap_for(&state.db, &ctx.account_id).await?;
194 Ok(Json(json!(grid)))
195}
196
197pub async fn content_breakdown(
199 State(state): State<Arc<AppState>>,
200 ctx: AccountContext,
201) -> Result<Json<Value>, ApiError> {
202 let breakdown = analytics::get_content_breakdown_for(&state.db, &ctx.account_id).await?;
203 Ok(Json(json!(breakdown)))
204}