//! REST client for the tavolo backend. Same-origin requests carry the //! session cookie automatically. use crate::model::*; use gloo_net::http::Request; fn server_error(status: u16) -> String { format!("server returned {status}") } /// Fetch the current user; `None` when unauthenticated (401). pub async fn me() -> Result, String> { let resp = Request::get("/api/me") .send() .await .map_err(|e| e.to_string())?; if resp.status() == 401 { return Ok(None); } if !resp.ok() { return Err(server_error(resp.status())); } resp.json().await.map(Some).map_err(|e| e.to_string()) } /// Fetch the configured OIDC login providers (public endpoint). pub async fn auth_providers() -> Result, String> { let resp = Request::get("/api/auth/providers") .send() .await .map_err(|e| e.to_string())?; if !resp.ok() { return Err(server_error(resp.status())); } let page: AuthProvidersPage = resp.json().await.map_err(|e| e.to_string())?; Ok(page.providers) } /// Fetch the card games the platform can host (for the creation dropdown). pub async fn game_types() -> Result, String> { let resp = Request::get("/api/game-types") .send() .await .map_err(|e| e.to_string())?; if !resp.ok() { return Err(server_error(resp.status())); } let page: GameTypesPage = resp.json().await.map_err(|e| e.to_string())?; Ok(page.results) } /// Create a lobby game. `options` is the game-specific creation object /// described by the engine's `options_schema` (see /// [`crate::model::GameTypeInfo::option_fields`]). pub async fn create_game(game_type: &str, options: serde_json::Value) -> Result { let resp = Request::post("/api/games") .json(&serde_json::json!({ "game_type": game_type, "options": options })) .map_err(|e| e.to_string())? .send() .await .map_err(|e| e.to_string())?; if !resp.ok() { return Err(server_error(resp.status())); } resp.json().await.map_err(|e| e.to_string()) } pub async fn join_game(code: &str) -> Result { let resp = Request::post("/api/games/join") .json(&serde_json::json!({ "code": code })) .map_err(|e| e.to_string())? .send() .await .map_err(|e| e.to_string())?; if !resp.ok() { let body: serde_json::Value = resp.json().await.unwrap_or_default(); let message = body .get("error") .and_then(|e| e.as_str()) .map(str::to_string) .unwrap_or_else(|| server_error(resp.status())); return Err(message); } resp.json().await.map_err(|e| e.to_string()) } /// Look up a game by join code without joining, for the pre-join summary. /// `Ok(None)` means the code names no live game (404); other failures are /// `Err`. pub async fn join_preview(code: &str) -> Result, String> { let resp = Request::get(&format!("/api/games/preview?code={code}")) .send() .await .map_err(|e| e.to_string())?; if resp.status() == 404 { return Ok(None); } if !resp.ok() { return Err(server_error(resp.status())); } resp.json().await.map(Some).map_err(|e| e.to_string()) } /// Why a game connection failed before the first snapshot arrived. /// /// A refused WebSocket handshake is invisible to the browser: the close /// event reports an abnormal closure (1006) with no HTTP status, so the /// game page asks the REST API to tell a deliberate rejection from a /// transient network problem. pub enum GameEntry { /// Not logged in (`401` from `GET /api/games/{id}`). Unauthenticated, /// Unknown game (`404`) or the user is not seated (`403`). Unavailable, /// The API could not be asked (or answered unexpectedly): treat the /// connection loss as transient and retry. Transient, } /// Classify a failed game connection via `GET /api/games/{game_id}`, /// mirroring the statuses used to refuse the websocket upgrade. pub async fn probe_game(game_id: &str) -> GameEntry { match Request::get(&format!("/api/games/{game_id}")).send().await { Ok(resp) => match resp.status() { 401 => GameEntry::Unauthenticated, 403 | 404 => GameEntry::Unavailable, _ => GameEntry::Transient, }, Err(_) => GameEntry::Transient, } } pub async fn my_matches(cursor: Option<&str>) -> Result { let url = match cursor { Some(c) => format!("/api/me/matches?limit=10&cursor={c}"), None => "/api/me/matches?limit=10".to_string(), }; let resp = Request::get(&url).send().await.map_err(|e| e.to_string())?; if !resp.ok() { return Err(server_error(resp.status())); } resp.json().await.map_err(|e| e.to_string()) } /// Fetch the caller's Elo ratings (one row per game type played). pub async fn my_ratings() -> Result { let resp = Request::get("/api/me/ratings") .send() .await .map_err(|e| e.to_string())?; if !resp.ok() { return Err(server_error(resp.status())); } resp.json().await.map_err(|e| e.to_string()) } pub async fn leaderboard() -> Result { let resp = Request::get("/api/leaderboard") .send() .await .map_err(|e| e.to_string())?; if !resp.ok() { return Err(server_error(resp.status())); } resp.json().await.map_err(|e| e.to_string()) }