208 lines
7.0 KiB
Rust
208 lines
7.0 KiB
Rust
mod admin;
|
|
mod frontend;
|
|
mod handlers;
|
|
mod responders;
|
|
pub mod schema;
|
|
mod trace_layer;
|
|
|
|
use self::handlers::no_cache_header;
|
|
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::{
|
|
http::Request,
|
|
middleware::{self, Next},
|
|
response::{IntoResponse, Response},
|
|
Extension, Json, ServiceExt,
|
|
};
|
|
use axum_prometheus::PrometheusMetricLayerBuilder;
|
|
use prometheus::TextEncoder;
|
|
use std::{
|
|
net::{AddrParseError, SocketAddr},
|
|
str::FromStr,
|
|
sync::Arc,
|
|
};
|
|
use tokio::sync::mpsc::Sender;
|
|
use tower_http::{
|
|
compression::CompressionLayer, cors::CorsLayer, normalize_path::NormalizePath,
|
|
trace::TraceLayer, CompressionLevel,
|
|
};
|
|
use tracing::{debug, info};
|
|
|
|
const CAPABILITIES: &[&str] = &["arbitrary-range-query"];
|
|
|
|
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);
|
|
|
|
metrics_prometheus::install();
|
|
|
|
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, |mut op| {
|
|
admin::admin_auth_doc(&mut op);
|
|
op.tag("Admin").description("Join the specified channels")
|
|
})
|
|
.delete_with(admin::remove_channels, |mut op| {
|
|
admin::admin_auth_doc(&mut 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::get_channel_logs, |op| {
|
|
op.description("Get channel logs. If the `to` and `from` query params are not given, redirect to latest available day")
|
|
}),
|
|
)
|
|
// 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::get_user_logs_by_name, |op| {
|
|
op.description("Get user logs by name. If the `to` and `from` query params are not given, redirect to latest available month")
|
|
}),
|
|
)
|
|
.api_route(
|
|
"/:channel_id_type/:channel/userid/:user",
|
|
get_with(handlers::get_user_logs_id, |op| {
|
|
op.description("Get user logs by id. If the `to` and `from` query params are not given, redirect to latest available month")
|
|
}),
|
|
)
|
|
.api_route(
|
|
"/:channel_id_type/:channel/:year/:month/:day",
|
|
get_with(handlers::get_channel_logs_by_date, |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_date_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_date_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))
|
|
.api_route("/capabilities", get(capabilities))
|
|
.route("/docs", Redoc::new("/openapi.json").axum_route())
|
|
.route("/openapi.json", get(serve_openapi))
|
|
.route("/assets/*asset", get(frontend::static_asset))
|
|
.fallback(frontend::static_asset)
|
|
.layer(middleware::from_fn(capabilities_header_middleware))
|
|
.layer(
|
|
TraceLayer::new_for_http()
|
|
.make_span_with(trace_layer::make_span_with)
|
|
.on_response(trace_layer::on_response),
|
|
)
|
|
.layer(
|
|
PrometheusMetricLayerBuilder::new()
|
|
.with_prefix("rustlog")
|
|
.build(),
|
|
)
|
|
.route("/metrics", get(metrics))
|
|
.finish_api(&mut api)
|
|
.layer(Extension(Arc::new(api)))
|
|
.with_state(app)
|
|
.layer(cors)
|
|
.layer(CompressionLayer::new().quality(CompressionLevel::Fastest));
|
|
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 capabilities() -> Json<Vec<&'static str>> {
|
|
Json(CAPABILITIES.to_vec())
|
|
}
|
|
|
|
async fn capabilities_header_middleware<B>(request: Request<B>, next: Next<B>) -> Response {
|
|
let mut response = next.run(request).await;
|
|
response.headers_mut().insert(
|
|
"x-rustlog-capabilities",
|
|
CAPABILITIES.join(",").try_into().unwrap(),
|
|
);
|
|
response
|
|
}
|
|
|
|
async fn metrics() -> impl IntoApiResponse {
|
|
let metric_families = prometheus::gather();
|
|
|
|
let encoder = TextEncoder::new();
|
|
let metrics = encoder.encode_to_string(&metric_families).unwrap();
|
|
(no_cache_header(), metrics)
|
|
}
|
|
async fn serve_openapi(Extension(api): Extension<Arc<OpenApi>>) -> impl IntoApiResponse {
|
|
Json(api.as_ref()).into_response()
|
|
}
|