use std::collections::HashMap;
use futures_util::stream::BoxStream;
use reqwest::Method;
use reqwest::header::CONTENT_TYPE;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::client::LichessClient;
use crate::config::Host;
use crate::error::Result;
use crate::http;
#[derive(Debug)]
pub struct BroadcastsApi<'a> {
client: &'a LichessClient,
}
impl<'a> BroadcastsApi<'a> {
pub(crate) fn new(client: &'a LichessClient) -> Self {
Self { client }
}
pub async fn official(&self) -> Result<BoxStream<'static, Result<LichessBroadcast>>> {
let request = self
.client
.request(Method::GET, Host::Default, "/api/broadcast");
http::stream(request, self.client.max_line_bytes()).await
}
pub async fn top(&self) -> Result<LichessBroadcastTop> {
let request = self
.client
.request(Method::GET, Host::Default, "/api/broadcast/top");
http::json(request, "LichessBroadcastTop").await
}
pub async fn search(&self, query: &str, page: u32) -> Result<LichessBroadcastSearchPage> {
let request = self
.client
.request(Method::GET, Host::Default, "/api/broadcast/search")
.query(&[("q", query), ("page", &page.to_string())]);
http::json(request, "LichessBroadcastSearchPage").await
}
pub async fn by_user(
&self,
username: &str,
) -> Result<BoxStream<'static, Result<LichessBroadcast>>> {
let path = format!("/api/broadcast/by/{}", http::segment(username));
let request = self.client.request(Method::GET, Host::Default, &path);
http::stream(request, self.client.max_line_bytes()).await
}
pub async fn my_rounds(&self) -> Result<BoxStream<'static, Result<LichessBroadcastMyRound>>> {
let request = self
.client
.request(Method::GET, Host::Default, "/api/broadcast/my-rounds");
http::stream(request, self.client.max_line_bytes()).await
}
pub async fn get_tournament(&self, tournament_id: &str) -> Result<LichessBroadcast> {
let path = format!("/api/broadcast/{}", http::segment(tournament_id));
let request = self.client.request(Method::GET, Host::Default, &path);
http::json(request, "LichessBroadcast").await
}
pub async fn round(
&self,
tour_slug: &str,
round_slug: &str,
round_id: &str,
) -> Result<LichessBroadcastRoundView> {
let path = format!(
"/api/broadcast/{}/{}/{}",
http::segment(tour_slug),
http::segment(round_slug),
http::segment(round_id)
);
let request = self.client.request(Method::GET, Host::Default, &path);
http::json(request, "LichessBroadcastRoundView").await
}
pub async fn round_pgn(&self, round_id: &str) -> Result<String> {
let path = format!("/api/broadcast/round/{}.pgn", http::segment(round_id));
http::text(self.client.request(Method::GET, Host::Default, &path)).await
}
pub async fn all_rounds_pgn(&self, tournament_id: &str) -> Result<String> {
let path = format!("/api/broadcast/{}.pgn", http::segment(tournament_id));
http::text(self.client.request(Method::GET, Host::Default, &path)).await
}
pub async fn stream_round_pgn(&self, round_id: &str) -> Result<String> {
let path = format!(
"/api/stream/broadcast/round/{}.pgn",
http::segment(round_id)
);
http::text(self.client.request(Method::GET, Host::Default, &path)).await
}
pub async fn stream_group_pgn(&self, group_id: &str) -> Result<String> {
let path = format!(
"/api/stream/broadcast/group/{}.pgn",
http::segment(group_id)
);
http::text(self.client.request(Method::GET, Host::Default, &path)).await
}
pub async fn push_pgn(&self, round_id: &str, pgn: &str) -> Result<LichessBroadcastPushResult> {
let path = format!("/api/broadcast/round/{}/push", http::segment(round_id));
let request = self
.client
.request(Method::POST, Host::Default, &path)
.header(CONTENT_TYPE, "text/plain")
.body(pgn.to_owned());
http::json(request, "LichessBroadcastPushResult").await
}
pub async fn reset_round(&self, round_id: &str) -> Result<()> {
let path = format!("/api/broadcast/round/{}/reset", http::segment(round_id));
http::ok(self.client.request(Method::POST, Host::Default, &path)).await
}
pub async fn players(&self, tournament_id: &str) -> Result<Vec<LichessBroadcastPlayerEntry>> {
let path = format!("/broadcast/{}/players", http::segment(tournament_id));
let request = self.client.request(Method::GET, Host::Default, &path);
http::json(request, "Vec<LichessBroadcastPlayerEntry>").await
}
pub async fn player(
&self,
tournament_id: &str,
player_id: &str,
) -> Result<LichessBroadcastPlayerEntry> {
let path = format!(
"/broadcast/{}/players/{}",
http::segment(tournament_id),
http::segment(player_id)
);
let request = self.client.request(Method::GET, Host::Default, &path);
http::json(request, "LichessBroadcastPlayerEntry").await
}
pub async fn team_standings(
&self,
tournament_id: &str,
) -> Result<Vec<LichessBroadcastPlayerEntry>> {
let path = format!(
"/broadcast/{}/teams/standings",
http::segment(tournament_id)
);
let request = self.client.request(Method::GET, Host::Default, &path);
http::json(request, "broadcast team standings").await
}
#[must_use]
pub fn create_tour(&self, name: &'a str) -> TourRequest<'a> {
TourRequest::new(self.client, None, name)
}
#[must_use]
pub fn update_tour(&self, tournament_id: &'a str, name: &'a str) -> TourRequest<'a> {
TourRequest::new(self.client, Some(tournament_id), name)
}
#[must_use]
pub fn create_round(&self, tournament_id: &'a str, name: &'a str) -> RoundRequest<'a> {
RoundRequest::new(self.client, tournament_id, false, name)
}
#[must_use]
pub fn update_round(&self, round_id: &'a str, name: &'a str) -> RoundRequest<'a> {
RoundRequest::new(self.client, round_id, true, name)
}
}
impl LichessClient {
#[must_use]
pub fn broadcasts(&self) -> BroadcastsApi<'_> {
BroadcastsApi::new(self)
}
}
#[derive(Debug, Serialize)]
struct TourForm<'a> {
name: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
info: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
visibility: Option<&'a str>,
}
#[derive(Debug)]
pub struct TourRequest<'a> {
client: &'a LichessClient,
edit_id: Option<&'a str>,
form: TourForm<'a>,
}
impl<'a> TourRequest<'a> {
pub(crate) fn new(client: &'a LichessClient, edit_id: Option<&'a str>, name: &'a str) -> Self {
Self {
client,
edit_id,
form: TourForm {
name,
info: None,
visibility: None,
},
}
}
#[must_use]
pub fn info(mut self, info: &'a str) -> Self {
self.form.info = Some(info);
self
}
#[must_use]
pub fn visibility(mut self, visibility: &'a str) -> Self {
self.form.visibility = Some(visibility);
self
}
pub async fn send(self) -> Result<LichessBroadcast> {
let path = match self.edit_id {
Some(id) => format!("/broadcast/{}/edit", http::segment(id)),
None => "/broadcast/new".to_owned(),
};
let request = self
.client
.request(Method::POST, Host::Default, &path)
.form(&self.form);
http::json(request, "LichessBroadcast").await
}
}
#[derive(Debug, Serialize)]
struct RoundForm<'a> {
name: &'a str,
#[serde(rename = "syncUrl", skip_serializing_if = "Option::is_none")]
sync_url: Option<&'a str>,
#[serde(rename = "startsAt", skip_serializing_if = "Option::is_none")]
starts_at: Option<i64>,
}
#[derive(Debug)]
pub struct RoundRequest<'a> {
client: &'a LichessClient,
target_id: &'a str,
edit: bool,
form: RoundForm<'a>,
}
impl<'a> RoundRequest<'a> {
pub(crate) fn new(
client: &'a LichessClient,
target_id: &'a str,
edit: bool,
name: &'a str,
) -> Self {
Self {
client,
target_id,
edit,
form: RoundForm {
name,
sync_url: None,
starts_at: None,
},
}
}
#[must_use]
pub fn sync_url(mut self, url: &'a str) -> Self {
self.form.sync_url = Some(url);
self
}
#[must_use]
pub fn starts_at(mut self, timestamp: i64) -> Self {
self.form.starts_at = Some(timestamp);
self
}
pub async fn send(self) -> Result<LichessBroadcastRoundView> {
let path = if self.edit {
format!("/broadcast/round/{}/edit", http::segment(self.target_id))
} else {
format!("/broadcast/{}/new", http::segment(self.target_id))
};
let request = self
.client
.request(Method::POST, Host::Default, &path)
.form(&self.form);
http::json(request, "LichessBroadcastRoundView").await
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessBroadcastTour {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slug: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tier: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessBroadcastRoundInfo {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slug: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rated: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ongoing: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub starts_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finished_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finished: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessBroadcast {
pub tour: LichessBroadcastTour,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(default)]
pub rounds: Vec<LichessBroadcastRoundInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_round_id: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_broadcast_with_rounds() {
let json = r#"{"tour":{"id":"abc","name":"World Champ","slug":"wc"},
"rounds":[{"id":"r1","name":"Round 1","slug":"round-1","url":"u",
"createdAt":1,"rated":true,"finished":false}]}"#;
let broadcast: LichessBroadcast = serde_json::from_str(json).unwrap();
assert_eq!(broadcast.tour.name, "World Champ");
assert_eq!(broadcast.rounds[0].id, "r1");
assert_eq!(broadcast.rounds[0].finished, Some(false));
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessBroadcastTop {
#[serde(default)]
pub active: Vec<LichessBroadcast>,
#[serde(default)]
pub upcoming: Vec<LichessBroadcast>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub past: Option<Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessBroadcastSearchPage {
pub current_page: u32,
pub max_per_page: u32,
#[serde(default)]
pub current_page_results: Vec<LichessBroadcast>,
#[serde(default)]
pub previous_page: Option<u32>,
#[serde(default)]
pub next_page: Option<u32>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessBroadcastRoundView {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tour: Option<LichessBroadcastTour>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub round: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub games: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub study: Option<Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessBroadcastPlayerEntry {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(flatten)]
pub other: HashMap<String, Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessBroadcastMyRound {
#[serde(flatten)]
pub data: HashMap<String, Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessBroadcastPushResult {
#[serde(flatten)]
pub data: HashMap<String, Value>,
}