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) -> 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);
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) -> Result<Vec<LichessTeamRequestWithUser>> {
let path = format!("/api/team/{}/requests", http::segment(team_id));
let request = self.client.request(Method::GET, Host::Default, &path);
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,
) -> 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);
http::stream(request, self.client.max_line_bytes()).await
}
pub async fn swiss_tournaments(
&self,
team_id: &str,
) -> 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);
http::stream(request, self.client.max_line_bytes()).await
}
}
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 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");
}
}