1pub mod account;
2pub mod analysis;
3pub mod arena_tournaments;
4pub mod board;
5pub mod bot;
6pub mod broadcasts;
7pub mod bulk_pairings;
8pub mod challenges;
9pub mod external_engine;
10pub mod fide;
11pub mod games;
12pub mod messaging;
13#[cfg(feature = "oauth")]
14pub mod oauth;
15pub mod openings;
16pub mod puzzles;
17pub mod relations;
18pub mod simuls;
19pub mod studies;
20pub mod swiss_tournaments;
21pub mod tablebase;
22pub mod teams;
23pub mod tv;
24pub mod users;
25
26use crate::error;
27use serde::{Deserialize, Serialize, de::DeserializeOwned};
28use serde_with::skip_serializing_none;
29
30pub trait BodyBounds: Serialize {}
31impl<B: Serialize> BodyBounds for B {}
32
33pub trait QueryBounds: Serialize + Default {}
34impl<Q: Serialize + Default> QueryBounds for Q {}
35
36pub trait ModelBounds: DeserializeOwned {}
37impl<M: DeserializeOwned> ModelBounds for M {}
38
39#[derive(Default, Clone, Debug)]
40pub enum Body<B: BodyBounds> {
41 Form(B),
42 Json(B),
43 PlainText(String),
44 #[default]
45 Empty,
46}
47
48impl<B: BodyBounds> Body<B> {
49 fn as_mime(&self) -> Option<mime::Mime> {
50 match &self {
51 Body::Form(_) => Some(mime::APPLICATION_WWW_FORM_URLENCODED),
52 Body::Json(_) => Some(mime::APPLICATION_JSON),
53 Body::PlainText(_) => Some(mime::TEXT_PLAIN),
54 Body::Empty => None,
55 }
56 }
57
58 fn as_encoded_string(&self) -> error::Result<String> {
59 let body = match &self {
60 Body::Form(form) => to_form_string(&form)?,
61 Body::Json(json) => to_json_string(&json)?,
62 Body::PlainText(text) => text.to_string(),
63 Body::Empty => "".to_string(),
64 };
65 Ok(body)
66 }
67}
68
69#[derive(Clone, Copy, Debug, Default)]
70pub enum Domain {
71 #[default]
72 Lichess,
73 Tablebase,
74 Engine,
75 Explorer,
76}
77
78impl AsRef<str> for Domain {
79 fn as_ref(&self) -> &str {
80 match self {
81 Domain::Lichess => "lichess.org",
82 Domain::Tablebase => "tablebase.lichess.ovh",
83 Domain::Engine => "engine.lichess.ovh",
84 Domain::Explorer => "explorer.lichess.ovh",
85 }
86 }
87}
88
89#[derive(Clone, Debug)]
90pub struct Request<Q, B = ()>
91where
92 Q: QueryBounds,
93 B: BodyBounds,
94{
95 pub(crate) domain: Domain,
96 pub(crate) method: http::Method,
97 pub(crate) path: String,
98 pub(crate) query: Option<Q>,
99 pub(crate) body: Body<B>,
100}
101
102impl<Q, B> Request<Q, B>
103where
104 Q: QueryBounds + Default,
105 B: BodyBounds,
106{
107 pub(crate) fn create(
108 path: impl Into<String>,
109 query: impl Into<Option<Q>>,
110 body: impl Into<Option<Body<B>>>,
111 domain: impl Into<Option<Domain>>,
112 method: http::Method,
113 ) -> Self {
114 Self {
115 domain: domain.into().unwrap_or_default(),
116 method,
117 path: path.into(),
118 query: query.into(),
119 body: body.into().unwrap_or_default(),
120 }
121 }
122
123 pub(crate) fn get(
124 path: impl Into<String>,
125 query: impl Into<Option<Q>>,
126 domain: impl Into<Option<Domain>>,
127 ) -> Self {
128 Self::create(path, query, None, domain, http::Method::GET)
129 }
130
131 pub(crate) fn head(
132 path: impl Into<String>,
133 query: impl Into<Option<Q>>,
134 domain: impl Into<Option<Domain>>,
135 ) -> Self {
136 Self::create(path, query, None, domain, http::Method::HEAD)
137 }
138
139 pub(crate) fn post(
140 path: impl Into<String>,
141 query: impl Into<Option<Q>>,
142 body: impl Into<Option<Body<B>>>,
143 domain: impl Into<Option<Domain>>,
144 ) -> Self {
145 Self::create(path, query, body, domain, http::Method::POST)
146 }
147
148 pub(crate) fn put(
149 path: impl Into<String>,
150 query: impl Into<Option<Q>>,
151 body: impl Into<Option<Body<B>>>,
152 domain: impl Into<Option<Domain>>,
153 ) -> Self {
154 Self::create(path, query, body, domain, http::Method::PUT)
155 }
156
157 pub(crate) fn delete(
158 path: impl Into<String>,
159 query: impl Into<Option<Q>>,
160 body: impl Into<Option<Body<B>>>,
161 domain: Option<Domain>,
162 ) -> Self {
163 Self::create(path, query, body, domain, http::Method::DELETE)
164 }
165}
166
167impl<Q, B> Request<Q, B>
168where
169 Q: QueryBounds,
170 B: BodyBounds,
171{
172 pub(crate) fn into_http_request(
173 self,
174 accept: &str,
175 ) -> error::Result<http::Request<bytes::Bytes>> {
176 make_request(
177 self.domain,
178 self.method,
179 self.path,
180 self.query,
181 self.body,
182 accept,
183 )
184 }
185}
186
187fn make_request<Q, B>(
188 domain: Domain,
189 method: http::Method,
190 path: String,
191 query: Option<Q>,
192 body: Body<B>,
193 accept: &str,
194) -> error::Result<http::Request<bytes::Bytes>>
195where
196 Q: QueryBounds,
197 B: BodyBounds,
198{
199 let mut builder = http::Request::builder();
200
201 if let Some(mime) = body.as_mime() {
202 builder = builder.header(http::header::CONTENT_TYPE, mime.to_string());
203 }
204 let accept_header = http::HeaderValue::from_str(accept)
205 .map_err(|e| error::Error::HttpRequestBuilder(http::Error::from(e)))?;
206 builder = builder.header(http::header::ACCEPT, accept_header);
207
208 let url = make_url(domain, path, query)?;
209 let body = bytes::Bytes::from(body.as_encoded_string()?);
210
211 let request = builder
212 .method(method)
213 .uri(url.as_str())
214 .body(body)
215 .map_err(error::Error::HttpRequestBuilder)?;
216
217 Ok(request)
218}
219
220fn make_url<Q>(domain: Domain, path: String, query: Option<Q>) -> error::Result<url::Url>
221where
222 Q: QueryBounds,
223{
224 let base_url = format!("https://{}", domain.as_ref());
225 let mut url = url::Url::parse(&base_url).expect("invalid base url");
226
227 if let Some(query) = query {
228 let mut query_pairs = url.query_pairs_mut();
229 let query_serializer = serde_urlencoded::Serializer::new(&mut query_pairs);
230 query.serialize(query_serializer)?;
231 }
232
233 url.set_path(&path.to_string());
234
235 Ok(url)
236}
237
238fn to_json_string<B: BodyBounds>(body: &B) -> error::Result<String> {
239 serde_json::to_string(&body).map_err(error::Error::Json)
240}
241
242fn to_form_string<B: BodyBounds>(body: &B) -> error::Result<String> {
243 serde_urlencoded::to_string(body).map_err(error::Error::UrlEncoded)
244}
245
246#[derive(Clone, Debug, Serialize, Deserialize)]
247pub struct Ok {
248 pub ok: bool,
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize)]
252#[serde(untagged)]
253pub enum Response<M> {
254 Model(M),
255 Error { error: String },
256}
257
258#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
259#[serde(rename_all = "lowercase")]
260pub enum Color {
261 #[default]
262 White,
263 Black,
264 Random,
265}
266
267#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
268#[serde(rename_all = "lowercase")]
269pub enum PlayerColor {
270 #[default]
271 White,
272 Black,
273}
274
275#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
276#[serde(rename_all = "camelCase")]
277pub enum Speed {
278 UltraBullet,
279 Bullet,
280 Blitz,
281 Rapid,
282 Classical,
283 Correspondence,
284}
285
286#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Hash)]
287#[serde(rename_all = "camelCase")]
288pub enum PerfType {
289 UltraBullet,
290 Bullet,
291 Blitz,
292 Rapid,
293 Classical,
294 Correspondence,
295 Chess960,
296 Crazyhouse,
297 Antichess,
298 Atomic,
299 Horde,
300 KingOfTheHill,
301 RacingKings,
302 ThreeCheck,
303}
304
305impl PerfType {
306 pub fn as_str(&self) -> &'static str {
307 match self {
308 Self::UltraBullet => "ultraBullet",
309 Self::Bullet => "bullet",
310 Self::Blitz => "blitz",
311 Self::Rapid => "rapid",
312 Self::Classical => "classical",
313 Self::Correspondence => "correspondence",
314 Self::Chess960 => "chess960",
315 Self::Crazyhouse => "crazyhouse",
316 Self::Antichess => "antichess",
317 Self::Atomic => "atomic",
318 Self::Horde => "horde",
319 Self::KingOfTheHill => "kingOfTheHill",
320 Self::RacingKings => "racingKings",
321 Self::ThreeCheck => "threeCheck",
322 }
323 }
324}
325
326impl std::fmt::Display for PerfType {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 f.write_str(self.as_str())
329 }
330}
331
332#[skip_serializing_none]
333#[derive(Clone, Debug, Deserialize, Serialize)]
334pub struct LightUser {
335 pub id: String,
336 pub name: String,
337 pub title: Option<Title>,
338 pub flair: Option<String>,
339 pub patron: Option<bool>,
340 #[serde(rename = "patronColor")]
341 pub patron_color: Option<u8>,
342 pub online: Option<bool>,
343}
344
345#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
346#[serde(rename_all = "UPPERCASE")]
347pub enum Title {
348 Gm,
349 Wgm,
350 Im,
351 Wim,
352 Fm,
353 Wfm,
354 Nm,
355 Cm,
356 Wcm,
357 Wnm,
358 Lm,
359 Bot,
360}
361
362#[skip_serializing_none]
363#[derive(Clone, Debug, Deserialize, Serialize)]
364pub struct Variant {
365 pub key: VariantKey,
366 pub name: String,
367 pub short: Option<String>,
368 pub icon: Option<String>,
369}
370
371#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
372#[serde(rename_all = "camelCase")]
373pub enum VariantKey {
374 #[default]
375 Standard,
376 Chess960,
377 Crazyhouse,
378 Antichess,
379 Atomic,
380 Horde,
381 KingOfTheHill,
382 RacingKings,
383 ThreeCheck,
384 FromPosition,
385}
386
387#[derive(Default, Clone, Debug, Serialize, PartialEq, Eq, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub enum Room {
390 #[default]
391 Player,
392 Spectator,
393}
394
395#[derive(Clone, Debug, Deserialize, Serialize)]
396pub struct GameCompat {
397 pub bot: Option<bool>,
398 pub board: Option<bool>,
399}
400
401#[serde_with::skip_serializing_none]
402#[derive(Clone, Debug, Serialize, Deserialize)]
403#[serde(rename_all = "camelCase")]
404pub struct Clock {
405 pub initial: u32,
406 pub increment: u32,
407 pub total_time: Option<u32>,
408}
409
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411pub enum Days {
412 One,
413 Two,
414 Three,
415 Five,
416 Seven,
417 Ten,
418 Fourteen,
419}
420
421impl Serialize for Days {
422 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
423 where
424 S: serde::Serializer,
425 {
426 let value: u32 = (*self).into();
427 value.serialize(serializer)
428 }
429}
430
431impl<'de> Deserialize<'de> for Days {
432 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
433 where
434 D: serde::Deserializer<'de>,
435 {
436 let value = u32::deserialize(deserializer)?;
437 Ok(Days::from(value))
438 }
439}
440
441impl From<u32> for Days {
442 fn from(value: u32) -> Self {
443 match value {
444 1 => Days::One,
445 2 => Days::Two,
446 3 => Days::Three,
447 5 => Days::Five,
448 7 => Days::Seven,
449 10 => Days::Ten,
450 14 => Days::Fourteen,
451 _ => panic!("Invalid days {}", value),
452 }
453 }
454}
455
456impl From<Days> for u32 {
457 fn from(value: Days) -> Self {
458 match value {
459 Days::One => 1,
460 Days::Two => 2,
461 Days::Three => 3,
462 Days::Five => 5,
463 Days::Seven => 7,
464 Days::Ten => 10,
465 Days::Fourteen => 14,
466 }
467 }
468}
469
470#[skip_serializing_none]
471#[derive(Clone, Debug, Deserialize, Serialize)]
472#[serde(rename_all = "camelCase")]
473pub struct ArenaTournament {
474 pub id: String,
475 pub created_by: String,
476 pub system: String,
477 pub minutes: u32,
478 pub clock: ArenaClock,
479 pub rated: bool,
480 pub full_name: String,
481 pub nb_players: u32,
482 pub variant: Variant,
483 pub starts_at: i64,
484 pub finishes_at: i64,
485 pub status: ArenaStatus,
486 pub perf: ArenaPerf,
487 pub seconds_to_start: Option<i32>,
488 pub has_max_rating: Option<bool>,
489 pub max_rating: Option<ArenaRatingObj>,
490 pub min_rating: Option<ArenaRatingObj>,
491 pub min_rated_games: Option<ArenaMinRatedGames>,
492 pub bots_allowed: Option<bool>,
493 pub min_account_age_in_days: Option<i32>,
494 pub only_titled: Option<bool>,
495 pub team_member: Option<String>,
496 pub private: Option<bool>,
497 pub position: Option<ArenaPosition>,
498 pub schedule: Option<ArenaSchedule>,
499 pub team_battle: Option<ArenaTeamBattle>,
500 pub winner: Option<LightUser>,
501}
502
503#[derive(Clone, Debug, Deserialize, Serialize)]
504pub struct ArenaClock {
505 pub limit: u32,
506 pub increment: u32,
507}
508
509#[derive(Clone, Copy, Debug, PartialEq, Eq)]
510pub enum ArenaStatus {
511 Created,
512 Started,
513 Finished,
514}
515
516impl Serialize for ArenaStatus {
517 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
518 where
519 S: serde::Serializer,
520 {
521 let value: u8 = match self {
522 ArenaStatus::Created => 10,
523 ArenaStatus::Started => 20,
524 ArenaStatus::Finished => 30,
525 };
526 value.serialize(serializer)
527 }
528}
529
530impl<'de> Deserialize<'de> for ArenaStatus {
531 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
532 where
533 D: serde::Deserializer<'de>,
534 {
535 let value = u8::deserialize(deserializer)?;
536 match value {
537 10 => Ok(ArenaStatus::Created),
538 20 => Ok(ArenaStatus::Started),
539 30 => Ok(ArenaStatus::Finished),
540 _ => Err(serde::de::Error::custom(format!(
541 "invalid arena tournament status {value}"
542 ))),
543 }
544 }
545}
546
547#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
548#[serde(rename_all = "lowercase")]
549pub enum ArenaStatusName {
550 Created,
551 Started,
552 Finished,
553}
554
555#[skip_serializing_none]
556#[derive(Clone, Debug, Deserialize, Serialize)]
557pub struct ArenaPerf {
558 pub key: PerfType,
559 pub name: String,
560 pub position: i32,
561 pub icon: Option<String>,
562}
563
564#[skip_serializing_none]
565#[derive(Clone, Debug, Deserialize, Serialize)]
566pub struct ArenaRatingObj {
567 pub perf: Option<PerfType>,
568 pub rating: i32,
569}
570
571#[derive(Clone, Debug, Deserialize, Serialize)]
572pub struct ArenaMinRatedGames {
573 pub nb: Option<i32>,
574}
575
576#[derive(Clone, Debug, Deserialize, Serialize)]
577#[serde(untagged)]
578pub enum ArenaPosition {
579 Thematic {
580 eco: String,
581 name: String,
582 fen: String,
583 url: String,
584 },
585 Custom {
586 name: String,
587 fen: String,
588 },
589}
590
591#[skip_serializing_none]
592#[derive(Clone, Debug, Deserialize, Serialize)]
593pub struct ArenaSchedule {
594 pub freq: Option<String>,
595 pub speed: Option<String>,
596}
597
598#[skip_serializing_none]
599#[derive(Clone, Debug, Deserialize, Serialize)]
600pub struct ArenaTeamBattle {
601 pub teams: Option<Vec<String>>,
602 pub nb_leaders: Option<i32>,
603}
604
605#[skip_serializing_none]
606#[derive(Clone, Debug, Deserialize, Serialize)]
607#[serde(rename_all = "camelCase")]
608pub struct SwissTournament {
609 pub id: String,
610 pub created_by: String,
611 pub starts_at: String,
612 pub name: String,
613 pub clock: SwissClock,
614 pub variant: VariantKey,
615 pub round: i32,
616 pub nb_rounds: i32,
617 pub nb_players: i32,
618 pub nb_ongoing: i32,
619 pub status: SwissStatus,
620 pub stats: Option<SwissStats>,
621 pub rated: bool,
622 pub verdicts: Verdicts,
623 pub next_round: Option<SwissNextRound>,
624}
625
626#[derive(Clone, Debug, Deserialize, Serialize)]
627pub struct SwissClock {
628 pub limit: i32,
629 pub increment: i32,
630}
631
632#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
633#[serde(rename_all = "lowercase")]
634pub enum SwissStatus {
635 Created,
636 Started,
637 Finished,
638}
639
640#[derive(Clone, Debug, Deserialize, Serialize)]
641#[serde(rename_all = "camelCase")]
642pub struct SwissStats {
643 pub games: i32,
644 pub white_wins: i32,
645 pub black_wins: i32,
646 pub draws: i32,
647 pub byes: i32,
648 pub absences: i32,
649 pub average_rating: f64,
650}
651
652#[derive(Clone, Debug, Deserialize, Serialize)]
653pub struct SwissNextRound {
654 pub at: String,
655 #[serde(rename = "in")]
656 pub in_seconds: i32,
657}
658
659#[derive(Clone, Debug, Deserialize, Serialize)]
660pub struct Verdicts {
661 pub accepted: bool,
662 pub list: Vec<Verdict>,
663}
664
665#[derive(Clone, Debug, Deserialize, Serialize)]
666pub struct Verdict {
667 pub condition: String,
668 pub verdict: String,
669}