ars/src/main.rs

397 lines
11 KiB
Rust
Raw Normal View History

2023-11-01 21:12:18 +01:00
#![deny(clippy::unwrap_used)]
use std::{
collections::{BinaryHeap, HashMap},
env,
2023-11-06 18:38:00 +01:00
ops::Deref,
2023-11-01 21:12:18 +01:00
sync::Arc,
};
use axum::{
2023-11-03 19:48:48 +01:00
error_handling::HandleErrorLayer,
2023-11-06 18:38:00 +01:00
extract::{Multipart, Path, Query, State},
2023-11-10 14:22:57 +01:00
http::{header::CONTENT_TYPE, HeaderMap, HeaderValue, StatusCode, Uri},
2023-11-01 21:12:18 +01:00
response::{
sse::{Event, KeepAlive},
Html, IntoResponse, Redirect, Sse,
},
routing::get,
2023-11-03 19:48:48 +01:00
BoxError, Form, Router,
2023-11-01 21:12:18 +01:00
};
use axum_htmx::{HxRedirect, HxRequest};
2023-11-03 19:48:48 +01:00
use axum_oidc::{
2023-11-06 18:38:00 +01:00
error::MiddlewareError, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer,
2023-11-03 19:48:48 +01:00
};
2023-11-01 21:12:18 +01:00
use futures_util::Stream;
2023-11-10 14:22:57 +01:00
use game::{Game, GameCollector, GameId, PlayerId};
2023-11-01 21:12:18 +01:00
use garbage_collector::{start_gc, GarbageCollectorItem};
2023-11-10 14:22:57 +01:00
use prometheus_client::{
encoding::EncodeLabelSet,
metrics::{counter::Counter, family::Family, gauge::Gauge},
registry::Registry,
};
2023-11-06 18:38:00 +01:00
use question::{single_choice::SingleChoiceQuestion, Question};
2023-11-01 21:12:18 +01:00
use sailfish::TemplateOnce;
use serde::{Deserialize, Serialize};
use stream::{PlayerBroadcastStream, ViewerBroadcastStream};
use tokio::sync::RwLock;
2023-11-06 18:38:00 +01:00
use tower::ServiceBuilder;
2023-11-01 21:12:18 +01:00
use tower_http::services::ServeDir;
2023-11-06 18:38:00 +01:00
use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer};
2023-11-01 21:12:18 +01:00
use crate::error::Error;
type HandlerResult<T> = Result<T, Error>;
mod error;
mod game;
mod garbage_collector;
mod stream;
2023-11-02 22:46:38 +01:00
mod question;
2023-11-01 21:12:18 +01:00
#[derive(Clone)]
pub struct AppState {
2023-11-06 18:38:00 +01:00
games: Arc<RwLock<HashMap<GameId, Game>>>,
2023-11-01 21:12:18 +01:00
game_expiry: Arc<RwLock<BinaryHeap<GarbageCollectorItem>>>,
2023-11-06 18:38:00 +01:00
application_base: &'static str,
2023-11-10 14:22:57 +01:00
prometheus_registry: Arc<Registry>,
metrics: Arc<AppMetrics>,
}
#[derive(Clone, Default)]
pub struct AppMetrics {
arc_games_total: Counter,
2023-11-01 21:12:18 +01:00
}
#[tokio::main]
pub async fn main() {
dotenvy::dotenv().ok();
env_logger::init();
let application_base = env::var("APPLICATION_BASE").expect("APPLICATION_BASE env var");
let issuer = env::var("ISSUER").expect("ISSUER env var");
let client_id = env::var("CLIENT_ID").expect("CLIENT_ID env var");
let client_secret = env::var("CLIENT_SECRET").ok();
let scopes = env::var("SCOPES")
.expect("SCOPES env var")
.split(' ')
.map(|x| x.to_owned())
.collect::<Vec<_>>();
2023-11-03 19:48:48 +01:00
let session_store = MemoryStore::default();
let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(SessionManagerLayer::new(session_store).with_same_site(SameSite::Lax));
let oidc_login_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
e.into_response()
}))
.layer(OidcLoginLayer::<EmptyAdditionalClaims>::new());
let oidc_auth_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
e.into_response()
}))
.layer(
OidcAuthLayer::<EmptyAdditionalClaims>::discover_client(
Uri::from_maybe_shared(application_base.clone()).expect("valid APPLICATION_BASE"),
issuer.to_string(),
client_id.to_string(),
client_secret.to_owned(),
scopes.clone(),
)
.await
.expect("OIDC Client"),
);
2023-11-01 21:12:18 +01:00
let game_expiry: Arc<RwLock<BinaryHeap<GarbageCollectorItem>>> =
Arc::new(RwLock::new(BinaryHeap::new()));
let games = Arc::new(RwLock::new(HashMap::new()));
2023-11-10 14:22:57 +01:00
let app_metrics = Arc::new(AppMetrics::default());
let mut registry = Registry::default();
registry.register(
"arc_games_total",
"number of games created",
app_metrics.arc_games_total.clone(),
);
registry.register_collector(Box::new(GameCollector::new(games.clone())));
2023-11-01 21:12:18 +01:00
start_gc(game_expiry.clone(), games.clone());
let app_state = AppState {
games,
game_expiry,
2023-11-06 18:38:00 +01:00
application_base: Box::leak(application_base.into()),
2023-11-10 14:22:57 +01:00
prometheus_registry: Arc::new(registry),
metrics: app_metrics,
2023-11-01 21:12:18 +01:00
};
let app = Router::new()
.route("/", get(handle_index).post(handle_create))
.route("/:id/view", get(handle_view).post(handle_view_next))
.route("/:id/view/events", get(sse_view))
2023-11-03 19:48:48 +01:00
.layer(oidc_login_service)
.route("/:id", get(handle_player).post(handle_player_answer))
.route("/:id/events", get(sse_player))
2023-11-10 14:22:57 +01:00
.route("/metrics", get(metrics))
2023-11-01 21:12:18 +01:00
.nest_service("/static", ServeDir::new("static"))
2023-11-03 19:48:48 +01:00
.with_state(app_state)
.layer(oidc_auth_service)
.layer(session_service);
2023-11-01 21:12:18 +01:00
axum::Server::bind(&"[::]:8080".parse().expect("valid listen address"))
.serve(app.into_make_service())
.await
.expect("axum server");
}
2023-11-03 19:48:48 +01:00
pub async fn handle_index() -> HandlerResult<impl IntoResponse> {
2023-11-01 21:12:18 +01:00
Ok(Html(IndexTemplate {}.render_once()?))
}
pub async fn handle_create(
State(state): State<AppState>,
2023-11-03 19:48:48 +01:00
OidcClaims(claims): OidcClaims<EmptyAdditionalClaims>,
2023-11-01 21:12:18 +01:00
mut body: Multipart,
) -> HandlerResult<impl IntoResponse> {
let mut quiz: Option<Quiz> = None;
while let Some(field) = body.next_field().await? {
if field.name() == Some("quizfile") {
quiz = Some(toml::from_str::<Quiz>(&field.text().await?)?);
}
}
let quiz = quiz.ok_or(Error::QuizFileNotFound)?;
2023-11-06 18:38:00 +01:00
let game_id = GameId::random();
2023-11-01 21:12:18 +01:00
2023-11-03 19:48:48 +01:00
let game = Game::new(game_id.clone(), claims.subject().to_string(), quiz);
2023-11-01 21:12:18 +01:00
let mut games = state.games.write().await;
games.insert(game_id.clone(), game);
2023-11-06 18:38:00 +01:00
let url = format!("{}/{}/view", state.application_base, &game_id.deref());
2023-11-01 21:12:18 +01:00
let mut game_expiry = state.game_expiry.write().await;
game_expiry.push(GarbageCollectorItem::new_in(game_id, 24 * 3600));
2023-11-10 14:22:57 +01:00
state.metrics.arc_games_total.inc();
2023-11-01 21:12:18 +01:00
Ok((HxRedirect(Uri::from_maybe_shared(url.clone())?), "Ok"))
}
pub async fn handle_view(
2023-11-06 18:38:00 +01:00
Path(id): Path<GameId>,
2023-11-01 21:12:18 +01:00
State(state): State<AppState>,
HxRequest(htmx): HxRequest,
2023-11-03 19:48:48 +01:00
OidcClaims(claims): OidcClaims<EmptyAdditionalClaims>,
2023-11-01 21:12:18 +01:00
) -> HandlerResult<impl IntoResponse> {
let games = state.games.read().await;
let game = games.get(&id).ok_or(Error::NotFound)?;
2023-11-03 19:48:48 +01:00
if game.owner != claims.subject().to_string() {
2023-11-01 21:12:18 +01:00
return Err(Error::Forbidden);
}
2023-11-06 18:38:00 +01:00
Ok(Html(game.viewer_view(htmx, state.application_base).await?))
2023-11-01 21:12:18 +01:00
}
pub async fn handle_view_next(
2023-11-06 18:38:00 +01:00
Path(id): Path<GameId>,
2023-11-01 21:12:18 +01:00
State(state): State<AppState>,
2023-11-03 19:48:48 +01:00
OidcClaims(claims): OidcClaims<EmptyAdditionalClaims>,
2023-11-01 21:12:18 +01:00
) -> HandlerResult<impl IntoResponse> {
let mut games = state.games.write().await;
let game = games.get_mut(&id).ok_or(Error::NotFound)?;
2023-11-03 19:48:48 +01:00
if game.owner != claims.subject().to_string() {
2023-11-01 21:12:18 +01:00
return Err(Error::Forbidden);
}
game.next().await;
Ok("Ok".into_response())
}
pub async fn sse_view(
2023-11-06 18:38:00 +01:00
Path(id): Path<GameId>,
2023-11-01 21:12:18 +01:00
State(state): State<AppState>,
2023-11-03 19:48:48 +01:00
OidcClaims(claims): OidcClaims<EmptyAdditionalClaims>,
2023-11-01 21:12:18 +01:00
) -> HandlerResult<Sse<impl Stream<Item = Result<Event, Error>>>> {
let games = state.games.read().await;
let game = games.get(&id).ok_or(Error::NotFound)?;
2023-11-03 19:48:48 +01:00
if game.owner != claims.subject().to_string() {
2023-11-01 21:12:18 +01:00
return Err(Error::Forbidden);
}
let rx1 = game.on_state_update.subscribe();
let rx2 = game.on_submission.subscribe();
2023-11-06 18:38:00 +01:00
let stream =
ViewerBroadcastStream::new(rx1, rx2, state.games.clone(), id, state.application_base);
2023-11-01 21:12:18 +01:00
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
}
#[derive(Deserialize)]
pub struct PlayerQuery {
player: Option<String>,
}
2023-11-02 22:46:38 +01:00
pub async fn handle_player(
2023-11-01 21:12:18 +01:00
Query(query): Query<PlayerQuery>,
2023-11-06 18:38:00 +01:00
Path(id): Path<GameId>,
2023-11-01 21:12:18 +01:00
State(state): State<AppState>,
HxRequest(htmx): HxRequest,
) -> HandlerResult<impl IntoResponse> {
let mut games = state.games.write().await;
let game = games.get_mut(&id).ok_or(Error::NotFound)?;
2023-11-06 18:38:00 +01:00
if let Some(player_id) = query.player.map(PlayerId::from) {
2023-11-01 21:12:18 +01:00
Ok(Html(game.player_view(&player_id, htmx).await?).into_response())
} else {
2023-11-06 18:38:00 +01:00
let player_id = PlayerId::random();
game.players.insert(player_id.clone());
2023-11-01 21:12:18 +01:00
game.on_submission.send(());
Ok(Redirect::temporary(&format!(
"{}/{}?player={}",
2023-11-06 18:38:00 +01:00
state.application_base,
id.deref(),
player_id.deref()
2023-11-01 21:12:18 +01:00
))
.into_response())
}
}
#[derive(Deserialize)]
pub struct SubmissionPayload {
2023-11-06 18:38:00 +01:00
player_id: PlayerId,
2023-11-02 22:46:38 +01:00
#[serde(flatten)]
values: HashMap<String, String>,
2023-11-01 21:12:18 +01:00
}
2023-11-02 22:46:38 +01:00
pub async fn handle_player_answer(
2023-11-06 18:38:00 +01:00
Path(id): Path<GameId>,
2023-11-01 21:12:18 +01:00
State(state): State<AppState>,
Form(form): Form<SubmissionPayload>,
) -> HandlerResult<impl IntoResponse> {
let mut games = state.games.write().await;
let game = games.get_mut(&id).ok_or(Error::NotFound)?;
2023-11-02 22:46:38 +01:00
game.handle_answer(&form.player_id, &form.values).await?;
2023-11-01 21:12:18 +01:00
Ok(Html(game.player_view(&form.player_id, true).await?))
}
#[derive(Deserialize)]
pub struct SsePlayerQuery {
2023-11-06 18:38:00 +01:00
player: PlayerId,
2023-11-01 21:12:18 +01:00
}
2023-11-02 22:46:38 +01:00
pub async fn sse_player(
2023-11-01 21:12:18 +01:00
Query(query): Query<SsePlayerQuery>,
2023-11-06 18:38:00 +01:00
Path(id): Path<GameId>,
2023-11-01 21:12:18 +01:00
State(state): State<AppState>,
) -> HandlerResult<Sse<impl Stream<Item = Result<Event, Error>>>> {
let games = state.games.read().await;
let game = games.get(&id).ok_or(Error::NotFound)?;
let rx = game.on_state_update.subscribe();
let stream = PlayerBroadcastStream::new(rx, state.games.clone(), id, query.player);
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
}
2023-11-10 14:22:57 +01:00
async fn metrics(State(app_state): State<AppState>) -> HandlerResult<impl IntoResponse> {
let mut buffer = String::new();
prometheus_client::encoding::text::encode(&mut buffer, &app_state.prometheus_registry)
.map_err(Error::Prometheus)?;
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
HeaderValue::from_static("text/plain; version=0.0.4"),
);
Ok((headers, buffer))
}
2023-11-01 21:12:18 +01:00
#[derive(TemplateOnce)]
#[template(path = "index.stpl")]
struct IndexTemplate {}
#[derive(TemplateOnce)]
#[template(path = "play.stpl")]
struct PlayTemplate<'a> {
htmx: bool,
id: &'a str,
player_id: &'a str,
2023-11-02 22:46:38 +01:00
state: PlayerState,
2023-11-01 21:12:18 +01:00
}
2023-11-02 22:46:38 +01:00
pub enum PlayerState {
2023-11-01 21:12:18 +01:00
NotStarted,
2023-11-02 22:46:38 +01:00
Answering { inner_body: String },
2023-11-01 21:12:18 +01:00
Waiting(u32),
2023-11-02 22:46:38 +01:00
Result { inner_body: String },
2023-11-01 21:12:18 +01:00
Completed(f32),
}
#[derive(TemplateOnce)]
#[template(path = "view.stpl")]
struct ViewTemplate<'a> {
htmx: bool,
id: &'a str,
2023-11-02 22:46:38 +01:00
state: ViewerState,
2023-11-01 21:12:18 +01:00
}
2023-11-02 22:46:38 +01:00
pub enum ViewerState {
2023-11-01 21:12:18 +01:00
NotStarted((u32, String, String)),
2023-11-02 22:46:38 +01:00
Answering {
inner_body: String,
},
Result {
last_question: bool,
inner_body: String,
},
2023-11-01 21:12:18 +01:00
Completed,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Quiz {
pub wait_for: u64,
2023-11-02 22:46:38 +01:00
pub questions: Vec<QuizQuestion>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum QuizQuestion {
#[serde(rename = "single_choice")]
SingleChoice(SingleChoice),
2023-11-01 21:12:18 +01:00
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SingleChoice {
2023-11-06 18:38:00 +01:00
name: Box<str>,
answers: Box<[Box<str>]>,
2023-11-01 21:12:18 +01:00
correct: u32,
}
2023-11-02 22:46:38 +01:00
impl From<QuizQuestion> for Box<dyn Question> {
fn from(value: QuizQuestion) -> Self {
match value {
QuizQuestion::SingleChoice(x) => Box::new(SingleChoiceQuestion::new(x)) as _,
}
}
}