rustlog/src/web/mod.rs

170 lines
5.7 KiB
Rust

mod admin;
mod frontend;
mod handlers;
mod responders;
pub mod schema;
mod trace_layer;
use crate::{app::App, bot::BotMessage, web::admin::admin_auth, ShutdownRx};
use aide::{
axum::{
routing::{get, get_with, post, post_with},
ApiRouter, IntoApiResponse,
},
openapi::OpenApi,
redoc::Redoc,
};
use axum::{middleware, response::IntoResponse, Extension, Json, ServiceExt};
use prometheus::TextEncoder;
use std::{
net::{AddrParseError, SocketAddr},
str::FromStr,
sync::Arc,
};
use tokio::sync::mpsc::Sender;
use tower_http::{cors::CorsLayer, normalize_path::NormalizePath, trace::TraceLayer};
use tracing::{debug, info};
pub async fn run(app: App, mut shutdown_rx: ShutdownRx, bot_tx: Sender<BotMessage>) {
aide::gen::on_error(|error| {
panic!("Could not generate docs: {error}");
});
aide::gen::infer_responses(true);
aide::gen::extract_schemas(true);
let listen_address =
parse_listen_addr(&app.config.listen_address).expect("Invalid listen address");
let cors = CorsLayer::permissive();
let mut api = OpenApi::default();
let admin_routes = ApiRouter::new()
.api_route(
"/channels",
post_with(admin::add_channels, |op| {
op.tag("Admin").description("Join the specified channels")
})
.delete_with(admin::remove_channels, |op| {
op.tag("Admin").description("Leave the specified channels")
}),
)
.route_layer(middleware::from_fn_with_state(app.clone(), admin_auth))
.layer(Extension(bot_tx));
let app = ApiRouter::new()
.nest("/admin", admin_routes)
.api_route(
"/channels",
get_with(handlers::get_channels, |op| {
op.description("List logged channels")
}),
)
.api_route(
"/list",
get_with(handlers::list_available_logs, |op| {
op.description("List available logs")
}),
)
.api_route(
"/:channel_id_type/:channel",
get_with(handlers::redirect_to_latest_channel_logs, |op| {
op.description("Get latest channel logs")
}),
)
// For some reason axum considers it a path overlap if user id type is dynamic
.api_route(
"/:channel_id_type/:channel/user/:user",
get_with(handlers::redirect_to_latest_user_name_logs, |op| {
op.description("Get latest user logs")
}),
)
.api_route(
"/:channel_id_type/:channel/userid/:user",
get_with(handlers::redirect_to_latest_user_id_logs, |op| {
op.description("Get latest user logs")
}),
)
.api_route(
"/:channel_id_type/:channel/:year/:month/:day",
get_with(handlers::get_channel_logs, |op| {
op.description("Get channel logs from the given day")
}),
)
.api_route(
"/:channel_id_type/:channel/user/:user/:year/:month",
get_with(handlers::get_user_logs_by_name, |op| {
op.description("Get user logs in a channel from the given month")
}),
)
.api_route(
"/:channel_id_type/:channel/userid/:user/:year/:month",
get_with(handlers::get_user_logs_by_id, |op| {
op.description("Get user logs in a channel from the given month")
}),
)
.api_route(
"/:channel_id_type/:channel/random",
get_with(handlers::random_channel_line, |op| {
op.description("Get a random line from the channel's logs")
}),
)
.api_route(
"/:channel_id_type/:channel/userid/:user/random",
get_with(handlers::random_user_line_by_id, |op| {
op.description("Get a random line from the user's logs in a channel")
}),
)
.api_route(
"/:channel_id_type/:channel/user/:user/random",
get_with(handlers::random_user_line_by_name, |op| {
op.description("Get a random line from the user's logs in a channel")
}),
)
.api_route("/optout", post(handlers::optout))
.route("/docs", Redoc::new("/openapi.json").axum_route())
.layer(
TraceLayer::new_for_http()
.make_span_with(trace_layer::make_span_with)
.on_response(trace_layer::on_response),
)
.route("/openapi.json", get(serve_openapi))
.route("/metrics", get(metrics))
.route("/assets/*asset", get(frontend::static_asset))
.finish_api(&mut api)
.layer(Extension(Arc::new(api)))
.with_state(app)
.fallback(frontend::static_asset)
.layer(cors);
let app = NormalizePath::trim_trailing_slash(app);
info!("Listening on {listen_address}");
axum::Server::bind(&listen_address)
.serve(app.into_make_service())
.with_graceful_shutdown(async move {
shutdown_rx.changed().await.ok();
debug!("Shutting down web task");
})
.await
.unwrap();
}
pub fn parse_listen_addr(addr: &str) -> Result<SocketAddr, AddrParseError> {
if addr.starts_with(':') {
SocketAddr::from_str(&format!("0.0.0.0{addr}"))
} else {
SocketAddr::from_str(addr)
}
}
async fn metrics() -> String {
let metric_families = prometheus::gather();
let encoder = TextEncoder::new();
encoder.encode_to_string(&metric_families).unwrap()
}
async fn serve_openapi(Extension(api): Extension<Arc<OpenApi>>) -> impl IntoApiResponse {
Json(api.as_ref()).into_response()
}