1use 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, Reader, 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> {
19 let c = ctx.db.conn().await?;
20 let a = model::ensure_account(&ctx.db, &*c, who.0.player).await?;
21 let mut v = serde_json::to_value(model::user_json(&ctx.db, &*c, &a, ctx.cfg.public_url()).await?)
22 .map_err(|e| ApiError::from(anyhow::Error::from(e)))?;
23 let r = c
24 .query_one(
25 &ctx.db.sql(
26 "SELECT role, roles, (SELECT count(*) FROM chess.passkeys WHERE player_id = $1)
27 FROM chess.players WHERE id = $1",
28 ),
29 &[&who.0.player],
30 )
31 .await?;
32 let (admin, manager) = crate::auth::staff_roles(&r.get::<_, String>(0), &r.get::<_, Vec<String>>(1));
33 let roles: Vec<&str> =
34 [(admin, "admin"), (manager, "manager")].iter().filter(|x| x.0).map(|x| x.1).collect();
35 v["roles"] = json!(roles);
36 v["passkeys"] = json!(r.get::<_, i64>(2));
37 if who.0.admin || who.0.manager {
38 v["staff"] = json!("checked");
39 } else if who.0.pending {
40 v["staff"] = json!("pending");
41 }
42 v["accountUrl"] = json!(format!("{}/account", ctx.cfg.public_url()));
43 Ok(Json(v).into_response())
44}
45
46pub async fn email(State(ctx): State<Ctx>, who: Who) -> ApiResult<Response> {
47 let id = who.need(&["email:read"])?;
48 let c = ctx.db.conn().await?;
49 let email: Option<String> = c
50 .query_opt(&ctx.db.sql("SELECT email FROM chess.players WHERE id = $1"), &[&id.player])
51 .await?
52 .and_then(|r| r.get(0));
53 Ok(Json(json!({"email": email.unwrap_or_default()})).into_response())
54}
55
56#[derive(Deserialize)]
57pub struct Nb {
58 nb: Option<usize>,
59}
60
61pub async fn playing(State(ctx): State<Ctx>, who: Who, Query(q): Query<Nb>) -> ApiResult<Response> {
62 let c = ctx.db.conn().await?;
63 let games = model::ongoing_games(&ctx.db, &*c, who.0.player).await?;
64 let now: Vec<chess_proto::GameEventInfo> = games
65 .iter()
66 .filter_map(|g| g.event_info(who.0.player))
67 .take(q.nb.unwrap_or(9).clamp(1, 50))
68 .collect();
69 let my_turn = now.iter().filter(|g| g.is_my_turn).count();
70 Ok(Json(chess_proto::NowPlaying { now_playing: now, nb_my_turn: my_turn as i64 }).into_response())
71}
72
73pub async fn user(
74 State(ctx): State<Ctx>,
75 _reader: Reader,
76 Path(username): Path<String>,
77) -> ApiResult<Response> {
78 let c = ctx.db.conn().await?;
79 let a =
80 model::account_by_name(&ctx.db, &*c, &username).await?.ok_or_else(|| ApiError::not_found("user"))?;
81 Ok(Json(model::user_json(&ctx.db, &*c, &a, ctx.cfg.public_url()).await?).into_response())
82}
83
84pub async fn upgrade(State(ctx): State<Ctx>, who: Who) -> ApiResult<Response> {
87 let id = who.need(&["bot:play"])?;
88 let c = ctx.db.conn().await?;
89 let a = model::ensure_account(&ctx.db, &*c, id.player).await?;
90 if a.bot {
91 return Err(ApiError::bad("This account is already a bot account"));
92 }
93 let played: bool = c
94 .query_one(
95 &ctx.db.sql("SELECT EXISTS (SELECT 1 FROM chess.games WHERE white_id = $1 OR black_id = $1)"),
96 &[&id.player],
97 )
98 .await?
99 .get(0);
100 if played {
101 return Err(ApiError::bad("This account has played games and can't be a bot account"));
102 }
103 c.execute(&ctx.db.sql("UPDATE chess.accounts SET kind = 'bot' WHERE player_id = $1"), &[&id.player])
104 .await?;
105 Ok(ok())
106}
107
108pub async fn bots_online(
110 State(ctx): State<Ctx>,
111 _reader: Reader,
112 Query(q): Query<Nb>,
113) -> ApiResult<Response> {
114 let c = ctx.db.conn().await?;
115 let rows = c
116 .query(
117 &ctx.db.sql(
118 "SELECT player_id FROM chess.accounts
119 WHERE kind = 'bot' AND seen_at > now() - interval '10 minutes'
120 ORDER BY seen_at DESC LIMIT $1",
121 ),
122 &[&(q.nb.unwrap_or(50).clamp(1, 300) as i64)],
123 )
124 .await?;
125 let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(rows.len().max(1));
126 for r in rows {
127 if let Some(a) = model::account(&ctx.db, &*c, r.get(0)).await? {
128 let u = model::user_json(&ctx.db, &*c, &a, ctx.cfg.public_url()).await?;
129 let _ = tx.try_send(line(&u));
130 }
131 }
132 drop(tx);
133 Ok(ndjson(rx))
134}
135
136#[derive(Debug, Deserialize)]
137pub struct Autocomplete {
138 term: Option<String>,
139 object: Option<bool>,
140 nb: Option<i64>,
141}
142
143pub async fn autocomplete(
149 State(ctx): State<Ctx>,
150 _reader: Reader,
151 Query(q): Query<Autocomplete>,
152) -> ApiResult<Response> {
153 let term = q.term.unwrap_or_default().trim().to_lowercase();
154 if term.is_empty() {
155 return Ok(
156 Json(if q.object.unwrap_or(false) { json!({ "result": [] }) } else { json!([]) }).into_response()
157 );
158 }
159 let like = format!("%{}%", term.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_"));
160 let c = ctx.db.conn().await?;
161 let rows = c
162 .query(
163 &ctx.db.sql(
164 "SELECT username, seen_at > now() - interval '10 minutes' AS online
165 FROM chess.accounts
166 WHERE lower(username) LIKE $1 AND username !~ '^ai-level-[0-9]$'
167 ORDER BY (lower(username) LIKE $2) DESC, lower(username)
168 LIMIT $3",
169 ),
170 &[&like, &format!("{}%", &like[1..]), &q.nb.unwrap_or(12).clamp(1, 50)],
171 )
172 .await?;
173 let names: Vec<(String, bool)> = rows
174 .iter()
175 .map(|r| (r.get("username"), r.get::<_, Option<bool>>("online").unwrap_or(false)))
176 .collect();
177 Ok(Json(if q.object.unwrap_or(false) {
178 json!({ "result": names.iter().map(|(n, on)| json!({ "id": n.to_lowercase(), "name": n, "online": on })).collect::<Vec<_>>() })
179 } else {
180 json!(names.iter().map(|(n, _)| n).collect::<Vec<_>>())
181 })
182 .into_response())
183}