caveman/gatherer/src/http_server.rs

245 lines
6.9 KiB
Rust
Raw Normal View History

use std::{
2024-03-27 02:11:35 +01:00
collections::{HashMap, HashSet}, net::SocketAddr, ops::Deref
};
2022-11-06 01:29:58 +01:00
use askama::Template;
use axum::{
async_trait,
Extension,
extract::{self, FromRequestParts},
http::{StatusCode, Request, Response, request::Parts},
2022-11-06 02:20:22 +01:00
response::IntoResponse,
2022-11-06 01:29:58 +01:00
routing::get,
2022-12-30 02:59:09 +01:00
Router,
middleware::{Next, self},
2024-03-27 02:11:35 +01:00
body::{Body},
2022-11-06 01:29:58 +01:00
};
2024-03-27 02:11:35 +01:00
use futures::{stream, StreamExt};
2022-12-30 01:56:20 +01:00
use futures::future::{join, join_all};
2022-11-16 19:12:25 +01:00
use cave::{
firehose::FirehoseFactory,
2022-12-30 01:56:20 +01:00
store::{Store, TREND_POOL_SIZE}, PERIODS,
2023-08-08 18:42:34 +02:00
systemd, db::Database,
2022-11-16 19:12:25 +01:00
};
2024-03-27 02:11:35 +01:00
2022-12-30 02:59:09 +01:00
use metrics_exporter_prometheus::PrometheusHandle;
2022-11-06 01:29:58 +01:00
use crate::{
html_template::HtmlTemplate,
2022-12-30 01:56:20 +01:00
trends::{TrendAnalyzer, TrendsResults},
2022-11-06 01:29:58 +01:00
};
use tower_http::services::ServeDir;
2022-11-06 01:29:58 +01:00
2023-08-08 18:42:34 +02:00
mod token_donate;
mod token_collect;
2022-12-30 01:56:20 +01:00
type Languages = Vec<String>;
#[derive(Clone)]
pub struct ServerState {
store: Store,
2023-08-08 18:42:34 +02:00
db: Database,
2023-08-09 00:05:26 +02:00
http_client: reqwest::Client,
2022-12-30 01:56:20 +01:00
}
impl ServerState {
async fn query_trends(&self, language: Option<String>) -> (TrendsResults, Languages, HashMap<String, String>) {
2022-12-30 01:56:20 +01:00
let mut store = self.store.clone();
let mut store_ = self.store.clone();
let (results, mut languages) = join(async move {
TrendAnalyzer::run(&mut store_, TREND_POOL_SIZE, PERIODS, language)
.await
.unwrap()
}, async {
store.get_languages()
.await
.unwrap()
}).await;
languages.sort();
let tags = results.iter()
.flat_map(|(_until, _period, result)| result.iter().map(|(_score, tag)| tag.name.to_string()))
.collect::<HashSet<String>>();
let tag_images = join_all(tags.into_iter().map(|name| {
let mut store = store.clone();
async move {
let images = store.get_tag_images(&name)
.await
.unwrap()
.into_iter()
.enumerate()
.flat_map(|(i, url)| if i == 0 {
["".to_owned(), url]
} else {
[" ".to_owned(), url]
})
.collect::<String>();
(name, images)
}
})).await.into_iter().collect();
(results, languages, tag_images)
}
}
2022-11-06 01:29:58 +01:00
#[async_trait]
impl<S> FromRequestParts<S> for ServerState
2022-11-06 01:29:58 +01:00
where
S: Send + Sync,
2022-11-06 01:29:58 +01:00
{
type Rejection = (StatusCode, String);
async fn from_request_parts<'life0, 'life1>(
parts: &'life0 mut Parts,
state: &'life1 S
) -> Result<Self, Self::Rejection> {
let Extension(state) = Extension::<ServerState>::from_request_parts(parts, state)
2022-11-06 01:29:58 +01:00
.await
.map_err(internal_error)?;
2022-12-30 01:56:20 +01:00
Ok(state)
2022-11-06 01:29:58 +01:00
}
}
/// Utility function for mapping any error into a `500 Internal Server Error`
/// response.
fn internal_error<E>(err: E) -> (StatusCode, String)
where
E: std::error::Error,
{
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
}
#[derive(Template)]
#[template(path = "trends.html")]
struct TrendsPage {
language: Option<String>,
2022-11-10 02:47:09 +01:00
languages: Vec<String>,
2022-11-06 01:29:58 +01:00
results: TrendsResults,
2022-11-29 01:51:22 +01:00
tag_images: HashMap<String, String>,
2022-11-06 01:29:58 +01:00
}
impl TrendsPage {
2022-12-30 01:56:20 +01:00
async fn generate(language: Option<String>, state: ServerState) -> Self {
let (results, languages, tag_images) = state.query_trends(language.clone()).await;
2022-11-06 01:29:58 +01:00
// redis queries done, data is ready for rendering, means the
// service is very much alive:
systemd::watchdog();
2022-11-06 01:29:58 +01:00
TrendsPage {
2022-11-29 01:51:22 +01:00
results,
2022-11-06 01:29:58 +01:00
language,
2022-11-10 02:47:09 +01:00
languages,
2022-11-29 01:51:22 +01:00
tag_images,
2022-11-06 01:29:58 +01:00
}
}
fn template(self) -> HtmlTemplate<Self> {
HtmlTemplate(self)
}
}
2022-12-30 02:59:09 +01:00
async fn trends_page(
state: ServerState,
language: Option<String>,
) -> Response<Body> {
2022-12-30 02:59:09 +01:00
let lang = if language.is_some() { "some" } else { "any" };
let page = TrendsPage::generate(language, state)
.await;
let res = page.template().into_response();
metrics::increment_counter!("trends_page_requests", "lang" => lang);
res
}
async fn home(Extension(state): Extension<ServerState>) -> Response<Body> {
2022-12-30 02:59:09 +01:00
trends_page(state, None).await
2022-11-06 01:29:58 +01:00
}
async fn in_language(
2022-12-30 01:56:20 +01:00
Extension(state): Extension<ServerState>,
2022-11-06 01:29:58 +01:00
extract::Path(language): extract::Path<String>,
) -> Response<Body> {
2022-12-30 02:59:09 +01:00
trends_page(state, Some(language)).await
2022-11-06 01:29:58 +01:00
}
2022-11-15 00:45:02 +01:00
async fn streaming_api(
Extension(firehose_factory): Extension<FirehoseFactory>,
) -> Response<Body> {
2022-11-15 00:45:02 +01:00
let firehose = firehose_factory.produce()
.await
.expect("firehose");
2023-10-01 23:19:00 +02:00
let stream = stream::once(async { Ok::<Vec<u8>, axum::Error>(b":)\n".to_vec()) })
2022-11-15 00:45:02 +01:00
.chain(
2023-10-01 23:19:00 +02:00
firehose.flat_map(|(event_type, data)|
stream::iter([
Ok(b"event: ".to_vec()),
Ok(event_type),
Ok(b"\ndata: ".to_vec()),
Ok(data),
Ok(b"\n\n".to_vec()),
].into_iter()).boxed())
2022-11-15 00:45:02 +01:00
);
let body = Body::from_stream(stream);
2022-11-15 00:45:02 +01:00
Response::builder()
.status(200)
.header("content-type", "text/event-stream")
.header("cache-control", "no-store")
2024-03-27 02:11:35 +01:00
.body(body)
2022-11-15 00:45:02 +01:00
.expect("Response")
}
2022-11-14 20:54:30 +01:00
async fn print_request(
req: Request<Body>,
next: Next,
2022-11-14 20:54:30 +01:00
) -> Result<impl IntoResponse, (StatusCode, String)> {
2022-12-01 01:39:38 +01:00
tracing::info!(
2022-11-14 20:54:30 +01:00
"{} {} {:?}",
req.method(),
req.uri(),
req.headers().get("user-agent")
.and_then(|ua| ua.to_str().ok())
.unwrap_or("-")
);
let res = next.run(req).await;
Ok(res)
}
2022-12-30 02:59:09 +01:00
pub async fn start(
listen_port: u16,
store: Store,
2023-08-08 18:42:34 +02:00
db: Database,
2023-08-09 00:05:26 +02:00
http_client: reqwest::Client,
2022-12-30 02:59:09 +01:00
firehose_factory: FirehoseFactory,
recorder: PrometheusHandle,
) {
2022-11-06 01:29:58 +01:00
cave::systemd::status("Starting HTTP server");
// build our application with some routes
let app = Router::new()
.route("/", get(home))
.route("/in/:language", get(in_language))
2022-11-15 00:45:02 +01:00
.route("/api/v1/streaming/public", get(streaming_api))
2023-08-08 18:42:34 +02:00
.route("/token/donate", get(token_donate::get_token_donate).post(token_donate::post_token_donate))
.route("/token/collect/:host", get(token_collect::get_token_collect))
.route("/token/thanks", get(token_collect::get_token_thanks))
2023-08-09 00:05:26 +02:00
.layer(Extension(ServerState { store, db, http_client }))
2022-11-15 00:45:02 +01:00
.layer(Extension(firehose_factory))
2022-12-30 02:59:09 +01:00
.route("/metrics", get(|| async move {
recorder.render().into_response()
}))
2022-11-14 20:54:30 +01:00
.layer(middleware::from_fn(print_request))
.nest_service("/assets", ServeDir::new("assets"));
2022-11-06 01:29:58 +01:00
// run it
2023-08-08 22:06:06 +02:00
let addr = SocketAddr::from(([0, 0, 0, 0], listen_port));
let listener = tokio::net::TcpListener::bind(&addr)
.await
.unwrap();
axum::serve(listener, app.into_make_service())
2022-11-06 01:29:58 +01:00
.await
.unwrap();
}