Skip to main content

open_chess_server/http/
play.rs

1//! Board API and Bot API game actions (the same handlers serve both; the
2//! matched path says which).
3
4use std::collections::HashMap;
5
6use axum::Json;
7use axum::extract::{MatchedPath, Path, Query, State};
8use axum::response::{IntoResponse, Response};
9use bytes::Bytes;
10use serde_json::json;
11
12use crate::app::{Ctx, Who};
13use crate::db::{Note, notify};
14use crate::error::{ApiError, ApiResult};
15use crate::http::{form, ok};
16use crate::model;
17use crate::play::{self, Api};
18
19/// Which API was called, and the token scope it needs.
20fn api(path: &MatchedPath, who: &Who) -> ApiResult<(Api, i64)> {
21    let (api, scope) =
22        if path.as_str().contains("/api/bot/") { (Api::Bot, "bot:play") } else { (Api::Board, "board:play") };
23    let id = who.need(&[scope])?;
24    Ok((api, id.player))
25}
26
27fn yes(s: &str) -> ApiResult<bool> {
28    match s {
29        "yes" | "true" | "accept" => Ok(true),
30        "no" | "false" | "decline" => Ok(false),
31        _ => Err(ApiError::bad("say yes or no")),
32    }
33}
34
35pub async fn make_move(
36    State(ctx): State<Ctx>,
37    mp: MatchedPath,
38    who: Who,
39    Path((id, uci)): Path<(String, String)>,
40    Query(q): Query<HashMap<String, String>>,
41) -> ApiResult<Response> {
42    let (api, player) = api(&mp, &who)?;
43    let offering = q.get("offeringDraw").is_some_and(|v| v == "true" || v == "1");
44    play::make_move(&ctx, &id, player, api, &uci, offering).await?;
45    Ok(ok())
46}
47
48pub async fn resign(
49    State(ctx): State<Ctx>,
50    mp: MatchedPath,
51    who: Who,
52    Path(id): Path<String>,
53) -> ApiResult<Response> {
54    let (api, player) = api(&mp, &who)?;
55    play::resign(&ctx, &id, player, api).await?;
56    Ok(ok())
57}
58
59pub async fn abort(
60    State(ctx): State<Ctx>,
61    mp: MatchedPath,
62    who: Who,
63    Path(id): Path<String>,
64) -> ApiResult<Response> {
65    let (api, player) = api(&mp, &who)?;
66    play::abort(&ctx, &id, player, api).await?;
67    Ok(ok())
68}
69
70pub async fn draw(
71    State(ctx): State<Ctx>,
72    mp: MatchedPath,
73    who: Who,
74    Path((id, accept)): Path<(String, String)>,
75) -> ApiResult<Response> {
76    let (api, player) = api(&mp, &who)?;
77    play::draw(&ctx, &id, player, api, yes(&accept)?).await?;
78    Ok(ok())
79}
80
81pub async fn takeback(
82    State(ctx): State<Ctx>,
83    mp: MatchedPath,
84    who: Who,
85    Path((id, accept)): Path<(String, String)>,
86) -> ApiResult<Response> {
87    let (api, player) = api(&mp, &who)?;
88    play::takeback(&ctx, &id, player, api, yes(&accept)?).await?;
89    Ok(ok())
90}
91
92pub async fn claim_victory(
93    State(ctx): State<Ctx>,
94    mp: MatchedPath,
95    who: Who,
96    Path(id): Path<String>,
97) -> ApiResult<Response> {
98    let (api, player) = api(&mp, &who)?;
99    play::claim_victory(&ctx, &id, player, api).await?;
100    Ok(ok())
101}
102
103pub async fn claim_draw(
104    State(ctx): State<Ctx>,
105    mp: MatchedPath,
106    who: Who,
107    Path(id): Path<String>,
108) -> ApiResult<Response> {
109    let (api, player) = api(&mp, &who)?;
110    play::claim_draw(&ctx, &id, player, api).await?;
111    Ok(ok())
112}
113
114pub async fn chat_post(
115    State(ctx): State<Ctx>,
116    mp: MatchedPath,
117    who: Who,
118    Path(id): Path<String>,
119    body: Bytes,
120) -> ApiResult<Response> {
121    let (api, player) = api(&mp, &who)?;
122    let f = form(&body);
123    let room = f.get("room").map_or("player", String::as_str);
124    if !matches!(room, "player" | "spectator") {
125        return Err(ApiError::bad("room is player or spectator"));
126    }
127    let text: String = f.get("text").map(|t| t.trim().chars().take(140).collect()).unwrap_or_default();
128    if text.is_empty() {
129        return Err(ApiError::bad("say something"));
130    }
131    let mut c = ctx.db.conn().await?;
132    let tx = c.transaction().await?;
133    let row = model::game(&ctx.db, &tx, &id).await?.ok_or_else(|| ApiError::not_found("game"))?;
134    let side = row.side_of(player).ok_or_else(|| ApiError::not_found("game"))?;
135    api.check(row.player(side))?;
136    let username = row.player(side).username.clone();
137    tx.execute(
138        &ctx.db.sql(
139            "INSERT INTO chess.chat (game_id, room, player_id, username, text) VALUES ($1, $2, $3, $4, $5)",
140        ),
141        &[&id, &room, &player, &username, &text],
142    )
143    .await?;
144    let line = json!({"type": "chatLine", "room": room, "username": username, "text": text});
145    notify(&tx, &ctx.db, &Note::Chat { game: id, line }).await?;
146    tx.commit().await?;
147    Ok(ok())
148}
149
150pub async fn chat_get(
151    State(ctx): State<Ctx>,
152    mp: MatchedPath,
153    who: Who,
154    Path(id): Path<String>,
155) -> ApiResult<Response> {
156    let (_, player) = api(&mp, &who)?;
157    let c = ctx.db.conn().await?;
158    let row = model::game(&ctx.db, &*c, &id).await?.ok_or_else(|| ApiError::not_found("game"))?;
159    row.side_of(player).ok_or_else(|| ApiError::not_found("game"))?;
160    let lines: Vec<serde_json::Value> = c
161        .query(
162            &ctx.db.sql(
163                "SELECT username, text FROM chess.chat WHERE game_id = $1 AND room = 'player' ORDER BY id",
164            ),
165            &[&id],
166        )
167        .await?
168        .iter()
169        .map(|r| json!({"text": r.get::<_, String>(1), "user": r.get::<_, String>(0)}))
170        .collect();
171    Ok(Json(lines).into_response())
172}