1use 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
19fn 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}