Skip to main content

tuitbot_server/
lib.rs

1//! Tuitbot HTTP API server.
2//!
3//! Exposes `tuitbot-core`'s storage layer as a REST API with read + write
4//! endpoints, local bearer-token auth, and a WebSocket for real-time events.
5
6pub mod auth;
7pub mod error;
8pub mod routes;
9pub mod state;
10pub mod ws;
11
12use std::sync::Arc;
13
14use axum::middleware;
15use axum::routing::{delete, get, patch, post};
16use axum::Router;
17use tower_http::cors::CorsLayer;
18use tower_http::trace::TraceLayer;
19
20use crate::state::AppState;
21
22/// Build the complete axum router with all API routes and middleware.
23pub fn build_router(state: Arc<AppState>) -> Router {
24    let api = Router::new()
25        .route("/health", get(routes::health::health))
26        // Analytics
27        .route("/analytics/summary", get(routes::analytics::summary))
28        .route("/analytics/followers", get(routes::analytics::followers))
29        .route(
30            "/analytics/performance",
31            get(routes::analytics::performance),
32        )
33        .route("/analytics/topics", get(routes::analytics::topics))
34        .route(
35            "/analytics/recent-performance",
36            get(routes::analytics::recent_performance),
37        )
38        // Approval
39        .route("/approval", get(routes::approval::list_items))
40        .route("/approval/stats", get(routes::approval::stats))
41        .route("/approval/approve-all", post(routes::approval::approve_all))
42        .route("/approval/{id}", patch(routes::approval::edit_item))
43        .route(
44            "/approval/{id}/approve",
45            post(routes::approval::approve_item),
46        )
47        .route("/approval/{id}/reject", post(routes::approval::reject_item))
48        // Activity
49        .route("/activity", get(routes::activity::list_activity))
50        .route(
51            "/activity/rate-limits",
52            get(routes::activity::rate_limit_usage),
53        )
54        // Replies
55        .route("/replies", get(routes::replies::list_replies))
56        // Content
57        .route(
58            "/content/tweets",
59            get(routes::content::list_tweets).post(routes::content::compose_tweet),
60        )
61        .route(
62            "/content/threads",
63            get(routes::content::list_threads).post(routes::content::compose_thread),
64        )
65        .route("/content/calendar", get(routes::content::calendar))
66        .route("/content/schedule", get(routes::content::schedule))
67        .route("/content/compose", post(routes::content::compose))
68        .route(
69            "/content/scheduled/{id}",
70            patch(routes::content::edit_scheduled).delete(routes::content::cancel_scheduled),
71        )
72        // Drafts
73        .route(
74            "/content/drafts",
75            get(routes::content::list_drafts).post(routes::content::create_draft),
76        )
77        .route(
78            "/content/drafts/{id}",
79            patch(routes::content::edit_draft).delete(routes::content::delete_draft),
80        )
81        .route(
82            "/content/drafts/{id}/schedule",
83            post(routes::content::schedule_draft),
84        )
85        .route(
86            "/content/drafts/{id}/publish",
87            post(routes::content::publish_draft),
88        )
89        // Targets
90        .route(
91            "/targets",
92            get(routes::targets::list_targets).post(routes::targets::add_target),
93        )
94        .route(
95            "/targets/{username}/timeline",
96            get(routes::targets::target_timeline),
97        )
98        .route(
99            "/targets/{username}/stats",
100            get(routes::targets::target_stats),
101        )
102        .route(
103            "/targets/{username}",
104            delete(routes::targets::remove_target),
105        )
106        // Strategy
107        .route("/strategy/current", get(routes::strategy::current))
108        .route("/strategy/history", get(routes::strategy::history))
109        .route("/strategy/refresh", post(routes::strategy::refresh))
110        .route("/strategy/inputs", get(routes::strategy::inputs))
111        // Costs — LLM
112        .route("/costs/summary", get(routes::costs::summary))
113        .route("/costs/daily", get(routes::costs::daily))
114        .route("/costs/by-model", get(routes::costs::by_model))
115        .route("/costs/by-type", get(routes::costs::by_type))
116        // Costs — X API
117        .route("/costs/x-api/summary", get(routes::costs::x_api_summary))
118        .route("/costs/x-api/daily", get(routes::costs::x_api_daily))
119        .route(
120            "/costs/x-api/by-endpoint",
121            get(routes::costs::x_api_by_endpoint),
122        )
123        // AI Assist
124        .route("/assist/tweet", post(routes::assist::assist_tweet))
125        .route("/assist/reply", post(routes::assist::assist_reply))
126        .route("/assist/thread", post(routes::assist::assist_thread))
127        .route("/assist/improve", post(routes::assist::assist_improve))
128        .route("/assist/topics", get(routes::assist::assist_topics))
129        .route(
130            "/assist/optimal-times",
131            get(routes::assist::assist_optimal_times),
132        )
133        .route("/assist/mode", get(routes::assist::get_mode))
134        // Discovery feed
135        .route("/discovery/feed", get(routes::discovery::feed))
136        .route(
137            "/discovery/{tweet_id}/compose-reply",
138            post(routes::discovery::compose_reply),
139        )
140        .route(
141            "/discovery/{tweet_id}/queue-reply",
142            post(routes::discovery::queue_reply),
143        )
144        // Media
145        .route("/media/upload", post(routes::media::upload))
146        .route("/media/file", get(routes::media::serve_file))
147        // Settings
148        .route("/settings/status", get(routes::settings::config_status))
149        .route("/settings/init", post(routes::settings::init_settings))
150        .route(
151            "/settings/validate",
152            post(routes::settings::validate_settings),
153        )
154        .route("/settings/defaults", get(routes::settings::get_defaults))
155        .route("/settings/test-llm", post(routes::settings::test_llm))
156        .route(
157            "/settings",
158            get(routes::settings::get_settings).patch(routes::settings::patch_settings),
159        )
160        // MCP governance
161        .route(
162            "/mcp/policy",
163            get(routes::mcp::get_policy).patch(routes::mcp::patch_policy),
164        )
165        .route(
166            "/mcp/telemetry/summary",
167            get(routes::mcp::telemetry_summary),
168        )
169        .route(
170            "/mcp/telemetry/metrics",
171            get(routes::mcp::telemetry_metrics),
172        )
173        .route("/mcp/telemetry/errors", get(routes::mcp::telemetry_errors))
174        .route("/mcp/telemetry/recent", get(routes::mcp::telemetry_recent))
175        // Runtime
176        .route("/runtime/status", get(routes::runtime::status))
177        .route("/runtime/start", post(routes::runtime::start))
178        .route("/runtime/stop", post(routes::runtime::stop))
179        // WebSocket
180        .route("/ws", get(ws::ws_handler))
181        // Auth middleware — applied to all routes; health is exempted internally.
182        .layer(middleware::from_fn_with_state(
183            state.clone(),
184            auth::auth_middleware,
185        ));
186
187    Router::new()
188        .nest("/api", api)
189        .layer(CorsLayer::permissive())
190        .layer(TraceLayer::new_for_http())
191        .with_state(state)
192}