open_chess_server/http/
account.rs1use axum::Json;
4use axum::extract::{Path, Query, State};
5use axum::response::{IntoResponse, Response};
6use bytes::Bytes;
7use serde::Deserialize;
8use serde_json::json;
9
10use crate::app::{Ctx, Who};
11use crate::error::{ApiError, ApiResult};
12use crate::http::{line, ndjson, ok};
13use crate::model;
14
15pub async fn me(State(ctx): State<Ctx>, who: Who) -> ApiResult<Response> {
16 let c = ctx.db.conn().await?;
17 let a = model::ensure_account(&ctx.db, &*c, who.0.player).await?;
18 Ok(Json(model::user_json(&ctx.db, &*c, &a, ctx.cfg.public_url()).await?).into_response())
19}
20
21pub async fn email(State(ctx): State<Ctx>, who: Who) -> ApiResult<Response> {
22 let id = who.need(&["email:read"])?;
23 let c = ctx.db.conn().await?;
24 let email: Option<String> = c
25 .query_opt(&ctx.db.sql("SELECT email FROM chess.players WHERE id = $1"), &[&id.player])
26 .await?
27 .and_then(|r| r.get(0));
28 Ok(Json(json!({"email": email.unwrap_or_default()})).into_response())
29}
30
31#[derive(Deserialize)]
32pub struct Nb {
33 nb: Option<usize>,
34}
35
36pub async fn playing(State(ctx): State<Ctx>, who: Who, Query(q): Query<Nb>) -> ApiResult<Response> {
37 let c = ctx.db.conn().await?;
38 let games = model::ongoing_games(&ctx.db, &*c, who.0.player).await?;
39 let now: Vec<chess_proto::GameEventInfo> = games
40 .iter()
41 .filter_map(|g| g.event_info(who.0.player))
42 .take(q.nb.unwrap_or(9).clamp(1, 50))
43 .collect();
44 let my_turn = now.iter().filter(|g| g.is_my_turn).count();
45 Ok(Json(chess_proto::NowPlaying { now_playing: now, nb_my_turn: my_turn as i64 }).into_response())
46}
47
48pub async fn user(State(ctx): State<Ctx>, Path(username): Path<String>) -> ApiResult<Response> {
49 let c = ctx.db.conn().await?;
50 let a =
51 model::account_by_name(&ctx.db, &*c, &username).await?.ok_or_else(|| ApiError::not_found("user"))?;
52 Ok(Json(model::user_json(&ctx.db, &*c, &a, ctx.cfg.public_url()).await?).into_response())
53}
54
55pub async fn upgrade(State(ctx): State<Ctx>, who: Who) -> ApiResult<Response> {
58 let id = who.need(&["bot:play"])?;
59 let c = ctx.db.conn().await?;
60 let a = model::ensure_account(&ctx.db, &*c, id.player).await?;
61 if a.bot {
62 return Err(ApiError::bad("This account is already a bot account"));
63 }
64 let played: bool = c
65 .query_one(
66 &ctx.db.sql("SELECT EXISTS (SELECT 1 FROM chess.games WHERE white_id = $1 OR black_id = $1)"),
67 &[&id.player],
68 )
69 .await?
70 .get(0);
71 if played {
72 return Err(ApiError::bad("This account has played games and can't be a bot account"));
73 }
74 c.execute(&ctx.db.sql("UPDATE chess.accounts SET kind = 'bot' WHERE player_id = $1"), &[&id.player])
75 .await?;
76 Ok(ok())
77}
78
79pub async fn bots_online(State(ctx): State<Ctx>, Query(q): Query<Nb>) -> ApiResult<Response> {
81 let c = ctx.db.conn().await?;
82 let rows = c
83 .query(
84 &ctx.db.sql(
85 "SELECT player_id FROM chess.accounts
86 WHERE kind = 'bot' AND seen_at > now() - interval '10 minutes'
87 ORDER BY seen_at DESC LIMIT $1",
88 ),
89 &[&(q.nb.unwrap_or(50).clamp(1, 300) as i64)],
90 )
91 .await?;
92 let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(rows.len().max(1));
93 for r in rows {
94 if let Some(a) = model::account(&ctx.db, &*c, r.get(0)).await? {
95 let u = model::user_json(&ctx.db, &*c, &a, ctx.cfg.public_url()).await?;
96 let _ = tx.try_send(line(&u));
97 }
98 }
99 drop(tx);
100 Ok(ndjson(rx))
101}
102
103#[derive(Debug, Deserialize)]
104pub struct Autocomplete {
105 term: Option<String>,
106 object: Option<bool>,
107 nb: Option<i64>,
108}
109
110pub async fn autocomplete(State(ctx): State<Ctx>, Query(q): Query<Autocomplete>) -> ApiResult<Response> {
116 let term = q.term.unwrap_or_default().trim().to_lowercase();
117 if term.is_empty() {
118 return Ok(
119 Json(if q.object.unwrap_or(false) { json!({ "result": [] }) } else { json!([]) }).into_response()
120 );
121 }
122 let like = format!("%{}%", term.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_"));
123 let c = ctx.db.conn().await?;
124 let rows = c
125 .query(
126 &ctx.db.sql(
127 "SELECT username, seen_at > now() - interval '10 minutes' AS online
128 FROM chess.accounts
129 WHERE lower(username) LIKE $1 AND username !~ '^ai-level-[0-9]$'
130 ORDER BY (lower(username) LIKE $2) DESC, lower(username)
131 LIMIT $3",
132 ),
133 &[&like, &format!("{}%", &like[1..]), &q.nb.unwrap_or(12).clamp(1, 50)],
134 )
135 .await?;
136 let names: Vec<(String, bool)> = rows
137 .iter()
138 .map(|r| (r.get("username"), r.get::<_, Option<bool>>("online").unwrap_or(false)))
139 .collect();
140 Ok(Json(if q.object.unwrap_or(false) {
141 json!({ "result": names.iter().map(|(n, on)| json!({ "id": n.to_lowercase(), "name": n, "online": on })).collect::<Vec<_>>() })
142 } else {
143 json!(names.iter().map(|(n, _)| n).collect::<Vec<_>>())
144 })
145 .into_response())
146}