Skip to main content

open_chess_server/http/
account.rs

1//! Account, user and bot endpoints.
2
3use 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 site.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
55/// Make this account a bot. Lichess's rules: irreversible, and only for an
56/// account that hasn't played a game.
57pub 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
79/// Bots seen in the last ten minutes, as NDJSON users.
80pub 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}