add http API, channel-driven life loop, predefined diary timer
- Extract http.rs: unified HTTP server with /api/timers and gitea webhook - Life loop: select! on interval tick + mpsc channel for force-fire - Predefined diary timer (cron 22:55 daily), auto-registered on startup - BufferOutput for system timers (chat_id=0), no TG message - state: ensure_timer(), get_timer() - context.md: add blog and Hugo docs for AI
This commit is contained in:
98
src/http.rs
Normal file
98
src/http.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::{Path, State as AxumState};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{get, post};
|
||||
use axum::Json;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::life::LifeEvent;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpState {
|
||||
pub app_state: Arc<AppState>,
|
||||
pub life_tx: mpsc::Sender<LifeEvent>,
|
||||
}
|
||||
|
||||
pub async fn start_http_server(
|
||||
config: &Config,
|
||||
app_state: Arc<AppState>,
|
||||
life_tx: mpsc::Sender<LifeEvent>,
|
||||
) {
|
||||
let port = config
|
||||
.gitea
|
||||
.as_ref()
|
||||
.map(|g| g.webhook_port)
|
||||
.unwrap_or(9880);
|
||||
|
||||
let state = Arc::new(HttpState {
|
||||
app_state,
|
||||
life_tx,
|
||||
});
|
||||
|
||||
let mut app = axum::Router::new()
|
||||
.route("/api/timers", get(list_timers))
|
||||
.route("/api/timers/{id}/fire", post(fire_timer))
|
||||
.with_state(state);
|
||||
|
||||
// merge gitea webhook router if configured
|
||||
if let Some(gitea_config) = &config.gitea {
|
||||
let bot_user = std::env::var("GITEA_ADMIN_USER").unwrap_or_else(|_| "noc".into());
|
||||
app = app.merge(crate::gitea::webhook_router(gitea_config, bot_user));
|
||||
}
|
||||
|
||||
let addr = format!("0.0.0.0:{port}");
|
||||
info!("http server listening on {addr}");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("bind {addr}: {e}"));
|
||||
|
||||
if let Err(e) = axum::serve(listener, app).await {
|
||||
error!("http server error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_timers(AxumState(state): AxumState<Arc<HttpState>>) -> impl IntoResponse {
|
||||
let timers = state.app_state.list_timers(None).await;
|
||||
let items: Vec<serde_json::Value> = timers
|
||||
.iter()
|
||||
.map(|(id, chat_id, label, schedule, next_fire, enabled)| {
|
||||
serde_json::json!({
|
||||
"id": id,
|
||||
"chat_id": chat_id,
|
||||
"label": label,
|
||||
"schedule": schedule,
|
||||
"next_fire": next_fire,
|
||||
"enabled": enabled,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Json(serde_json::json!(items))
|
||||
}
|
||||
|
||||
async fn fire_timer(
|
||||
AxumState(state): AxumState<Arc<HttpState>>,
|
||||
Path(id): Path<i64>,
|
||||
) -> impl IntoResponse {
|
||||
match state.life_tx.send(LifeEvent::FireTimer(id)).await {
|
||||
Ok(_) => {
|
||||
info!(timer_id = id, "timer fire requested via API");
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({"status": "fired", "timer_id": id})),
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(timer_id = id, "failed to send fire event: {e}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": "life loop not responding"})),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user