- 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
99 lines
2.7 KiB
Rust
99 lines
2.7 KiB
Rust
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"})),
|
|
)
|
|
}
|
|
}
|
|
}
|