use futures_util::stream::BoxStream;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::api::tournaments::arena::LichessArena;
use crate::api::tournaments::swiss::LichessSwiss;
use crate::client::LichessClient;
use crate::config::Host;
use crate::error::Result;
use crate::http;
use crate::model::{LichessLightUser, LichessUser};
use crate::secret::Secret;
#[derive(Debug, Serialize)]
struct JoinForm<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
password: Option<Secret<&'a str>>,
}
#[derive(Debug)]
pub struct TeamsApi<'a> {
client: &'a LichessClient,
}
impl<'a> TeamsApi<'a> {
pub(crate) fn new(client: &'a LichessClient) -> Self {
Self { client }
}
pub async fn get(&self, team_id: &str) -> Result<LichessTeam> {
let path = format!("/api/team/{}", http::segment(team_id));
let request = self.client.request(Method::GET, Host::Default, &path);
http::json(request, "LichessTeam").await
}
pub async fn all(&self, page: u32) -> Result<LichessTeamPaginator> {
let request = self
.client
.request(Method::GET, Host::Default, "/api/team/all")
.query(&[("page", page)]);
http::json(request, "LichessTeamPaginator").await
}
pub async fn of_user(&self, username: &str) -> Result<Vec<LichessTeam>> {
let path = format!("/api/team/of/{}", http::segment(username));
let request = self.client.request(Method::GET, Host::Default, &path);
http::json(request, "Vec<LichessTeam>").await
}
pub async fn search(&self, text: &str, page: u32) -> Result<LichessTeamPaginator> {
let request = self
.client
.request(Method::GET, Host::Default, "/api/team/search")
.query(&[("text", text), ("page", &page.to_string())]);
http::json(request, "LichessTeamPaginator").await
}
pub async fn members(
&self,
team_id: &str,
full: Option<bool>,
) -> Result<BoxStream<'static, Result<LichessUser>>> {
let path = format!("/api/team/{}/users", http::segment(team_id));
let request = self
.client
.request(Method::GET, Host::Default, &path)
.query(&[("full", full)]);
http::stream(request, self.client.max_line_bytes()).await
}
pub async fn join(
&self,
team_id: &str,
message: Option<&str>,
password: Option<&str>,
) -> Result<()> {
let path = format!("/team/{}/join", http::segment(team_id));
let request = self
.client
.request(Method::POST, Host::Default, &path)
.form(&JoinForm {
message,
password: password.map(Secret::new),
});
http::ok(request).await
}
pub async fn quit(&self, team_id: &str) -> Result<()> {
let path = format!("/team/{}/quit", http::segment(team_id));
http::ok(self.client.request(Method::POST, Host::Default, &path)).await
}
pub async fn kick(&self, team_id: &str, user_id: &str) -> Result<()> {
let path = format!(
"/api/team/{}/kick/{}",
http::segment(team_id),
http::segment(user_id)
);
http::ok(self.client.request(Method::POST, Host::Default, &path)).await
}
pub async fn message_all(&self, team_id: &str, message: &str) -> Result<()> {
let path = format!("/team/{}/pm-all", http::segment(team_id));
let request = self
.client
.request(Method::POST, Host::Default, &path)
.form(&[("message", message)]);
http::ok(request).await
}
pub async fn join_requests(
&self,
team_id: &str,
declined: Option<bool>,
) -> Result<Vec<LichessTeamRequestWithUser>> {
let path = format!("/api/team/{}/requests", http::segment(team_id));
let request = self
.client
.request(Method::GET, Host::Default, &path)
.query(&[("declined", declined)]);
http::json(request, "Vec<LichessTeamRequestWithUser>").await
}
pub async fn accept_request(&self, team_id: &str, user_id: &str) -> Result<()> {
let path = format!(
"/api/team/{}/request/{}/accept",
http::segment(team_id),
http::segment(user_id)
);
http::ok(self.client.request(Method::POST, Host::Default, &path)).await
}
pub async fn decline_request(&self, team_id: &str, user_id: &str) -> Result<()> {
let path = format!(
"/api/team/{}/request/{}/decline",
http::segment(team_id),
http::segment(user_id)
);
http::ok(self.client.request(Method::POST, Host::Default, &path)).await
}
pub async fn arena_tournaments(
&self,
team_id: &str,
query: &TeamTournamentQuery<'_>,
) -> Result<BoxStream<'static, Result<LichessArena>>> {
let path = format!("/api/team/{}/arena", http::segment(team_id));
let request = self
.client
.request(Method::GET, Host::Default, &path)
.query(query);
http::stream(request, self.client.max_line_bytes()).await
}
pub async fn swiss_tournaments(
&self,
team_id: &str,
query: &TeamTournamentQuery<'_>,
) -> Result<BoxStream<'static, Result<LichessSwiss>>> {
let path = format!("/api/team/{}/swiss", http::segment(team_id));
let request = self
.client
.request(Method::GET, Host::Default, &path)
.query(query);
http::stream(request, self.client.max_line_bytes()).await
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct TeamTournamentQuery<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
max: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<&'a str>,
#[serde(rename = "createdBy", skip_serializing_if = "Option::is_none")]
created_by: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<&'a str>,
}
impl<'a> TeamTournamentQuery<'a> {
#[must_use]
pub fn max(mut self, max: u32) -> Self {
self.max = Some(max);
self
}
#[must_use]
pub fn status(mut self, status: &'a str) -> Self {
self.status = Some(status);
self
}
#[must_use]
pub fn created_by(mut self, username: &'a str) -> Self {
self.created_by = Some(username);
self
}
#[must_use]
pub fn name(mut self, name: &'a str) -> Self {
self.name = Some(name);
self
}
}
impl LichessClient {
#[must_use]
pub fn teams(&self) -> TeamsApi<'_> {
TeamsApi::new(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessTeam {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flair: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub leader: Option<LichessLightUser>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub leaders: Option<Vec<LichessLightUser>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nb_members: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub open: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub joined: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requested: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessTeamPaginator {
pub current_page: u32,
pub max_per_page: u32,
pub current_page_results: Vec<LichessTeam>,
#[serde(default)]
pub previous_page: Option<u32>,
#[serde(default)]
pub next_page: Option<u32>,
pub nb_results: u32,
pub nb_pages: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessTeamRequest {
pub team_id: String,
pub user_id: String,
pub date: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessTeamRequestWithUser {
pub request: LichessTeamRequest,
pub user: LichessUser,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn team_tournament_query_serializes_filters() {
let query = TeamTournamentQuery::default()
.max(10)
.status("started")
.created_by("thibault");
let encoded = serde_urlencoded::to_string(&query).unwrap();
assert_eq!(encoded, "max=10&status=started&createdBy=thibault");
assert_eq!(
serde_urlencoded::to_string(TeamTournamentQuery::default()).unwrap(),
""
);
}
#[test]
fn join_form_debug_redacts_password() {
let form = JoinForm {
message: Some("hi"),
password: Some(Secret::new("supersecret")),
};
let debug = format!("{form:?}");
assert!(!debug.contains("supersecret"));
assert!(debug.contains("<redacted>"));
assert!(debug.contains("hi"));
}
#[test]
fn parses_team() {
let json = r#"{"id":"coders","name":"Coders","nbMembers":42,"open":true,
"leader":{"id":"t","name":"T"}}"#;
let team: LichessTeam = serde_json::from_str(json).unwrap();
assert_eq!(team.id, "coders");
assert_eq!(team.nb_members, Some(42));
assert_eq!(team.leader.unwrap().name, "T");
}
#[test]
fn parses_paginator_with_null_pages() {
let json = r#"{"currentPage":1,"maxPerPage":15,"currentPageResults":[],
"previousPage":null,"nextPage":2,"nbResults":30,"nbPages":2}"#;
let page: LichessTeamPaginator = serde_json::from_str(json).unwrap();
assert_eq!(page.previous_page, None);
assert_eq!(page.next_page, Some(2));
}
#[test]
fn parses_request_with_user() {
let json = r#"{"request":{"userId":"mary","teamId":"t","date":1,"message":"hi"},
"user":{"id":"mary","username":"Mary"}}"#;
let req: LichessTeamRequestWithUser = serde_json::from_str(json).unwrap();
assert_eq!(req.request.user_id, "mary");
assert_eq!(req.user.username, "Mary");
}
}