diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0cffa83..87bdc3e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -43,7 +43,7 @@ jobs: run: cargo build --verbose - name: wasm check - run: cargo check --verbose --target wasm32-unknown-unknown + run: cargo check --lib --verbose --target wasm32-unknown-unknown - name: test run: cargo test --verbose diff --git a/Cargo.toml b/Cargo.toml index 19f927d..d7351b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,38 +1,3 @@ -[package] -name = "lichess-api" -version = "0.6.0" -edition = "2021" -license = "Apache-2.0" -description = "A Rust client for Lichess API v2.0.0" -keywords = ["lichess", "api", "client"] -categories = ["api-bindings", "asynchronous"] -homepage = "https://github.com/ion232/lichess-api" -repository = "https://github.com/ion232/lichess-api" -readme = "README.md" - -[dependencies] -reqwest = { version = "0.12.7", features = ["json", "stream"] } - -# Other dependencies. -async-std = "1.13.1" -bytes = "1.10.1" -futures = "0.3.31" -futures-core = "0.3.31" -http = "1.3.1" -mime = "0.3.17" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -serde_with = { version = "3.12.0", features = ["chrono"] } -serde_urlencoded = "0.7.1" -thiserror = "2.0.12" -tracing = "0.1.41" -url = "2.5.4" - -[dev-dependencies] -clap = { version = "4.5.32", features = ["derive"] } -color-eyre = "0.6.3" -getrandom = { version = "0.3.2" } -rand = "0.9.0" -tokio = { version = "1.44.1", features = ["macros", "rt"] } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } - +[workspace] +members = ["cli", "lib"] +resolver = "2" \ No newline at end of file diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..5abef45 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "lichess-cli" +version = "0.6.0" +edition = "2024" +license = "Apache-2.0" +description = "A cli client for the lichess API." +keywords = ["lichess", "api", "client"] +categories = ["api-bindings"] +homepage = "https://github.com/ion232/lichess-api" +repository = "https://github.com/ion232/lichess-api" + +[[bin]] +name = "lichess" +path = "src/main.rs" + +[dependencies] +lichess-api = { path = "../lib" } +clap = { version = "4.5.32", features = ["derive"] } +color-eyre = "0.6.3" +futures = "0.3.31" +rand = "0.9.0" +reqwest = "0.12.7" +tokio = { version = "1.44.1", features = ["macros", "rt"] } +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } \ No newline at end of file diff --git a/cli/src/commands/challenges.rs b/cli/src/commands/challenges.rs new file mode 100644 index 0000000..ef78e37 --- /dev/null +++ b/cli/src/commands/challenges.rs @@ -0,0 +1,184 @@ +use clap::Subcommand; +use color_eyre::Result; +use lichess_api::client::LichessApi; +use lichess_api::model::VariantKey; +use lichess_api::model::challenges::*; +use reqwest; + +type Lichess = LichessApi; + +#[derive(Debug, Subcommand)] +pub enum ChallengesCommand { + /// List your challenges + List, + /// Create a challenge + Create { + /// Username to challenge + username: String, + /// Whether the game is rated + #[arg(long)] + rated: bool, + /// Clock limit in seconds + #[arg(long)] + clock_limit: Option, + /// Clock increment in seconds + #[arg(long)] + clock_increment: Option, + /// Days per turn for correspondence games + #[arg(long)] + days: Option, + /// Chess variant + #[arg(long, default_value = "standard")] + variant: String, + /// Custom starting position (FEN) + #[arg(long)] + fen: Option, + /// Message to the opponent + #[arg(long)] + message: Option, + }, + /// Accept a challenge + Accept { + /// Challenge ID + challenge_id: String, + }, + /// Decline a challenge + Decline { + /// Challenge ID + challenge_id: String, + /// Reason for declining + #[arg(long, value_enum)] + reason: Option, + }, + /// Cancel a challenge you sent + Cancel { + /// Challenge ID + challenge_id: String, + /// Opponent token (if applicable) + #[arg(long)] + opponent_token: Option, + }, +} + +#[derive(Debug, Clone, clap::ValueEnum)] +pub enum DeclineReason { + Generic, + Later, + TooFast, + TooSlow, + TimeControl, + Rated, + Casual, + Standard, + Variant, + NoBot, + OnlyBot, +} + +impl From for decline::Reason { + fn from(reason: DeclineReason) -> Self { + match reason { + DeclineReason::Generic => decline::Reason::Generic, + DeclineReason::Later => decline::Reason::Later, + DeclineReason::TooFast => decline::Reason::TooFast, + DeclineReason::TooSlow => decline::Reason::TooSlow, + DeclineReason::TimeControl => decline::Reason::TimeControl, + DeclineReason::Rated => decline::Reason::Rated, + DeclineReason::Casual => decline::Reason::Casual, + DeclineReason::Standard => decline::Reason::Standard, + DeclineReason::Variant => decline::Reason::Variant, + DeclineReason::NoBot => decline::Reason::NoBot, + DeclineReason::OnlyBot => decline::Reason::OnlyBot, + } + } +} + +impl ChallengesCommand { + pub async fn run(self, lichess: Lichess) -> Result<()> { + match self { + ChallengesCommand::List => { + let challenges = lichess.list_challenges().await?; + println!("Incoming challenges:"); + for challenge in &challenges.r#in { + println!(" {} - {}", challenge.base.id, challenge.base.url); + } + println!("Outgoing challenges:"); + for challenge in &challenges.out { + println!(" {} - {}", challenge.base.id, challenge.base.url); + } + Ok(()) + } + ChallengesCommand::Create { + username, + rated, + clock_limit, + clock_increment, + days, + variant, + fen, + message, + } => { + let variant_key = match variant.as_str() { + "standard" => VariantKey::Standard, + "chess960" => VariantKey::Chess960, + "crazyhouse" => VariantKey::Crazyhouse, + "antichess" => VariantKey::Antichess, + "atomic" => VariantKey::Atomic, + "horde" => VariantKey::Horde, + "kingOfTheHill" => VariantKey::KingOfTheHill, + "racingKings" => VariantKey::RacingKings, + "threeCheck" => VariantKey::ThreeCheck, + _ => { + println!("Invalid variant: {}", variant); + return Ok(()); + } + }; + + let challenge = CreateChallenge { + base: ChallengeBase { + clock_limit: clock_limit, + clock_increment: clock_increment, + days: days.map(|d| d.into()), + variant: variant_key, + fen: fen, + }, + rated: rated, + keep_alive_stream: false, + accept_by_token: None, + message: message, + rules: String::new(), + }; + + let request = create::PostRequest::new(&username, challenge); + let result = lichess.create_challenge(request).await?; + println!("Challenge created: {:#?}", result); + Ok(()) + } + ChallengesCommand::Accept { challenge_id } => { + let request = accept::PostRequest::new(&challenge_id); + let result = lichess.accept_challenge(request).await?; + println!("Challenge accepted: {}", result); + Ok(()) + } + ChallengesCommand::Decline { + challenge_id, + reason, + } => { + let decline_reason = reason.unwrap_or(DeclineReason::Generic); + let request = decline::PostRequest::new(challenge_id, decline_reason.into()); + let result = lichess.decline_challenge(request).await?; + println!("Challenge declined: {}", result); + Ok(()) + } + ChallengesCommand::Cancel { + challenge_id, + opponent_token, + } => { + let request = cancel::PostRequest::new(challenge_id, opponent_token); + let result = lichess.cancel_challenge(request).await?; + println!("Challenge cancelled: {}", result); + Ok(()) + } + } + } +} diff --git a/examples/lichess.rs b/cli/src/commands/external_engine.rs similarity index 64% rename from examples/lichess.rs rename to cli/src/commands/external_engine.rs index ed77673..0cbc31e 100644 --- a/examples/lichess.rs +++ b/cli/src/commands/external_engine.rs @@ -1,153 +1,15 @@ -use clap::builder::styling::AnsiColor; -use clap::builder::Styles; use clap::{Parser, Subcommand}; use color_eyre::Result; use futures::StreamExt; use lichess_api::client::LichessApi; use lichess_api::model::external_engine::{self, *}; -use lichess_api::model::puzzles::{self, *}; use rand::Rng; use reqwest; -use tracing::level_filters::LevelFilter; -use tracing_subscriber::EnvFilter; - -const HELP_STYLES: Styles = Styles::styled() - .header(AnsiColor::Blue.on_default().bold()) - .usage(AnsiColor::Blue.on_default().bold()) - .literal(AnsiColor::White.on_default()) - .placeholder(AnsiColor::Green.on_default()); - -#[derive(Debug, Parser)] -#[command(author, version, about, styles = HELP_STYLES)] -struct Cli { - /// A personal API token for lichess (https://lichess.org/account/oauth/token) - #[arg(long, short)] - api_token: Option, - - #[clap(subcommand)] - command: Command, - - /// Enable verbose logging - #[arg(long, short)] - verbose: bool, -} - -#[derive(Debug, Subcommand)] -enum Command { - Puzzle { - #[clap(subcommand)] - command: PuzzleCommand, - }, - Engine { - #[clap(subcommand)] - command: ExternalEngineCommand, - }, -} type Lichess = LichessApi; -#[derive(Debug)] -struct App { - lichess: Lichess, -} - -#[tokio::main(flavor = "current_thread")] -async fn main() -> Result<()> { - let args = Cli::parse(); - let level = if args.verbose { - LevelFilter::DEBUG - } else { - LevelFilter::INFO - }; - init_tracing(level)?; - color_eyre::install()?; - let app = App::new(args.api_token.clone()); - app.run(args).await -} - -fn init_tracing(directive: LevelFilter) -> Result<()> { - let filter = EnvFilter::builder() - .from_env()? - .add_directive(directive.into()) - // remove hyper noise - .add_directive("hyper::proto=info".parse()?); - tracing_subscriber::fmt() - .with_env_filter(filter) - .compact() - .init(); - Ok(()) -} - -impl App { - pub fn new(api_token: Option) -> Self { - let client = reqwest::ClientBuilder::new().build().unwrap(); - let api = LichessApi::new(client, api_token); - Self { lichess: api } - } - - async fn run(self, args: Cli) -> Result<()> { - match args.command { - Command::Puzzle { command } => command.run(self.lichess).await, - Command::Engine { command } => command.run(self.lichess).await, - } - } -} - -#[derive(Debug, Subcommand)] -enum PuzzleCommand { - /// Get the daily puzzle - Daily, - /// Get a puzzle by its ID - Get { id: String }, - /// Get your puzzle activity - Activity { max_rounds: Option }, - /// Get your puzzle dashboard - Dashboard { days: Option }, - /// Get the storm dashboard of a player - Storm { username: String, days: Option }, -} - -impl PuzzleCommand { - async fn run(self, lichess: Lichess) -> Result<()> { - match self { - PuzzleCommand::Daily => { - let puzzle = lichess.get_daily_puzzle().await?; - println!("{puzzle:#?}"); - Ok(()) - } - PuzzleCommand::Get { id } => { - let request = puzzles::id::GetRequest::new(&id); - let puzzle = lichess.get_puzzle(request).await?; - println!("{puzzle:#?}"); - Ok(()) - } - PuzzleCommand::Activity { max_rounds } => { - let request = activity::GetRequest::new(max_rounds); - let mut stream = lichess.get_puzzle_activity(request).await?; - while let Some(round) = stream.next().await { - let round = round?; - println!("Round: {round:#?}"); - } - Ok(()) - } - PuzzleCommand::Dashboard { days } => { - let request = dashboard::GetRequest::new(days.unwrap_or(30)); - let dashboard = lichess.get_puzzle_dashboard(request).await?; - println!("{dashboard:#?}"); - Ok(()) - } - PuzzleCommand::Storm { username, days } => { - let request = storm_dashboard::GetRequest::new(&username, days); - let dashboard = lichess.get_puzzle_storm_dashboard(request).await?; - println!("{dashboard:#?}"); - Ok(()) - } - } - } -} - #[derive(Debug, Subcommand)] -enum ExternalEngineCommand { +pub enum ExternalEngineCommand { /// Lists all external engines that have been registered for the user, and the credentials required to use them. List, /// Registers a new external engine for the user. It can then be selected and used on the analysis board. @@ -170,7 +32,7 @@ enum ExternalEngineCommand { } #[derive(Debug, Parser)] -struct CreateExternalEngineArgs { +pub struct CreateExternalEngineArgs { /// Display name of the engine #[arg(long, short)] name: String, @@ -192,7 +54,7 @@ struct CreateExternalEngineArgs { } #[derive(Debug, Parser)] -struct UpdateExternalEngineArgs { +pub struct UpdateExternalEngineArgs { // The external engine id id: String, /// Display name of the engine @@ -216,7 +78,7 @@ struct UpdateExternalEngineArgs { } #[derive(Debug, Parser)] -struct AnalyseArgs { +pub struct AnalyseArgs { /// The external engine id id: String, /// The client secret for the engine @@ -249,7 +111,7 @@ struct AnalyseArgs { } impl ExternalEngineCommand { - async fn run(self, lichess: Lichess) -> Result<()> { + pub async fn run(self, lichess: Lichess) -> Result<()> { match self { ExternalEngineCommand::List => { let engines = lichess.list_external_engines().await?; diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs new file mode 100644 index 0000000..485ab59 --- /dev/null +++ b/cli/src/commands/mod.rs @@ -0,0 +1,9 @@ +pub mod challenges; +pub mod external_engine; +pub mod puzzles; +pub mod users; + +pub use challenges::ChallengesCommand; +pub use external_engine::ExternalEngineCommand; +pub use puzzles::PuzzlesCommand; +pub use users::UsersCommand; diff --git a/cli/src/commands/puzzles.rs b/cli/src/commands/puzzles.rs new file mode 100644 index 0000000..11a9710 --- /dev/null +++ b/cli/src/commands/puzzles.rs @@ -0,0 +1,110 @@ +use clap::{Subcommand, ValueEnum}; +use color_eyre::Result; +use futures::StreamExt; +use lichess_api::client::LichessApi; +use lichess_api::model::puzzles::{self, *}; +use reqwest; + +type Lichess = LichessApi; + +#[derive(Debug, Clone, ValueEnum)] +pub enum Difficulty { + Easiest, + Easier, + Normal, + Harder, + Hardest, +} + +impl From for next::Difficulty { + fn from(d: Difficulty) -> Self { + match d { + Difficulty::Easiest => next::Difficulty::Easiest, + Difficulty::Easier => next::Difficulty::Easier, + Difficulty::Normal => next::Difficulty::Normal, + Difficulty::Harder => next::Difficulty::Harder, + Difficulty::Hardest => next::Difficulty::Hardest, + } + } +} + +#[derive(Debug, Subcommand)] +pub enum PuzzlesCommand { + /// Get the daily puzzle + Daily, + /// Get a puzzle by its ID + Get { id: String }, + /// Get your puzzle activity + Activity { max_rounds: Option }, + /// Get your puzzle dashboard + Dashboard { days: Option }, + /// Get the storm dashboard of a player + Storm { username: String, days: Option }, + /// Get a new random puzzle + Next { + /// Filter puzzles by theme/angle + #[arg(long)] + angle: Option, + /// Puzzle difficulty relative to your rating + #[arg(long, value_enum)] + difficulty: Option, + }, + /// Get puzzles to replay for a specific theme + Replay { + /// Number of days to look back (e.g., 30) + days: u32, + /// Theme to filter puzzles (e.g., "mix", "endgame") + theme: String, + }, +} + +impl PuzzlesCommand { + pub async fn run(self, lichess: Lichess) -> Result<()> { + match self { + PuzzlesCommand::Daily => { + let puzzle = lichess.get_daily_puzzle().await?; + println!("{puzzle:#?}"); + Ok(()) + } + PuzzlesCommand::Get { id } => { + let request = puzzles::id::GetRequest::new(&id); + let puzzle = lichess.get_puzzle(request).await?; + println!("{puzzle:#?}"); + Ok(()) + } + PuzzlesCommand::Activity { max_rounds } => { + let request = activity::GetRequest::new(max_rounds); + let mut stream = lichess.get_puzzle_activity(request).await?; + while let Some(round) = stream.next().await { + let round = round?; + println!("Round: {round:#?}"); + } + Ok(()) + } + PuzzlesCommand::Dashboard { days } => { + let request = dashboard::GetRequest::new(days.unwrap_or(30)); + let dashboard = lichess.get_puzzle_dashboard(request).await?; + println!("{dashboard:#?}"); + Ok(()) + } + PuzzlesCommand::Storm { username, days } => { + let request = storm_dashboard::GetRequest::new(&username, days); + let dashboard = lichess.get_puzzle_storm_dashboard(request).await?; + println!("{dashboard:#?}"); + Ok(()) + } + PuzzlesCommand::Next { angle, difficulty } => { + let request = next::GetRequest::new(angle, difficulty.map(|d| d.into())); + let puzzle = lichess.get_new_puzzle(request).await?; + println!("{puzzle:#?}"); + Ok(()) + } + PuzzlesCommand::Replay { days, theme } => { + let request = replay::GetRequest::new(days, &theme); + let replay = lichess.get_puzzles_to_replay(request).await?; + println!("{replay:#?}"); + Ok(()) + } + } + } +} diff --git a/cli/src/commands/users.rs b/cli/src/commands/users.rs new file mode 100644 index 0000000..9022bf4 --- /dev/null +++ b/cli/src/commands/users.rs @@ -0,0 +1,212 @@ +use clap::Subcommand; +use color_eyre::Result; +use lichess_api::client::LichessApi; +use lichess_api::model::{PerfType, users}; +use reqwest; + +type Lichess = LichessApi; + +#[derive(Debug, Subcommand)] +pub enum UsersCommand { + /// Get public data of a user + Get { + /// Username + username: String, + /// Include trophy information + #[arg(long)] + trophies: bool, + }, + /// Get the online, playing and streaming statuses of several users + Status { + /// Comma-separated list of usernames (up to 100) + users: String, + /// Include current game IDs + #[arg(long)] + with_game_ids: bool, + }, + /// Get rating history of a user + RatingHistory { + /// Username + username: String, + }, + /// Get performance statistics of a user + Performance { + /// Username + username: String, + /// Performance type (e.g., bullet, blitz, rapid, classical, etc.) + perf: String, + }, + /// Get users by their IDs + ByIds { + /// Comma-separated list of user IDs + ids: String, + }, + /// Get current live streamers + LiveStreamers, + /// Get the crosstable of two users + Crosstable { + /// First username + user1: String, + /// Second username + user2: String, + /// Include match results + #[arg(long)] + matchup: bool, + }, + /// Autocomplete usernames + Autocomplete { + /// Search term (at least 3 characters) + term: String, + /// Include friend names + #[arg(long)] + friend: bool, + }, + /// Get all top 10 leaderboards + Top10, + /// Get one leaderboard + Leaderboard { + /// Variant (e.g., bullet, blitz, rapid, classical, etc.) + perf: String, + /// Number of users to fetch (1-200) + #[arg(default_value = "10")] + count: u8, + }, + /// Get activity feed of a user + Activity { + /// Username + username: String, + }, +} + +impl UsersCommand { + pub async fn run(self, lichess: Lichess) -> Result<()> { + match self { + UsersCommand::Get { username, trophies } => { + let request = users::public::GetRequest::new(&username, trophies); + let user = lichess.get_public_user_data(request).await?; + println!("{:#?}", user); + Ok(()) + } + UsersCommand::Status { + users, + with_game_ids, + } => { + let user_ids: Vec = + users.split(',').map(|s| s.trim().to_string()).collect(); + let request = users::status::GetRequest::new(user_ids, with_game_ids); + let statuses = lichess.get_status_of_users(request).await?; + for status in statuses { + println!("{:#?}", status); + } + Ok(()) + } + UsersCommand::RatingHistory { username } => { + let request = users::rating_history::GetRequest::new(&username); + let history = lichess.get_rating_history(request).await?; + println!("{:#?}", history); + Ok(()) + } + UsersCommand::Performance { username, perf } => { + let perf_type = match perf.as_str() { + "ultraBullet" => PerfType::UltraBullet, + "bullet" => PerfType::Bullet, + "blitz" => PerfType::Blitz, + "rapid" => PerfType::Rapid, + "classical" => PerfType::Classical, + "chess960" => PerfType::Chess960, + "crazyhouse" => PerfType::Crazyhouse, + "antichess" => PerfType::Antichess, + "atomic" => PerfType::Atomic, + "horde" => PerfType::Horde, + "kingOfTheHill" => PerfType::KingOfTheHill, + "racingKings" => PerfType::RacingKings, + "threeCheck" => PerfType::ThreeCheck, + _ => { + println!("Invalid performance type: {}", perf); + return Ok(()); + } + }; + let request = users::performance::GetRequest::new(&username, perf_type); + let perf_stat = lichess.get_user_performance_statistics(request).await?; + println!("{:#?}", perf_stat); + Ok(()) + } + UsersCommand::ByIds { ids } => { + let user_ids: Vec = ids.split(',').map(|s| s.trim().to_string()).collect(); + let request = users::by_id::PostRequest::new(user_ids); + let users = lichess.get_users_by_id(request).await?; + for user in users { + println!("{:#?}", user); + } + Ok(()) + } + UsersCommand::LiveStreamers => { + let streamers = lichess.get_live_streamers().await?; + for streamer in streamers { + println!("{:#?}", streamer); + } + Ok(()) + } + UsersCommand::Crosstable { + user1, + user2, + matchup, + } => { + let request = users::crosstable::GetRequest::new(&user1, &user2, Some(matchup)); + let crosstable = lichess.get_crosstable(request).await?; + println!("{:#?}", crosstable); + Ok(()) + } + UsersCommand::Autocomplete { term, friend } => { + if term.len() < 3 { + println!("Search term must be at least 3 characters"); + return Ok(()); + } + let request = users::autocomplete::GetRequest::new(&term, Some(friend)); + let suggestions = lichess.autocomplete_users(request).await?; + for user in suggestions.result { + println!("{} ({})", user.name, user.id); + } + Ok(()) + } + UsersCommand::Top10 => { + let leaderboards = lichess.get_all_top_10().await?; + println!("{:#?}", leaderboards); + Ok(()) + } + UsersCommand::Leaderboard { count, perf } => { + let perf_type = match perf.as_str() { + "ultraBullet" => PerfType::UltraBullet, + "bullet" => PerfType::Bullet, + "blitz" => PerfType::Blitz, + "rapid" => PerfType::Rapid, + "classical" => PerfType::Classical, + "chess960" => PerfType::Chess960, + "crazyhouse" => PerfType::Crazyhouse, + "antichess" => PerfType::Antichess, + "atomic" => PerfType::Atomic, + "horde" => PerfType::Horde, + "kingOfTheHill" => PerfType::KingOfTheHill, + "racingKings" => PerfType::RacingKings, + "threeCheck" => PerfType::ThreeCheck, + _ => { + println!("Invalid performance type: {}", perf); + return Ok(()); + } + }; + let request = users::leaderboard::GetRequest::new(count, perf_type); + let leaderboard = lichess.get_one_leaderboard(request).await?; + println!("{:#?}", leaderboard); + Ok(()) + } + UsersCommand::Activity { username } => { + let request = users::activity::GetRequest::new(&username); + let activities = lichess.get_user_activity(request).await?; + for activity in activities { + println!("{:#?}", activity); + } + Ok(()) + } + } + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..d11a365 --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,103 @@ +mod commands; + +use clap::builder::Styles; +use clap::builder::styling::AnsiColor; +use clap::{Parser, Subcommand}; +use color_eyre::Result; +use commands::{ChallengesCommand, ExternalEngineCommand, PuzzlesCommand, UsersCommand}; +use lichess_api::client::LichessApi; +use reqwest; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::EnvFilter; + +const HELP_STYLES: Styles = Styles::styled() + .header(AnsiColor::Blue.on_default().bold()) + .usage(AnsiColor::Blue.on_default().bold()) + .literal(AnsiColor::White.on_default()) + .placeholder(AnsiColor::Green.on_default()); + +#[derive(Debug, Parser)] +#[command(author, version, about, styles = HELP_STYLES)] +struct Cli { + /// A personal API token for lichess (https://lichess.org/account/oauth/token) + #[arg(long, short)] + api_token: Option, + + #[clap(subcommand)] + command: Command, + + /// Enable verbose logging + #[arg(long, short)] + verbose: bool, +} + +#[derive(Debug, Subcommand)] +enum Command { + Puzzles { + #[clap(subcommand)] + command: PuzzlesCommand, + }, + Engine { + #[clap(subcommand)] + command: ExternalEngineCommand, + }, + Challenges { + #[clap(subcommand)] + command: ChallengesCommand, + }, + Users { + #[clap(subcommand)] + command: UsersCommand, + }, +} + +type Lichess = LichessApi; + +#[derive(Debug)] +struct App { + lichess: Lichess, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let args = Cli::parse(); + let level = if args.verbose { + LevelFilter::DEBUG + } else { + LevelFilter::INFO + }; + init_tracing(level)?; + color_eyre::install()?; + let app = App::new(args.api_token.clone()); + app.run(args).await +} + +fn init_tracing(directive: LevelFilter) -> Result<()> { + let filter = EnvFilter::builder() + .from_env()? + .add_directive(directive.into()) + // remove hyper noise + .add_directive("hyper::proto=info".parse()?); + tracing_subscriber::fmt() + .with_env_filter(filter) + .compact() + .init(); + Ok(()) +} + +impl App { + pub fn new(api_token: Option) -> Self { + let client = reqwest::ClientBuilder::new().build().unwrap(); + let api = LichessApi::new(client, api_token); + Self { lichess: api } + } + + async fn run(self, args: Cli) -> Result<()> { + match args.command { + Command::Puzzles { command } => command.run(self.lichess).await, + Command::Engine { command } => command.run(self.lichess).await, + Command::Challenges { command } => command.run(self.lichess).await, + Command::Users { command } => command.run(self.lichess).await, + } + } +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 0000000..47a85b0 --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "lichess-api" +version = "0.6.0" +edition = "2024" +license = "Apache-2.0" +description = "A client library for the Lichess API" +keywords = ["lichess", "api", "client"] +categories = ["api-bindings", "asynchronous"] +homepage = "https://github.com/ion232/lichess-api" +repository = "https://github.com/ion232/lichess-api" +readme = "../README.md" + +[dependencies] +# Library dependencies. +async-std = "1.13.1" +bytes = "1.10.1" +futures = "0.3.31" +futures-core = "0.3.31" +http = "1.3.1" +mime = "0.3.17" +reqwest = { version = "0.12.7", features = ["json", "stream"] } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +serde_with = { version = "3.12.0", features = ["chrono"] } +serde_urlencoded = "0.7.1" +thiserror = "2.0.12" +tracing = "0.1.41" +url = "2.5.4" + +[dev-dependencies] +tokio = { version = "1.44.1", features = ["macros", "rt"] } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } \ No newline at end of file diff --git a/src/api/account.rs b/lib/src/api/account.rs similarity index 100% rename from src/api/account.rs rename to lib/src/api/account.rs diff --git a/src/api/analysis.rs b/lib/src/api/analysis.rs similarity index 100% rename from src/api/analysis.rs rename to lib/src/api/analysis.rs diff --git a/src/api/board.rs b/lib/src/api/board.rs similarity index 100% rename from src/api/board.rs rename to lib/src/api/board.rs diff --git a/src/api/bot.rs b/lib/src/api/bot.rs similarity index 100% rename from src/api/bot.rs rename to lib/src/api/bot.rs diff --git a/src/api/challenges.rs b/lib/src/api/challenges.rs similarity index 97% rename from src/api/challenges.rs rename to lib/src/api/challenges.rs index 661e847..ddbe48b 100644 --- a/src/api/challenges.rs +++ b/lib/src/api/challenges.rs @@ -11,7 +11,7 @@ impl LichessApi { pub async fn create_challenge( &self, request: impl Into, - ) -> Result { + ) -> Result { self.get_single_model(request.into()).await } diff --git a/src/api/external_engine.rs b/lib/src/api/external_engine.rs similarity index 100% rename from src/api/external_engine.rs rename to lib/src/api/external_engine.rs diff --git a/src/api/fide.rs b/lib/src/api/fide.rs similarity index 93% rename from src/api/fide.rs rename to lib/src/api/fide.rs index d7d1fe5..246a792 100644 --- a/src/api/fide.rs +++ b/lib/src/api/fide.rs @@ -1,5 +1,3 @@ -use async_std::stream::StreamExt; - use crate::client::LichessApi; use crate::error::Result; use crate::model::fide::*; diff --git a/src/api/games.rs b/lib/src/api/games.rs similarity index 100% rename from src/api/games.rs rename to lib/src/api/games.rs diff --git a/src/api/messaging.rs b/lib/src/api/messaging.rs similarity index 100% rename from src/api/messaging.rs rename to lib/src/api/messaging.rs diff --git a/src/api/mod.rs b/lib/src/api/mod.rs similarity index 100% rename from src/api/mod.rs rename to lib/src/api/mod.rs diff --git a/src/api/oauth.rs b/lib/src/api/oauth.rs similarity index 100% rename from src/api/oauth.rs rename to lib/src/api/oauth.rs diff --git a/src/api/openings.rs b/lib/src/api/openings.rs similarity index 100% rename from src/api/openings.rs rename to lib/src/api/openings.rs diff --git a/src/api/puzzles.rs b/lib/src/api/puzzles.rs similarity index 73% rename from src/api/puzzles.rs rename to lib/src/api/puzzles.rs index c4ada96..da82d76 100644 --- a/src/api/puzzles.rs +++ b/lib/src/api/puzzles.rs @@ -13,13 +13,27 @@ impl LichessApi { self.get_single_model(request.into()).await } + pub async fn get_new_puzzle( + &self, + request: impl Into, + ) -> Result { + self.get_single_model(request.into()).await + } + pub async fn get_puzzle_activity( &self, request: impl Into, - ) -> Result>> { + ) -> Result>> { self.get_streamed_models(request.into()).await } + pub async fn get_puzzles_to_replay( + &self, + request: impl Into, + ) -> Result { + self.get_single_model(request.into()).await + } + pub async fn get_puzzle_dashboard( &self, request: impl Into, diff --git a/src/api/relations.rs b/lib/src/api/relations.rs similarity index 100% rename from src/api/relations.rs rename to lib/src/api/relations.rs diff --git a/src/api/simuls.rs b/lib/src/api/simuls.rs similarity index 100% rename from src/api/simuls.rs rename to lib/src/api/simuls.rs diff --git a/src/api/studies.rs b/lib/src/api/studies.rs similarity index 100% rename from src/api/studies.rs rename to lib/src/api/studies.rs diff --git a/src/api/tablebase.rs b/lib/src/api/tablebase.rs similarity index 100% rename from src/api/tablebase.rs rename to lib/src/api/tablebase.rs diff --git a/src/api/tv.rs b/lib/src/api/tv.rs similarity index 100% rename from src/api/tv.rs rename to lib/src/api/tv.rs diff --git a/src/api/users.rs b/lib/src/api/users.rs similarity index 80% rename from src/api/users.rs rename to lib/src/api/users.rs index 67f067d..de96673 100644 --- a/src/api/users.rs +++ b/lib/src/api/users.rs @@ -1,8 +1,19 @@ use crate::client::LichessApi; use crate::error::Result; -use crate::model::{users::*, LightUser}; +use crate::model::users::*; impl LichessApi { + pub async fn get_all_top_10(&self) -> Result { + self.get_single_model(top_10::GetRequest::default()).await + } + + pub async fn get_one_leaderboard( + &self, + request: impl Into, + ) -> Result { + self.get_single_model(request.into()).await + } + pub async fn get_public_user_data( &self, request: impl Into, @@ -24,11 +35,10 @@ impl LichessApi { self.get_single_model(request.into()).await } - /// Get performance statistics of a user. pub async fn get_user_performance_statistics( &self, request: impl Into, - ) -> Result { + ) -> Result { self.get_single_model(request.into()).await } @@ -51,15 +61,10 @@ impl LichessApi { self.get_single_model(request.into()).await } - /// Get user autocomplete results. pub async fn autocomplete_users( &self, request: impl Into, - ) -> Result> { - self.get_single_model(request.into()).await - } - - pub async fn get_user_notes(&self, request: impl Into) -> Result> { + ) -> Result { self.get_single_model(request.into()).await } @@ -70,14 +75,17 @@ impl LichessApi { self.get_single_model(request.into()).await } - pub async fn get_all_top_10(&self) -> Result { - self.get_single_model(top_10::GetRequest::default()).await + pub async fn get_user_notes( + &self, + request: impl Into, + ) -> Result> { + self.get_single_model(request.into()).await } - pub async fn get_one_leaderboard( + pub async fn get_user_activity( &self, - request: impl Into, - ) -> Result { + request: impl Into, + ) -> Result> { self.get_single_model(request.into()).await } } diff --git a/src/client.rs b/lib/src/client.rs similarity index 84% rename from src/client.rs rename to lib/src/client.rs index 2e75c96..e58b123 100644 --- a/src/client.rs +++ b/lib/src/client.rs @@ -89,9 +89,10 @@ impl LichessApi { .map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e)) .into_async_read() .lines() - .filter(|l| { + .filter(|l| match l { // To avoid trying to serialize blank keep alive lines. - !l.as_ref().unwrap_or(&"".to_string()).is_empty() + Ok(line) => !line.is_empty(), + Err(_) => true, }) .map(|l| -> Result { let line = l?; @@ -99,6 +100,13 @@ impl LichessApi { if line.starts_with("") { return Err(crate::error::Error::PageNotFound()); } + // Check for error responses returned as json before model serialization is attempted. + // This can happen when not authorized to access an endpoint. + if let Ok(error_value) = serde_json::from_str::(&line) { + if let Some(error_msg) = error_value.get("error").and_then(|e| e.as_str()) { + return Err(crate::error::Error::Response(error_msg.to_string())); + } + } Ok(line) }); diff --git a/src/error.rs b/lib/src/error.rs similarity index 92% rename from src/error.rs rename to lib/src/error.rs index d1ad5f5..c0d14e1 100644 --- a/src/error.rs +++ b/lib/src/error.rs @@ -11,7 +11,7 @@ pub enum Error { #[error("lichess status error: {0}")] LichessStatus(String), - #[error("page not found error")] + #[error("page not found error (likely invalid path)")] PageNotFound(), #[error("request parameters error: {0}")] diff --git a/src/lib.rs b/lib/src/lib.rs similarity index 100% rename from src/lib.rs rename to lib/src/lib.rs diff --git a/src/model/account/email.rs b/lib/src/model/account/email.rs similarity index 100% rename from src/model/account/email.rs rename to lib/src/model/account/email.rs diff --git a/src/model/account/kid.rs b/lib/src/model/account/kid.rs similarity index 100% rename from src/model/account/kid.rs rename to lib/src/model/account/kid.rs diff --git a/src/model/account/mod.rs b/lib/src/model/account/mod.rs similarity index 100% rename from src/model/account/mod.rs rename to lib/src/model/account/mod.rs diff --git a/src/model/account/preferences.rs b/lib/src/model/account/preferences.rs similarity index 100% rename from src/model/account/preferences.rs rename to lib/src/model/account/preferences.rs diff --git a/src/model/account/profile.rs b/lib/src/model/account/profile.rs similarity index 100% rename from src/model/account/profile.rs rename to lib/src/model/account/profile.rs diff --git a/src/model/analysis/cloud.rs b/lib/src/model/analysis/cloud.rs similarity index 100% rename from src/model/analysis/cloud.rs rename to lib/src/model/analysis/cloud.rs diff --git a/src/model/analysis/mod.rs b/lib/src/model/analysis/mod.rs similarity index 100% rename from src/model/analysis/mod.rs rename to lib/src/model/analysis/mod.rs diff --git a/src/model/board/abort.rs b/lib/src/model/board/abort.rs similarity index 100% rename from src/model/board/abort.rs rename to lib/src/model/board/abort.rs diff --git a/src/model/board/berserk.rs b/lib/src/model/board/berserk.rs similarity index 100% rename from src/model/board/berserk.rs rename to lib/src/model/board/berserk.rs diff --git a/src/model/board/chat.rs b/lib/src/model/board/chat.rs similarity index 100% rename from src/model/board/chat.rs rename to lib/src/model/board/chat.rs diff --git a/src/model/board/claim_victory.rs b/lib/src/model/board/claim_victory.rs similarity index 100% rename from src/model/board/claim_victory.rs rename to lib/src/model/board/claim_victory.rs diff --git a/src/model/board/draw.rs b/lib/src/model/board/draw.rs similarity index 100% rename from src/model/board/draw.rs rename to lib/src/model/board/draw.rs diff --git a/src/model/board/mod.rs b/lib/src/model/board/mod.rs similarity index 100% rename from src/model/board/mod.rs rename to lib/src/model/board/mod.rs diff --git a/src/model/board/move.rs b/lib/src/model/board/move.rs similarity index 100% rename from src/model/board/move.rs rename to lib/src/model/board/move.rs diff --git a/src/model/board/resign.rs b/lib/src/model/board/resign.rs similarity index 100% rename from src/model/board/resign.rs rename to lib/src/model/board/resign.rs diff --git a/src/model/board/seek.rs b/lib/src/model/board/seek.rs similarity index 100% rename from src/model/board/seek.rs rename to lib/src/model/board/seek.rs diff --git a/src/model/board/stream/events.rs b/lib/src/model/board/stream/events.rs similarity index 88% rename from src/model/board/stream/events.rs rename to lib/src/model/board/stream/events.rs index 66e41de..a89323a 100644 --- a/src/model/board/stream/events.rs +++ b/lib/src/model/board/stream/events.rs @@ -1,5 +1,5 @@ -use crate::model::challenges::ChallengeJson; -use crate::model::{Color, Compat, Speed, Variant}; +use crate::model::challenges::{ChallengeDeclinedJson, ChallengeJson}; +use crate::model::{Color, GameCompat, Speed, Variant}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -27,7 +27,7 @@ pub struct Event { #[serde(flatten)] pub event: EventData, #[serde(skip_serializing_if = "Option::is_none")] - pub compat: Option, + pub compat: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -36,7 +36,7 @@ pub struct Event { pub enum EventData { Challenge { challenge: ChallengeJson }, ChallengeCanceled { challenge: ChallengeJson }, - ChallengeDeclined { challenge: ChallengeJson }, + ChallengeDeclined { challenge: ChallengeDeclinedJson }, GameStart { game: GameEventInfo }, GameFinish { game: GameEventInfo }, } @@ -59,7 +59,7 @@ pub struct GameEventInfo { pub opponent: Opponent, pub is_my_turn: bool, pub seconds_left: Option, - pub compat: Option, + pub compat: Option, pub winner: Option, } diff --git a/src/model/board/stream/game.rs b/lib/src/model/board/stream/game.rs similarity index 100% rename from src/model/board/stream/game.rs rename to lib/src/model/board/stream/game.rs diff --git a/src/model/board/stream/mod.rs b/lib/src/model/board/stream/mod.rs similarity index 100% rename from src/model/board/stream/mod.rs rename to lib/src/model/board/stream/mod.rs diff --git a/src/model/board/takeback.rs b/lib/src/model/board/takeback.rs similarity index 100% rename from src/model/board/takeback.rs rename to lib/src/model/board/takeback.rs diff --git a/src/model/bot/abort.rs b/lib/src/model/bot/abort.rs similarity index 100% rename from src/model/bot/abort.rs rename to lib/src/model/bot/abort.rs diff --git a/src/model/bot/chat.rs b/lib/src/model/bot/chat.rs similarity index 100% rename from src/model/bot/chat.rs rename to lib/src/model/bot/chat.rs diff --git a/src/model/bot/draw.rs b/lib/src/model/bot/draw.rs similarity index 100% rename from src/model/bot/draw.rs rename to lib/src/model/bot/draw.rs diff --git a/src/model/bot/mod.rs b/lib/src/model/bot/mod.rs similarity index 100% rename from src/model/bot/mod.rs rename to lib/src/model/bot/mod.rs diff --git a/src/model/bot/move.rs b/lib/src/model/bot/move.rs similarity index 100% rename from src/model/bot/move.rs rename to lib/src/model/bot/move.rs diff --git a/src/model/bot/online.rs b/lib/src/model/bot/online.rs similarity index 100% rename from src/model/bot/online.rs rename to lib/src/model/bot/online.rs diff --git a/src/model/bot/resign.rs b/lib/src/model/bot/resign.rs similarity index 100% rename from src/model/bot/resign.rs rename to lib/src/model/bot/resign.rs diff --git a/src/model/bot/stream/events.rs b/lib/src/model/bot/stream/events.rs similarity index 100% rename from src/model/bot/stream/events.rs rename to lib/src/model/bot/stream/events.rs diff --git a/src/model/bot/stream/game.rs b/lib/src/model/bot/stream/game.rs similarity index 100% rename from src/model/bot/stream/game.rs rename to lib/src/model/bot/stream/game.rs diff --git a/src/model/bot/stream/mod.rs b/lib/src/model/bot/stream/mod.rs similarity index 100% rename from src/model/bot/stream/mod.rs rename to lib/src/model/bot/stream/mod.rs diff --git a/src/model/bot/upgrade.rs b/lib/src/model/bot/upgrade.rs similarity index 100% rename from src/model/bot/upgrade.rs rename to lib/src/model/bot/upgrade.rs diff --git a/src/model/challenges/accept.rs b/lib/src/model/challenges/accept.rs similarity index 100% rename from src/model/challenges/accept.rs rename to lib/src/model/challenges/accept.rs diff --git a/src/model/challenges/add_time.rs b/lib/src/model/challenges/add_time.rs similarity index 100% rename from src/model/challenges/add_time.rs rename to lib/src/model/challenges/add_time.rs diff --git a/src/model/challenges/ai.rs b/lib/src/model/challenges/ai.rs similarity index 100% rename from src/model/challenges/ai.rs rename to lib/src/model/challenges/ai.rs diff --git a/src/model/challenges/cancel.rs b/lib/src/model/challenges/cancel.rs similarity index 100% rename from src/model/challenges/cancel.rs rename to lib/src/model/challenges/cancel.rs diff --git a/src/model/challenges/create.rs b/lib/src/model/challenges/create.rs similarity index 100% rename from src/model/challenges/create.rs rename to lib/src/model/challenges/create.rs diff --git a/src/model/challenges/decline.rs b/lib/src/model/challenges/decline.rs similarity index 100% rename from src/model/challenges/decline.rs rename to lib/src/model/challenges/decline.rs diff --git a/src/model/challenges/list.rs b/lib/src/model/challenges/list.rs similarity index 100% rename from src/model/challenges/list.rs rename to lib/src/model/challenges/list.rs index 731236f..21af84d 100644 --- a/src/model/challenges/list.rs +++ b/lib/src/model/challenges/list.rs @@ -1,5 +1,5 @@ -use crate::model::challenges::ChallengeJson; use crate::model::Request; +use crate::model::challenges::ChallengeJson; use serde::{Deserialize, Serialize}; #[derive(Default, Clone, Debug, Serialize)] diff --git a/src/model/challenges/mod.rs b/lib/src/model/challenges/mod.rs similarity index 61% rename from src/model/challenges/mod.rs rename to lib/src/model/challenges/mod.rs index bc9626a..1cd1d17 100644 --- a/src/model/challenges/mod.rs +++ b/lib/src/model/challenges/mod.rs @@ -8,7 +8,7 @@ pub mod list; pub mod open; pub mod start_clocks; -use crate::model::{Color, Compat, Days, LightUser, Speed, Variant, VariantKey}; +use crate::model::{Color, Days, GameCompat, Speed, Title, Variant, VariantKey}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -62,20 +62,63 @@ pub struct ChallengeBase { pub fen: Option, } -#[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChallengeOpenJson { - #[serde(flatten)] - pub base: ChallengeJsonBase, + pub id: String, + pub url: String, + pub status: Status, + pub challenger: Option, + pub dest_user: Option, + pub variant: Variant, + pub rated: bool, + pub speed: Speed, + pub time_control: TimeControl, + pub color: Color, + pub final_color: Option, + pub perf: Perf, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_fen: Option, pub url_white: String, pub url_black: String, + pub open: OpenChallengeUsers, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct OpenChallengeUsers { + #[serde(skip_serializing_if = "Option::is_none")] + pub user_ids: Option>, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ChallengeCreated { - pub challenge: ChallengeJson, +pub struct ChallengeDeclinedJson { + #[serde(flatten)] + pub base: ChallengeJson, + pub decline_reason: String, + pub decline_reason_key: DeclineReasonKey, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum DeclineReasonKey { + Generic, + Later, + #[serde(rename = "toofast")] + TooFast, + #[serde(rename = "tooslow")] + TooSlow, + #[serde(rename = "timecontrol")] + TimeControl, + Rated, + Casual, + Standard, + Variant, + #[serde(rename = "nobot")] + NoBot, + #[serde(rename = "onlybot")] + OnlyBot, } #[skip_serializing_none] @@ -107,6 +150,7 @@ pub struct ChallengeJsonBase { pub rated: bool, pub speed: Speed, pub status: Status, + pub final_color: Option, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] @@ -132,6 +176,31 @@ pub enum Status { Accepted, } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChallengeEvent { + #[serde(rename = "type")] + pub event_type: String, // Always "challenge" + pub challenge: ChallengeJson, + pub compat: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChallengeCanceledEvent { + #[serde(rename = "type")] + pub event_type: String, // Always "challengeCanceled" + pub challenge: ChallengeJson, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChallengeDeclinedEvent { + #[serde(rename = "type")] + pub event_type: String, // Always "challengeDeclined" + pub challenge: ChallengeDeclinedJson, +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type")] #[serde(rename_all = "camelCase")] @@ -150,9 +219,12 @@ pub enum TimeControl { #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ChallengeUser { - #[serde(flatten)] - pub user: LightUser, + pub id: String, + pub name: String, pub rating: u32, + pub title: Option, + pub flair: Option<String>, + pub patron: Option<bool>, pub provisional: Option<bool>, pub online: Option<bool>, pub lag: Option<u32>, diff --git a/src/model/challenges/open.rs b/lib/src/model/challenges/open.rs similarity index 100% rename from src/model/challenges/open.rs rename to lib/src/model/challenges/open.rs diff --git a/src/model/challenges/start_clocks.rs b/lib/src/model/challenges/start_clocks.rs similarity index 100% rename from src/model/challenges/start_clocks.rs rename to lib/src/model/challenges/start_clocks.rs diff --git a/src/model/external_engine/acquire_analysis.rs b/lib/src/model/external_engine/acquire_analysis.rs similarity index 100% rename from src/model/external_engine/acquire_analysis.rs rename to lib/src/model/external_engine/acquire_analysis.rs diff --git a/src/model/external_engine/analyse.rs b/lib/src/model/external_engine/analyse.rs similarity index 100% rename from src/model/external_engine/analyse.rs rename to lib/src/model/external_engine/analyse.rs diff --git a/src/model/external_engine/create.rs b/lib/src/model/external_engine/create.rs similarity index 100% rename from src/model/external_engine/create.rs rename to lib/src/model/external_engine/create.rs diff --git a/src/model/external_engine/delete.rs b/lib/src/model/external_engine/delete.rs similarity index 100% rename from src/model/external_engine/delete.rs rename to lib/src/model/external_engine/delete.rs diff --git a/src/model/external_engine/id.rs b/lib/src/model/external_engine/id.rs similarity index 100% rename from src/model/external_engine/id.rs rename to lib/src/model/external_engine/id.rs diff --git a/src/model/external_engine/list.rs b/lib/src/model/external_engine/list.rs similarity index 100% rename from src/model/external_engine/list.rs rename to lib/src/model/external_engine/list.rs diff --git a/src/model/external_engine/mod.rs b/lib/src/model/external_engine/mod.rs similarity index 100% rename from src/model/external_engine/mod.rs rename to lib/src/model/external_engine/mod.rs diff --git a/src/model/external_engine/update.rs b/lib/src/model/external_engine/update.rs similarity index 100% rename from src/model/external_engine/update.rs rename to lib/src/model/external_engine/update.rs diff --git a/src/model/fide/mod.rs b/lib/src/model/fide/mod.rs similarity index 95% rename from src/model/fide/mod.rs rename to lib/src/model/fide/mod.rs index 848b20d..28e72a9 100644 --- a/src/model/fide/mod.rs +++ b/lib/src/model/fide/mod.rs @@ -3,7 +3,6 @@ pub mod search; use crate::model::Title; use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; #[serde_with::skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/src/model/fide/player.rs b/lib/src/model/fide/player.rs similarity index 81% rename from src/model/fide/player.rs rename to lib/src/model/fide/player.rs index 3a692f5..a2bd65a 100644 --- a/src/model/fide/player.rs +++ b/lib/src/model/fide/player.rs @@ -1,5 +1,4 @@ -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; +use serde::Serialize; #[derive(Default, Clone, Debug, Serialize)] pub struct GetQuery; diff --git a/src/model/fide/search.rs b/lib/src/model/fide/search.rs similarity index 100% rename from src/model/fide/search.rs rename to lib/src/model/fide/search.rs diff --git a/src/model/games/export/by_ids.rs b/lib/src/model/games/export/by_ids.rs similarity index 90% rename from src/model/games/export/by_ids.rs rename to lib/src/model/games/export/by_ids.rs index 315401b..71f10e6 100644 --- a/src/model/games/export/by_ids.rs +++ b/lib/src/model/games/export/by_ids.rs @@ -1,4 +1,4 @@ -use crate::model::{games::export::Base, Body}; +use crate::model::{Body, games::export::Base}; use serde::Serialize; use std::borrow::Borrow; diff --git a/src/model/games/export/by_user.rs b/lib/src/model/games/export/by_user.rs similarity index 100% rename from src/model/games/export/by_user.rs rename to lib/src/model/games/export/by_user.rs diff --git a/src/model/games/export/mod.rs b/lib/src/model/games/export/mod.rs similarity index 100% rename from src/model/games/export/mod.rs rename to lib/src/model/games/export/mod.rs diff --git a/src/model/games/export/one.rs b/lib/src/model/games/export/one.rs similarity index 100% rename from src/model/games/export/one.rs rename to lib/src/model/games/export/one.rs diff --git a/src/model/games/export/ongoing.rs b/lib/src/model/games/export/ongoing.rs similarity index 100% rename from src/model/games/export/ongoing.rs rename to lib/src/model/games/export/ongoing.rs diff --git a/src/model/games/import.rs b/lib/src/model/games/import.rs similarity index 100% rename from src/model/games/import.rs rename to lib/src/model/games/import.rs diff --git a/src/model/games/mod.rs b/lib/src/model/games/mod.rs similarity index 100% rename from src/model/games/mod.rs rename to lib/src/model/games/mod.rs diff --git a/src/model/games/ongoing.rs b/lib/src/model/games/ongoing.rs similarity index 100% rename from src/model/games/ongoing.rs rename to lib/src/model/games/ongoing.rs diff --git a/src/model/games/stream/add_ids.rs b/lib/src/model/games/stream/add_ids.rs similarity index 100% rename from src/model/games/stream/add_ids.rs rename to lib/src/model/games/stream/add_ids.rs diff --git a/src/model/games/stream/by_ids.rs b/lib/src/model/games/stream/by_ids.rs similarity index 100% rename from src/model/games/stream/by_ids.rs rename to lib/src/model/games/stream/by_ids.rs diff --git a/src/model/games/stream/by_users.rs b/lib/src/model/games/stream/by_users.rs similarity index 100% rename from src/model/games/stream/by_users.rs rename to lib/src/model/games/stream/by_users.rs diff --git a/src/model/games/stream/mod.rs b/lib/src/model/games/stream/mod.rs similarity index 100% rename from src/model/games/stream/mod.rs rename to lib/src/model/games/stream/mod.rs diff --git a/src/model/games/stream/moves.rs b/lib/src/model/games/stream/moves.rs similarity index 100% rename from src/model/games/stream/moves.rs rename to lib/src/model/games/stream/moves.rs index e0550b9..4df8775 100644 --- a/src/model/games/stream/moves.rs +++ b/lib/src/model/games/stream/moves.rs @@ -1,5 +1,5 @@ -use crate::model::games::Players; use crate::model::Variant; +use crate::model::games::Players; use serde::{Deserialize, Serialize}; diff --git a/src/model/messaging/inbox.rs b/lib/src/model/messaging/inbox.rs similarity index 100% rename from src/model/messaging/inbox.rs rename to lib/src/model/messaging/inbox.rs diff --git a/src/model/messaging/mod.rs b/lib/src/model/messaging/mod.rs similarity index 100% rename from src/model/messaging/mod.rs rename to lib/src/model/messaging/mod.rs diff --git a/src/model/mod.rs b/lib/src/model/mod.rs similarity index 94% rename from src/model/mod.rs rename to lib/src/model/mod.rs index fa107bb..34b6d29 100644 --- a/src/model/mod.rs +++ b/lib/src/model/mod.rs @@ -18,7 +18,7 @@ pub mod tv; pub mod users; use crate::error; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_with::skip_serializing_none; pub trait BodyBounds: Serialize {} @@ -321,6 +321,7 @@ pub struct LightUser { pub title: Option<Title>, pub flair: Option<String>, pub patron: Option<bool>, + pub online: Option<bool>, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] @@ -374,9 +375,9 @@ pub enum Room { } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Compat { - pub bot: bool, - pub board: bool, +pub struct GameCompat { + pub bot: Option<bool>, + pub board: Option<bool>, } #[serde_with::skip_serializing_none] @@ -399,6 +400,21 @@ pub enum Days { Fourteen, } +impl From<u32> for Days { + fn from(value: u32) -> Self { + match value { + 1 => Days::One, + 2 => Days::Two, + 3 => Days::Three, + 5 => Days::Five, + 7 => Days::Seven, + 10 => Days::Ten, + 14 => Days::Fourteen, + _ => panic!("Invalid days {}", value), + } + } +} + impl Into<u32> for Days { fn into(self) -> u32 { match self { diff --git a/src/model/oauth/mod.rs b/lib/src/model/oauth/mod.rs similarity index 100% rename from src/model/oauth/mod.rs rename to lib/src/model/oauth/mod.rs diff --git a/src/model/oauth/revoke.rs b/lib/src/model/oauth/revoke.rs similarity index 100% rename from src/model/oauth/revoke.rs rename to lib/src/model/oauth/revoke.rs diff --git a/src/model/oauth/test.rs b/lib/src/model/oauth/test.rs similarity index 100% rename from src/model/oauth/test.rs rename to lib/src/model/oauth/test.rs diff --git a/src/model/openings/lichess.rs b/lib/src/model/openings/lichess.rs similarity index 100% rename from src/model/openings/lichess.rs rename to lib/src/model/openings/lichess.rs diff --git a/src/model/openings/masters.rs b/lib/src/model/openings/masters.rs similarity index 100% rename from src/model/openings/masters.rs rename to lib/src/model/openings/masters.rs diff --git a/src/model/openings/mod.rs b/lib/src/model/openings/mod.rs similarity index 100% rename from src/model/openings/mod.rs rename to lib/src/model/openings/mod.rs diff --git a/src/model/openings/otb.rs b/lib/src/model/openings/otb.rs similarity index 100% rename from src/model/openings/otb.rs rename to lib/src/model/openings/otb.rs diff --git a/src/model/openings/player.rs b/lib/src/model/openings/player.rs similarity index 100% rename from src/model/openings/player.rs rename to lib/src/model/openings/player.rs diff --git a/src/model/puzzles/activity.rs b/lib/src/model/puzzles/activity.rs similarity index 84% rename from src/model/puzzles/activity.rs rename to lib/src/model/puzzles/activity.rs index ac4768b..794310c 100644 --- a/src/model/puzzles/activity.rs +++ b/lib/src/model/puzzles/activity.rs @@ -26,20 +26,23 @@ impl From<u32> for GetRequest { } } -pub type Round = PuzzleRoundJson; +pub type Activity = PuzzleActivity; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PuzzleRoundJson { +pub struct PuzzleActivity { pub date: u64, pub win: bool, pub puzzle: Puzzle, } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Puzzle { pub id: String, pub fen: String, + pub last_move: Option<String>, pub plays: i32, pub rating: i32, pub solution: Vec<String>, diff --git a/src/model/puzzles/daily.rs b/lib/src/model/puzzles/daily.rs similarity index 100% rename from src/model/puzzles/daily.rs rename to lib/src/model/puzzles/daily.rs diff --git a/src/model/puzzles/dashboard.rs b/lib/src/model/puzzles/dashboard.rs similarity index 84% rename from src/model/puzzles/dashboard.rs rename to lib/src/model/puzzles/dashboard.rs index 5c8d9e9..530ab2f 100644 --- a/src/model/puzzles/dashboard.rs +++ b/lib/src/model/puzzles/dashboard.rs @@ -19,23 +19,23 @@ impl From<u32> for GetRequest { } } -pub type Dashboard = PuzzleDashboardJson; +pub type Dashboard = PuzzleDashboard; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PuzzleDashboardJson { +pub struct PuzzleDashboard { days: i64, - global: Results, + global: PuzzlePerformance, themes: HashMap<String, Theme>, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Theme { - results: Results, + results: PuzzlePerformance, theme: String, } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Results { +pub struct PuzzlePerformance { #[serde(rename = "firstWins")] first_wins: i64, nb: i64, diff --git a/src/model/puzzles/id.rs b/lib/src/model/puzzles/id.rs similarity index 100% rename from src/model/puzzles/id.rs rename to lib/src/model/puzzles/id.rs diff --git a/src/model/puzzles/mod.rs b/lib/src/model/puzzles/mod.rs similarity index 84% rename from src/model/puzzles/mod.rs rename to lib/src/model/puzzles/mod.rs index 3480ef4..4497041 100644 --- a/src/model/puzzles/mod.rs +++ b/lib/src/model/puzzles/mod.rs @@ -2,9 +2,12 @@ pub mod activity; pub mod daily; pub mod dashboard; pub mod id; +pub mod next; pub mod race; +pub mod replay; pub mod storm_dashboard; +use super::Title; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -44,7 +47,10 @@ pub struct Perf { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Player { pub color: String, + pub id: String, pub name: String, - #[serde(rename = "userId")] - pub user_id: Option<String>, + pub rating: i32, + pub flair: Option<String>, + pub patron: Option<bool>, + pub title: Option<Title>, } diff --git a/lib/src/model/puzzles/next.rs b/lib/src/model/puzzles/next.rs new file mode 100644 index 0000000..d367b04 --- /dev/null +++ b/lib/src/model/puzzles/next.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +#[serde_with::skip_serializing_none] +#[derive(Default, Clone, Debug, Serialize)] +pub struct GetQuery { + pub angle: Option<String>, + pub difficulty: Option<Difficulty>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Difficulty { + Easiest, + Easier, + Normal, + Harder, + Hardest, +} + +pub type GetRequest = crate::model::Request<GetQuery>; + +impl GetRequest { + pub fn new(angle: Option<String>, difficulty: Option<Difficulty>) -> Self { + let query = GetQuery { angle, difficulty }; + Self::get("/api/puzzle/next", query, None) + } +} + +impl Default for GetRequest { + fn default() -> Self { + Self::new(None, None) + } +} + +pub type Puzzle = super::PuzzleAndGame; diff --git a/src/model/puzzles/race.rs b/lib/src/model/puzzles/race.rs similarity index 88% rename from src/model/puzzles/race.rs rename to lib/src/model/puzzles/race.rs index 2978a44..9faa280 100644 --- a/src/model/puzzles/race.rs +++ b/lib/src/model/puzzles/race.rs @@ -17,10 +17,10 @@ impl Default for PostRequest { } } -pub type Race = PuzzleRaceJson; +pub type Race = PuzzleRacer; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PuzzleRaceJson { +pub struct PuzzleRacer { id: String, url: String, } diff --git a/lib/src/model/puzzles/replay.rs b/lib/src/model/puzzles/replay.rs new file mode 100644 index 0000000..e56d26e --- /dev/null +++ b/lib/src/model/puzzles/replay.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Clone, Debug, Serialize)] +pub struct GetQuery; + +pub type GetRequest = crate::model::Request<GetQuery>; + +impl GetRequest { + pub fn new(days: u32, theme: &str) -> Self { + Self::get(format!("/api/puzzle/replay/{days}/{theme}"), None, None) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PuzzleReplay { + pub replay: ReplayData, + pub angle: AngleData, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ReplayData { + pub days: u32, + pub theme: String, + pub nb: u32, + pub remaining: Vec<String>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AngleData { + pub key: String, + pub name: String, + pub desc: String, +} + +pub type Replay = PuzzleReplay; diff --git a/src/model/puzzles/storm_dashboard.rs b/lib/src/model/puzzles/storm_dashboard.rs similarity index 93% rename from src/model/puzzles/storm_dashboard.rs rename to lib/src/model/puzzles/storm_dashboard.rs index 37f6e9a..0e61183 100644 --- a/src/model/puzzles/storm_dashboard.rs +++ b/lib/src/model/puzzles/storm_dashboard.rs @@ -21,10 +21,10 @@ impl<S: AsRef<str>> From<S> for GetRequest { } } -pub type Dashboard = StormDashboardJson; +pub type Dashboard = StormDashboard; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct StormDashboardJson { +pub struct StormDashboard { high: High, days: Vec<Day>, } diff --git a/src/model/relations/block.rs b/lib/src/model/relations/block.rs similarity index 100% rename from src/model/relations/block.rs rename to lib/src/model/relations/block.rs diff --git a/src/model/relations/follow.rs b/lib/src/model/relations/follow.rs similarity index 100% rename from src/model/relations/follow.rs rename to lib/src/model/relations/follow.rs diff --git a/src/model/relations/following.rs b/lib/src/model/relations/following.rs similarity index 100% rename from src/model/relations/following.rs rename to lib/src/model/relations/following.rs diff --git a/src/model/relations/mod.rs b/lib/src/model/relations/mod.rs similarity index 100% rename from src/model/relations/mod.rs rename to lib/src/model/relations/mod.rs diff --git a/src/model/relations/unblock.rs b/lib/src/model/relations/unblock.rs similarity index 100% rename from src/model/relations/unblock.rs rename to lib/src/model/relations/unblock.rs diff --git a/src/model/relations/unfollow.rs b/lib/src/model/relations/unfollow.rs similarity index 100% rename from src/model/relations/unfollow.rs rename to lib/src/model/relations/unfollow.rs diff --git a/src/model/simuls/current.rs b/lib/src/model/simuls/current.rs similarity index 100% rename from src/model/simuls/current.rs rename to lib/src/model/simuls/current.rs diff --git a/src/model/simuls/mod.rs b/lib/src/model/simuls/mod.rs similarity index 89% rename from src/model/simuls/mod.rs rename to lib/src/model/simuls/mod.rs index efc3d76..a8c9cd7 100644 --- a/src/model/simuls/mod.rs +++ b/lib/src/model/simuls/mod.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; +use crate::model::LightUser; + use super::{Title, Variant}; pub mod current; @@ -30,11 +32,9 @@ pub struct Simul { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Host { - pub id: String, - pub name: String, + #[serde(flatten)] + pub user: LightUser, pub rating: u32, - pub title: Option<Title>, pub game_id: Option<String>, - pub online: Option<bool>, pub provisional: Option<bool>, } diff --git a/src/model/studies/import_pgn_into_study.rs b/lib/src/model/studies/import_pgn_into_study.rs similarity index 100% rename from src/model/studies/import_pgn_into_study.rs rename to lib/src/model/studies/import_pgn_into_study.rs diff --git a/src/model/studies/mod.rs b/lib/src/model/studies/mod.rs similarity index 100% rename from src/model/studies/mod.rs rename to lib/src/model/studies/mod.rs diff --git a/src/model/tablebase/antichess.rs b/lib/src/model/tablebase/antichess.rs similarity index 100% rename from src/model/tablebase/antichess.rs rename to lib/src/model/tablebase/antichess.rs diff --git a/src/model/tablebase/atomic.rs b/lib/src/model/tablebase/atomic.rs similarity index 100% rename from src/model/tablebase/atomic.rs rename to lib/src/model/tablebase/atomic.rs diff --git a/src/model/tablebase/mod.rs b/lib/src/model/tablebase/mod.rs similarity index 100% rename from src/model/tablebase/mod.rs rename to lib/src/model/tablebase/mod.rs diff --git a/src/model/tablebase/standard.rs b/lib/src/model/tablebase/standard.rs similarity index 100% rename from src/model/tablebase/standard.rs rename to lib/src/model/tablebase/standard.rs diff --git a/src/model/tv/channels.rs b/lib/src/model/tv/channels.rs similarity index 100% rename from src/model/tv/channels.rs rename to lib/src/model/tv/channels.rs diff --git a/src/model/tv/games.rs b/lib/src/model/tv/games.rs similarity index 100% rename from src/model/tv/games.rs rename to lib/src/model/tv/games.rs diff --git a/src/model/tv/mod.rs b/lib/src/model/tv/mod.rs similarity index 100% rename from src/model/tv/mod.rs rename to lib/src/model/tv/mod.rs diff --git a/src/model/tv/stream/channel.rs b/lib/src/model/tv/stream/channel.rs similarity index 100% rename from src/model/tv/stream/channel.rs rename to lib/src/model/tv/stream/channel.rs diff --git a/src/model/tv/stream/current.rs b/lib/src/model/tv/stream/current.rs similarity index 100% rename from src/model/tv/stream/current.rs rename to lib/src/model/tv/stream/current.rs diff --git a/src/model/tv/stream/mod.rs b/lib/src/model/tv/stream/mod.rs similarity index 100% rename from src/model/tv/stream/mod.rs rename to lib/src/model/tv/stream/mod.rs diff --git a/lib/src/model/users/activity.rs b/lib/src/model/users/activity.rs new file mode 100644 index 0000000..f845f4a --- /dev/null +++ b/lib/src/model/users/activity.rs @@ -0,0 +1,174 @@ +use crate::model::{Color, Request, VariantKey}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use std::collections::HashMap; + +#[derive(Default, Clone, Debug, Deserialize, Serialize)] +pub struct GetQuery; + +pub type GetRequest = Request<GetQuery>; + +impl GetRequest { + pub fn new(username: &str) -> Self { + Self::get(format!("/api/user/{username}/activity"), None, None) + } +} + +// Main UserActivity struct matching OpenAPI spec +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserActivity { + pub interval: ActivityInterval, + pub games: Option<HashMap<String, UserActivityScore>>, + pub puzzles: Option<PuzzleActivity>, + pub storm: Option<PuzzleModePerf>, + pub racer: Option<PuzzleModePerf>, + pub streak: Option<PuzzleModePerf>, + pub tournaments: Option<TournamentActivity>, + pub practice: Option<Vec<PracticeActivity>>, + pub simuls: Option<Vec<String>>, + pub correspondence_moves: Option<CorrespondenceMoves>, + pub correspondence_ends: Option<CorrespondenceEnds>, + pub follows: Option<FollowActivity>, + pub studies: Option<serde_json::Value>, // OpenAPI shows empty object + pub teams: Option<Vec<TeamActivity>>, + pub posts: Option<Vec<PostActivity>>, + pub patron: Option<PatronActivity>, + pub stream: Option<bool>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ActivityInterval { + pub start: u64, + pub end: u64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UserActivityScore { + pub win: u32, + pub loss: u32, + pub draw: u32, + pub rp: RatingProgress, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RatingProgress { + pub before: u32, + pub after: u32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PuzzleActivity { + pub score: UserActivityScore, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PuzzleModePerf { + pub runs: u32, + pub score: u32, +} + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TournamentActivity { + pub nb: u32, + pub best: Option<Vec<TournamentResult>>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TournamentResult { + pub tournament: TournamentInfo, + pub nb_games: u32, + pub score: u32, + pub rank: u32, + pub rank_percent: u32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TournamentInfo { + pub id: String, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PracticeActivity { + pub url: String, + pub name: String, + pub nb_positions: u32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CorrespondenceMoves { + pub nb: u32, + pub games: Vec<UserActivityCorrespondenceGame>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CorrespondenceEnds { + pub score: UserActivityScore, + pub games: Vec<UserActivityCorrespondenceGame>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UserActivityCorrespondenceGame { + pub id: String, + pub color: Color, + pub url: String, + pub variant: VariantKey, + pub speed: String, // Always "correspondence" + pub perf: String, // Always "correspondence" + pub rated: bool, + pub opponent: CorrespondenceOpponent, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CorrespondenceOpponent { + pub user: String, + pub rating: u32, +} + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct FollowActivity { + #[serde(rename = "in")] + pub incoming: Option<UserActivityFollowList>, + pub out: Option<UserActivityFollowList>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UserActivityFollowList { + pub ids: Vec<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub nb: Option<u32>, +} + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TeamActivity { + pub url: String, + pub name: String, + pub flair: Option<String>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PostActivity { + pub topic_url: String, + pub topic_name: String, + pub posts: Vec<PostInfo>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PostInfo { + pub url: String, + pub text: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PatronActivity { + pub months: u32, +} diff --git a/src/model/users/autocomplete.rs b/lib/src/model/users/autocomplete.rs similarity index 68% rename from src/model/users/autocomplete.rs rename to lib/src/model/users/autocomplete.rs index d5fccc4..05f11ff 100644 --- a/src/model/users/autocomplete.rs +++ b/lib/src/model/users/autocomplete.rs @@ -1,5 +1,10 @@ -use crate::model::Request; -use serde::Serialize; +use crate::model::{LightUser, Request}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Autocompletions { + pub result: Vec<LightUser>, +} #[serde_with::skip_serializing_none] #[derive(Default, Clone, Debug, Serialize)] @@ -19,7 +24,7 @@ impl GetRequest { friend, }; - Self::get("/api/autocomplete", query, None) + Self::get("/api/player/autocomplete", query, None) } } diff --git a/src/model/users/by_id.rs b/lib/src/model/users/by_id.rs similarity index 100% rename from src/model/users/by_id.rs rename to lib/src/model/users/by_id.rs diff --git a/src/model/users/crosstable.rs b/lib/src/model/users/crosstable.rs similarity index 100% rename from src/model/users/crosstable.rs rename to lib/src/model/users/crosstable.rs diff --git a/src/model/users/leaderboard.rs b/lib/src/model/users/leaderboard.rs similarity index 100% rename from src/model/users/leaderboard.rs rename to lib/src/model/users/leaderboard.rs diff --git a/src/model/users/live_streamers.rs b/lib/src/model/users/live_streamers.rs similarity index 100% rename from src/model/users/live_streamers.rs rename to lib/src/model/users/live_streamers.rs diff --git a/src/model/users/mod.rs b/lib/src/model/users/mod.rs similarity index 75% rename from src/model/users/mod.rs rename to lib/src/model/users/mod.rs index bbb4c7d..61678b1 100644 --- a/src/model/users/mod.rs +++ b/lib/src/model/users/mod.rs @@ -25,13 +25,12 @@ pub struct UserExtended { pub user: User, pub url: String, pub playing: Option<String>, - pub completion_rate: Option<u32>, pub count: Count, pub streaming: Option<bool>, - pub followable: bool, - pub following: bool, - pub blocking: bool, - pub follows_you: bool, + pub streamer: Option<UserStreamer>, + pub followable: Option<bool>, + pub following: Option<bool>, + pub blocking: Option<bool>, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -68,6 +67,7 @@ pub struct User { pub verified: Option<bool>, pub play_time: PlayTime, pub title: Option<Title>, + pub flair: Option<String>, } #[skip_serializing_none] @@ -86,6 +86,11 @@ pub struct Perfs { pub puzzle: Option<Perf>, pub classical: Option<Perf>, pub rapid: Option<Perf>, + pub three_check: Option<Perf>, + pub antichess: Option<Perf>, + pub crazyhouse: Option<Perf>, + pub storm: Option<Storm>, + pub racer: Option<Storm>, pub streak: Option<Storm>, } @@ -95,6 +100,7 @@ pub struct Perf { pub rating: u32, pub rd: u32, pub prog: i32, + #[serde(skip_serializing_if = "Option::is_none")] pub prov: Option<bool>, } @@ -113,14 +119,17 @@ pub struct PlayTime { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Profile { - pub country: Option<String>, + pub flag: Option<String>, pub location: Option<String>, pub bio: Option<String>, - pub first_name: Option<String>, - pub last_name: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub real_name: Option<String>, pub fide_rating: Option<u32>, pub uscf_rating: Option<u32>, pub ecf_rating: Option<u32>, + pub cfc_rating: Option<u32>, + pub rcf_rating: Option<u32>, + pub dsb_rating: Option<u32>, pub links: Option<String>, } @@ -131,6 +140,19 @@ pub struct Stream { pub lang: String, } +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UserStreamer { + pub twitch: Option<StreamerChannel>, + #[serde(rename = "youTube")] + pub youtube: Option<StreamerChannel>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct StreamerChannel { + pub channel: String, +} + #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Streamer { @@ -169,11 +191,11 @@ pub struct Crosstable { #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Note { - from: LightUser, - to: LightUser, - text: String, - date: u64, +pub struct UserNote { + pub from: LightUser, + pub to: LightUser, + pub text: String, + pub date: u64, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -241,22 +263,41 @@ pub struct Player { pub online: Option<bool>, } +pub type PerfTop10 = Vec<TopUser>; + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Leaderboards { - pub bullet: Vec<Player>, - pub blitz: Vec<Player>, - pub rapid: Vec<Player>, - pub classical: Vec<Player>, - pub ultra_bullet: Vec<Player>, - pub chess960: Vec<Player>, - pub crazyhouse: Vec<Player>, - pub antichess: Vec<Player>, - pub atomic: Vec<Player>, - pub horde: Vec<Player>, - pub king_of_the_hill: Vec<Player>, - pub racing_kings: Vec<Player>, - pub three_check: Vec<Player>, +pub struct Top10s { + pub bullet: PerfTop10, + pub blitz: PerfTop10, + pub rapid: PerfTop10, + pub classical: PerfTop10, + pub ultra_bullet: PerfTop10, + pub chess960: PerfTop10, + pub crazyhouse: PerfTop10, + pub antichess: PerfTop10, + pub atomic: PerfTop10, + pub horde: PerfTop10, + pub king_of_the_hill: PerfTop10, + pub racing_kings: PerfTop10, + pub three_check: PerfTop10, +} + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TopUser { + pub id: String, + pub username: String, + pub perfs: std::collections::HashMap<String, TopUserPerf>, + pub title: Option<Title>, + pub patron: Option<bool>, + pub online: Option<bool>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TopUserPerf { + pub rating: u32, + pub progress: i32, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/src/model/users/note.rs b/lib/src/model/users/note.rs similarity index 100% rename from src/model/users/note.rs rename to lib/src/model/users/note.rs diff --git a/src/model/users/performance.rs b/lib/src/model/users/performance.rs similarity index 92% rename from src/model/users/performance.rs rename to lib/src/model/users/performance.rs index 4a31338..bfdd936 100644 --- a/src/model/users/performance.rs +++ b/lib/src/model/users/performance.rs @@ -1,6 +1,12 @@ -use crate::model::{puzzles, PerfType, Request, Title}; +use crate::model::{PerfType, Request, Title}; use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PerfTypeData { + pub key: String, + pub name: String, +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct User { pub name: String, @@ -114,7 +120,7 @@ pub struct PlayStreak { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Stat { - pub perf_type: puzzles::Perf, + pub perf_type: PerfTypeData, pub id: String, pub highest: RatingExtreme, pub lowest: RatingExtreme, @@ -128,11 +134,11 @@ pub struct Stat { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct Performance { +pub struct PerfStat { pub user: User, pub perf: Perf, pub rank: Option<u32>, - pub percentile: f64, + pub percentile: Option<f64>, pub stat: Stat, } diff --git a/src/model/users/public.rs b/lib/src/model/users/public.rs similarity index 100% rename from src/model/users/public.rs rename to lib/src/model/users/public.rs diff --git a/src/model/users/rating_history.rs b/lib/src/model/users/rating_history.rs similarity index 100% rename from src/model/users/rating_history.rs rename to lib/src/model/users/rating_history.rs index a9cfd52..1416048 100644 --- a/src/model/users/rating_history.rs +++ b/lib/src/model/users/rating_history.rs @@ -1,6 +1,6 @@ use serde::{ - ser::{SerializeSeq, SerializeTuple}, Deserialize, Deserializer, Serialize, Serializer, + ser::{SerializeSeq, SerializeTuple}, }; #[derive(Default, Clone, Debug, Serialize)] diff --git a/src/model/users/status.rs b/lib/src/model/users/status.rs similarity index 96% rename from src/model/users/status.rs rename to lib/src/model/users/status.rs index 3193277..b42e7bb 100644 --- a/src/model/users/status.rs +++ b/lib/src/model/users/status.rs @@ -28,6 +28,7 @@ impl GetRequest { pub struct User { pub id: String, pub name: String, + pub flair: Option<String>, pub title: Option<Title>, pub online: Option<bool>, pub playing: Option<bool>, diff --git a/src/model/users/top_10.rs b/lib/src/model/users/top_10.rs similarity index 100% rename from src/model/users/top_10.rs rename to lib/src/model/users/top_10.rs diff --git a/tests/data/response/challenge.json b/lib/tests/data/response/challenge.json similarity index 96% rename from tests/data/response/challenge.json rename to lib/tests/data/response/challenge.json index 6c9fb56..17e0aae 100644 --- a/tests/data/response/challenge.json +++ b/lib/tests/data/response/challenge.json @@ -38,7 +38,8 @@ "perf": { "icon": "#", "name": "Rapid" - } + }, + "finalColor": "white" }, "compat": { "bot": false, diff --git a/tests/data/response/challenge_ai.json b/lib/tests/data/response/challenge_ai.json similarity index 100% rename from tests/data/response/challenge_ai.json rename to lib/tests/data/response/challenge_ai.json diff --git a/tests/data/response/challenge_anonymous.json b/lib/tests/data/response/challenge_anonymous.json similarity index 100% rename from tests/data/response/challenge_anonymous.json rename to lib/tests/data/response/challenge_anonymous.json diff --git a/tests/data/response/challenge_canceled.json b/lib/tests/data/response/challenge_canceled.json similarity index 100% rename from tests/data/response/challenge_canceled.json rename to lib/tests/data/response/challenge_canceled.json diff --git a/lib/tests/data/response/challenge_canceled_event.json b/lib/tests/data/response/challenge_canceled_event.json new file mode 100644 index 0000000..b98bd31 --- /dev/null +++ b/lib/tests/data/response/challenge_canceled_event.json @@ -0,0 +1,46 @@ +{ + "type": "challengeCanceled", + "challenge": { + "id": "H9fIRZUk", + "url": "https://lichess.org/H9fIRZUk", + "status": "canceled", + "challenger": { + "id": "bot1", + "name": "Bot1", + "rating": 1500, + "title": "BOT", + "provisional": true, + "online": true, + "lag": 4 + }, + "destUser": { + "id": "bobby", + "name": "Bobby", + "rating": 1635, + "title": "GM", + "provisional": true, + "online": true, + "lag": 4 + }, + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "rated": true, + "speed": "rapid", + "timeControl": { + "type": "clock", + "limit": 600, + "increment": 0, + "show": "10+0" + }, + "color": "random", + "finalColor": "black", + "perf": { + "icon": "", + "name": "Rapid" + }, + "direction": "out" + } +} \ No newline at end of file diff --git a/tests/data/response/challenge_declined.json b/lib/tests/data/response/challenge_declined.json similarity index 86% rename from tests/data/response/challenge_declined.json rename to lib/tests/data/response/challenge_declined.json index 60a8989..e516952 100644 --- a/tests/data/response/challenge_declined.json +++ b/lib/tests/data/response/challenge_declined.json @@ -3,7 +3,7 @@ "challenge": { "id": "7pGLxJ4F", "url": "https://lichess.org/VU0nyvsW", - "status": "created", + "status": "declined", "challenger": { "id": "lovlas", "name": "Lovlas", @@ -38,6 +38,8 @@ "perf": { "icon": "#", "name": "Rapid" - } + }, + "declineReason": "I'm not interested in playing right now", + "declineReasonKey": "generic" } } diff --git a/lib/tests/data/response/challenge_declined_event.json b/lib/tests/data/response/challenge_declined_event.json new file mode 100644 index 0000000..c7dbf29 --- /dev/null +++ b/lib/tests/data/response/challenge_declined_event.json @@ -0,0 +1,48 @@ +{ + "type": "challengeDeclined", + "challenge": { + "id": "H9fIRZUk", + "url": "https://lichess.org/H9fIRZUk", + "status": "declined", + "challenger": { + "id": "bot1", + "name": "Bot1", + "rating": 1500, + "title": "BOT", + "provisional": true, + "online": true, + "lag": 4 + }, + "destUser": { + "id": "bobby", + "name": "Bobby", + "rating": 1635, + "title": "GM", + "provisional": true, + "online": true, + "lag": 4 + }, + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "rated": true, + "speed": "rapid", + "timeControl": { + "type": "clock", + "limit": 600, + "increment": 0, + "show": "10+0" + }, + "color": "random", + "finalColor": "black", + "perf": { + "icon": "", + "name": "Rapid" + }, + "direction": "out", + "declineReason": "I'm not accepting challenges at the moment", + "declineReasonKey": "generic" + } +} \ No newline at end of file diff --git a/lib/tests/data/response/challenge_declined_json.json b/lib/tests/data/response/challenge_declined_json.json new file mode 100644 index 0000000..8dbf39f --- /dev/null +++ b/lib/tests/data/response/challenge_declined_json.json @@ -0,0 +1,45 @@ +{ + "id": "H9fIRZUk", + "url": "https://lichess.org/H9fIRZUk", + "status": "declined", + "challenger": { + "id": "bot1", + "name": "Bot1", + "rating": 1500, + "title": "BOT", + "provisional": true, + "online": true, + "lag": 4 + }, + "destUser": { + "id": "bobby", + "name": "Bobby", + "rating": 1635, + "title": "GM", + "provisional": true, + "online": true, + "lag": 4 + }, + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "rated": true, + "speed": "rapid", + "timeControl": { + "type": "clock", + "limit": 600, + "increment": 0, + "show": "10+0" + }, + "color": "random", + "finalColor": "black", + "perf": { + "icon": "", + "name": "Rapid" + }, + "direction": "out", + "declineReason": "I'm not accepting challenges at the moment", + "declineReasonKey": "generic" +} \ No newline at end of file diff --git a/lib/tests/data/response/challenge_event.json b/lib/tests/data/response/challenge_event.json new file mode 100644 index 0000000..65b9473 --- /dev/null +++ b/lib/tests/data/response/challenge_event.json @@ -0,0 +1,50 @@ +{ + "type": "challenge", + "challenge": { + "id": "H9fIRZUk", + "url": "https://lichess.org/H9fIRZUk", + "status": "created", + "challenger": { + "id": "bot1", + "name": "Bot1", + "rating": 1500, + "title": "BOT", + "provisional": true, + "online": true, + "lag": 4 + }, + "destUser": { + "id": "bobby", + "name": "Bobby", + "rating": 1635, + "title": "GM", + "provisional": true, + "online": true, + "lag": 4 + }, + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "rated": true, + "speed": "rapid", + "timeControl": { + "type": "clock", + "limit": 600, + "increment": 0, + "show": "10+0" + }, + "color": "random", + "finalColor": "black", + "perf": { + "icon": "", + "name": "Rapid" + }, + "direction": "in" + }, + "compat": { + "bot": false, + "board": true + } +} \ No newline at end of file diff --git a/lib/tests/data/response/challenge_json.json b/lib/tests/data/response/challenge_json.json new file mode 100644 index 0000000..d3eb91a --- /dev/null +++ b/lib/tests/data/response/challenge_json.json @@ -0,0 +1,43 @@ +{ + "id": "H9fIRZUk", + "url": "https://lichess.org/H9fIRZUk", + "status": "created", + "challenger": { + "id": "bot1", + "name": "Bot1", + "rating": 1500, + "title": "BOT", + "provisional": true, + "online": true, + "lag": 4 + }, + "destUser": { + "id": "bobby", + "name": "Bobby", + "rating": 1635, + "title": "GM", + "provisional": true, + "online": true, + "lag": 4 + }, + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "rated": true, + "speed": "rapid", + "timeControl": { + "type": "clock", + "limit": 600, + "increment": 0, + "show": "10+0" + }, + "color": "random", + "finalColor": "black", + "perf": { + "icon": "", + "name": "Rapid" + }, + "direction": "out" +} \ No newline at end of file diff --git a/lib/tests/data/response/challenge_open_json.json b/lib/tests/data/response/challenge_open_json.json new file mode 100644 index 0000000..549a596 --- /dev/null +++ b/lib/tests/data/response/challenge_open_json.json @@ -0,0 +1,26 @@ +{ + "id": "XaP00j5r", + "url": "https://lichess.org/XaP00j5r", + "status": "created", + "challenger": null, + "destUser": null, + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "rated": false, + "speed": "correspondence", + "timeControl": { + "type": "unlimited" + }, + "color": "random", + "finalColor": "black", + "perf": { + "icon": "", + "name": "Correspondence" + }, + "open": {}, + "urlWhite": "https://lichess.org/XaP00j5r?color=white", + "urlBlack": "https://lichess.org/XaP00j5r?color=black" +} \ No newline at end of file diff --git a/tests/data/response/chat_line.json b/lib/tests/data/response/chat_line.json similarity index 100% rename from tests/data/response/chat_line.json rename to lib/tests/data/response/chat_line.json diff --git a/tests/data/response/chat_line_spectator.json b/lib/tests/data/response/chat_line_spectator.json similarity index 100% rename from tests/data/response/chat_line_spectator.json rename to lib/tests/data/response/chat_line_spectator.json diff --git a/tests/data/response/current_simuls.json b/lib/tests/data/response/current_simuls.json similarity index 100% rename from tests/data/response/current_simuls.json rename to lib/tests/data/response/current_simuls.json diff --git a/tests/data/response/empty.json b/lib/tests/data/response/empty.json similarity index 100% rename from tests/data/response/empty.json rename to lib/tests/data/response/empty.json diff --git a/tests/data/response/error.json b/lib/tests/data/response/error.json similarity index 100% rename from tests/data/response/error.json rename to lib/tests/data/response/error.json diff --git a/tests/data/response/game_finish.json b/lib/tests/data/response/game_finish.json similarity index 100% rename from tests/data/response/game_finish.json rename to lib/tests/data/response/game_finish.json diff --git a/tests/data/response/game_full_ai.json b/lib/tests/data/response/game_full_ai.json similarity index 100% rename from tests/data/response/game_full_ai.json rename to lib/tests/data/response/game_full_ai.json diff --git a/tests/data/response/game_full_anonymous.json b/lib/tests/data/response/game_full_anonymous.json similarity index 100% rename from tests/data/response/game_full_anonymous.json rename to lib/tests/data/response/game_full_anonymous.json diff --git a/tests/data/response/game_full_human.json b/lib/tests/data/response/game_full_human.json similarity index 100% rename from tests/data/response/game_full_human.json rename to lib/tests/data/response/game_full_human.json diff --git a/tests/data/response/game_json.json b/lib/tests/data/response/game_json.json similarity index 100% rename from tests/data/response/game_json.json rename to lib/tests/data/response/game_json.json diff --git a/tests/data/response/game_start.json b/lib/tests/data/response/game_start.json similarity index 100% rename from tests/data/response/game_start.json rename to lib/tests/data/response/game_start.json diff --git a/tests/data/response/game_state.json b/lib/tests/data/response/game_state.json similarity index 100% rename from tests/data/response/game_state.json rename to lib/tests/data/response/game_state.json diff --git a/tests/data/response/game_state_resign.json b/lib/tests/data/response/game_state_resign.json similarity index 100% rename from tests/data/response/game_state_resign.json rename to lib/tests/data/response/game_state_resign.json diff --git a/lib/tests/data/response/live_streamers.json b/lib/tests/data/response/live_streamers.json new file mode 100644 index 0000000..7a5d15d --- /dev/null +++ b/lib/tests/data/response/live_streamers.json @@ -0,0 +1,39 @@ +[ + { + "name": "PokochajSzachy", + "title": "CM", + "flair": "symbols.orange-heart", + "patron": true, + "id": "pokochajszachy", + "stream": { + "service": "twitch", + "status": "CHess & Chill & Good Music !youtube !instagram lichess.org [EN] [PL]", + "lang": "pl" + }, + "streamer": { + "name": "Pokochaj Szachy", + "headline": "Streaming in Polish and English. Come by and say hello!", + "description": "Hi! Have a good time watching the stream! I very often play against viewers on DGT chessboard.", + "twitch": "https://www.twitch.tv/pokochajszachy", + "image": "https://image.lichess1.org/display?fmt=png&h=350&op=thumbnail&path=pokochajszachy:streamer:pokochajszachy:qfVf3nXP.png&w=350&sig=e329d57f8e6815da63dba66992fa8b3c4b100ba9" + } + }, + { + "name": "XadrezTotalTV", + "flair": "objects.telescope", + "id": "xadreztotaltv", + "stream": { + "service": "youTube", + "status": "Xadrez Total | Analisando partidas e batendo papo Lichess.org", + "lang": "en-US" + }, + "streamer": { + "name": "XadrezTotal", + "headline": "Empresa de OrganizaƧao de torenios FIDE", + "description": "Xadrez Total Ʃ uma empresa que organiza torneios FIDE", + "twitch": "https://www.twitch.tv/xadreztotaltv", + "youTube": "https://www.youtube.com/channel/UCl0pW-vG9r8AT8N2bJdMu8Q/live", + "image": "https://image.lichess1.org/display?fmt=webp&h=350&op=thumbnail&path=xadreztotaltv:streamer:xadreztotaltv:4DueCXhP.jpg&w=350&sig=e5fb7e683cb3ec7258778f727decd50b0495417a" + } + } +] \ No newline at end of file diff --git a/tests/data/response/not_found.json b/lib/tests/data/response/not_found.json similarity index 100% rename from tests/data/response/not_found.json rename to lib/tests/data/response/not_found.json diff --git a/tests/data/response/notes.json b/lib/tests/data/response/notes.json similarity index 100% rename from tests/data/response/notes.json rename to lib/tests/data/response/notes.json diff --git a/tests/data/response/ok.json b/lib/tests/data/response/ok.json similarity index 100% rename from tests/data/response/ok.json rename to lib/tests/data/response/ok.json diff --git a/tests/data/response/opponent_gone_false.json b/lib/tests/data/response/opponent_gone_false.json similarity index 100% rename from tests/data/response/opponent_gone_false.json rename to lib/tests/data/response/opponent_gone_false.json diff --git a/tests/data/response/opponent_gone_true.json b/lib/tests/data/response/opponent_gone_true.json similarity index 100% rename from tests/data/response/opponent_gone_true.json rename to lib/tests/data/response/opponent_gone_true.json diff --git a/tests/data/response/players.json b/lib/tests/data/response/players.json similarity index 100% rename from tests/data/response/players.json rename to lib/tests/data/response/players.json diff --git a/tests/data/response/public_user_data.json b/lib/tests/data/response/public_user_data.json similarity index 100% rename from tests/data/response/public_user_data.json rename to lib/tests/data/response/public_user_data.json diff --git a/tests/data/response/puzzle_round.json b/lib/tests/data/response/puzzle_activity.json similarity index 100% rename from tests/data/response/puzzle_round.json rename to lib/tests/data/response/puzzle_activity.json diff --git a/lib/tests/data/response/puzzle_and_game.json b/lib/tests/data/response/puzzle_and_game.json new file mode 100644 index 0000000..070ae0c --- /dev/null +++ b/lib/tests/data/response/puzzle_and_game.json @@ -0,0 +1,47 @@ +{ + "game": { + "clock": "5+0", + "id": "MLDVu1s1", + "perf": { + "key": "blitz", + "name": "Blitz" + }, + "pgn": "c4 Nf6 Nf3 g6 Nc3 Bg7 g3 d5 Bg2 dxc4 Qa4+ Nc6 Qxc4 O-O O-O a6 d4 e6 Bf4 Nd5 Ne5 Nxe5 Bxe5 c6 Bxg7 Kxg7 e4 Nxc3 bxc3 Qe7 e5 Bd7 Rab1 Rab8 Rb6 c5 Rxb7", + "players": [ + { + "color": "white", + "id": "sanasesino", + "name": "sanasesino", + "rating": 1969 + }, + { + "color": "black", + "id": "tolichach", + "name": "tolichach", + "rating": 2226, + "title": "FM" + } + ], + "rated": true + }, + "puzzle": { + "id": "i1Al4", + "initialPly": 36, + "plays": 83787, + "rating": 1873, + "solution": [ + "b8b7", + "g2b7", + "d7b5", + "c4c5", + "e7b7" + ], + "themes": [ + "advantage", + "long", + "discoveredAttack", + "master", + "middlegame" + ] + } +} \ No newline at end of file diff --git a/tests/data/response/puzzle_dashboard.json b/lib/tests/data/response/puzzle_dashboard.json similarity index 100% rename from tests/data/response/puzzle_dashboard.json rename to lib/tests/data/response/puzzle_dashboard.json diff --git a/tests/data/response/puzzle_race.json b/lib/tests/data/response/puzzle_racer.json similarity index 100% rename from tests/data/response/puzzle_race.json rename to lib/tests/data/response/puzzle_racer.json diff --git a/lib/tests/data/response/puzzle_replay.json b/lib/tests/data/response/puzzle_replay.json new file mode 100644 index 0000000..8acd584 --- /dev/null +++ b/lib/tests/data/response/puzzle_replay.json @@ -0,0 +1,20 @@ +{ + "replay": { + "days": 90, + "theme": "mix", + "nb": 6, + "remaining": [ + "0e7Q3", + "1EFXE", + "C3VR4", + "0UIdY", + "E27v9", + "3rPs6" + ] + }, + "angle": { + "key": "mix", + "name": "Puzzle Themes", + "desc": "A mix of everything. You don't know what to expect, so you remain ready for anything! Just like in real games." + } +} \ No newline at end of file diff --git a/tests/data/response/rating-history.json b/lib/tests/data/response/rating_history.json similarity index 100% rename from tests/data/response/rating-history.json rename to lib/tests/data/response/rating_history.json diff --git a/tests/data/response/storm_dashboard.json b/lib/tests/data/response/storm_dashboard.json similarity index 100% rename from tests/data/response/storm_dashboard.json rename to lib/tests/data/response/storm_dashboard.json diff --git a/tests/data/response/streamers.json b/lib/tests/data/response/streamers.json similarity index 100% rename from tests/data/response/streamers.json rename to lib/tests/data/response/streamers.json diff --git a/tests/data/response/tv_channels.json b/lib/tests/data/response/tv_channels.json similarity index 100% rename from tests/data/response/tv_channels.json rename to lib/tests/data/response/tv_channels.json diff --git a/tests/data/response/tv_stream_featured.json b/lib/tests/data/response/tv_stream_featured.json similarity index 100% rename from tests/data/response/tv_stream_featured.json rename to lib/tests/data/response/tv_stream_featured.json diff --git a/tests/data/response/tv_stream_featured_untitled.json b/lib/tests/data/response/tv_stream_featured_untitled.json similarity index 100% rename from tests/data/response/tv_stream_featured_untitled.json rename to lib/tests/data/response/tv_stream_featured_untitled.json diff --git a/tests/data/response/tv_stream_fen.json b/lib/tests/data/response/tv_stream_fen.json similarity index 100% rename from tests/data/response/tv_stream_fen.json rename to lib/tests/data/response/tv_stream_fen.json diff --git a/tests/data/response/activities.json b/lib/tests/data/response/user_activities.json similarity index 100% rename from tests/data/response/activities.json rename to lib/tests/data/response/user_activities.json diff --git a/lib/tests/data/response/user_activity.json b/lib/tests/data/response/user_activity.json new file mode 100644 index 0000000..b711bd1 --- /dev/null +++ b/lib/tests/data/response/user_activity.json @@ -0,0 +1,53 @@ +[ + { + "interval": { + "start": 1745020800000, + "end": 1745107200000 + }, + "games": { + "blitz": { + "win": 1, + "loss": 0, + "draw": 0, + "rp": { + "before": 1712, + "after": 1717 + } + } + }, + "follows": { + "in": { + "ids": [ + "aadhya5", + "mrbernhardknarkson", + "biggiantsandwich" + ] + } + } + }, + { + "interval": { + "start": 1744934400000, + "end": 1745020800000 + }, + "puzzles": { + "score": { + "win": 0, + "loss": 1, + "draw": 0, + "rp": { + "before": 1911, + "after": 1911 + } + } + }, + "follows": { + "in": { + "ids": [ + "midnight_marauder", + "aryanraj2014" + ] + } + } + } +] \ No newline at end of file diff --git a/lib/tests/data/response/user_autocompletions.json b/lib/tests/data/response/user_autocompletions.json new file mode 100644 index 0000000..a7c7059 --- /dev/null +++ b/lib/tests/data/response/user_autocompletions.json @@ -0,0 +1,8 @@ +{ + "result": [ + { + "name": "Bobby", + "id": "bobby" + } + ] +} \ No newline at end of file diff --git a/lib/tests/data/response/user_extended.json b/lib/tests/data/response/user_extended.json new file mode 100644 index 0000000..6a6d607 --- /dev/null +++ b/lib/tests/data/response/user_extended.json @@ -0,0 +1,127 @@ +{ + "id": "mary", + "username": "Mary", + "perfs": { + "bullet": { + "games": 119, + "rating": 1066, + "rd": 101, + "prog": -1 + }, + "blitz": { + "games": 34, + "rating": 1007, + "rd": 55, + "prog": -59 + }, + "rapid": { + "games": 134, + "rating": 1021, + "rd": 70, + "prog": 26 + }, + "classical": { + "games": 451, + "rating": 1136, + "rd": 78, + "prog": -1 + }, + "correspondence": { + "games": 35, + "rating": 1049, + "rd": 45, + "prog": 15 + }, + "chess960": { + "games": 52, + "rating": 996, + "rd": 72, + "prog": -9 + }, + "kingOfTheHill": { + "games": 1998, + "rating": 1169, + "rd": 79, + "prog": -13 + }, + "threeCheck": { + "games": 6, + "rating": 946, + "rd": 52, + "prog": 32 + }, + "antichess": { + "games": 79, + "rating": 1143, + "rd": 48, + "prog": 61 + }, + "atomic": { + "games": 239, + "rating": 978, + "rd": 76, + "prog": 22 + }, + "horde": { + "games": 246, + "rating": 1031, + "rd": 108, + "prog": -28 + }, + "crazyhouse": { + "games": 473, + "rating": 1063, + "rd": 88, + "prog": 2 + }, + "puzzle": { + "games": 37, + "rating": 977, + "rd": 86, + "prog": 26 + } + }, + "flair": "food-drink.coconut", + "createdAt": 1744526339498, + "profile": { + "location": "Mary City", + "cfcRating": 1173, + "rcfRating": 897, + "flag": "MF", + "dsbRating": 1110, + "fideRating": 1050, + "ecfRating": 1249, + "uscfRating": 1217, + "bio": "The elementary form of value of a commodity is contained in the equation, expressing its value relation to another commodity of a different kind, or in its exchange relation to the same.", + "links": "https://en.wikipedia.org/wiki/Joni_Mitchell\nhttps://en.wikipedia.org/wiki/Srinivasa_Ramanujan" + }, + "seenAt": 1744677685927, + "playTime": { + "total": 14336, + "tv": 0 + }, + "url": "http://localhost:8080/@/Mary", + "count": { + "all": 5251, + "rated": 4200, + "ai": 0, + "draw": 250, + "drawH": 250, + "loss": 2418, + "lossH": 2418, + "win": 2583, + "winH": 2583, + "bookmark": 0, + "playing": 0, + "import": 0, + "me": 1 + }, + "streamer": { + "twitch": { + "channel": "https://www.twitch.tv/lichessdotorg" + } + }, + "followable": true, + "following": false, + "blocking": false +} \ No newline at end of file diff --git a/tests/data/response/performance.json b/lib/tests/data/response/user_performance.json similarity index 100% rename from tests/data/response/performance.json rename to lib/tests/data/response/user_performance.json diff --git a/lib/tests/data/response/user_statuses.json b/lib/tests/data/response/user_statuses.json new file mode 100644 index 0000000..fe8f0d5 --- /dev/null +++ b/lib/tests/data/response/user_statuses.json @@ -0,0 +1,13 @@ +[ + { + "name": "Mary", + "flair": "food-drink.coconut", + "id": "mary" + }, + { + "name": "Ana", + "title": "GM", + "flair": "symbols.japanese-no-vacancy-button", + "id": "ana" + } +] \ No newline at end of file diff --git a/tests/offline.rs b/lib/tests/offline.rs similarity index 58% rename from tests/offline.rs rename to lib/tests/offline.rs index ec99a3d..8b8d2a1 100644 --- a/tests/offline.rs +++ b/lib/tests/offline.rs @@ -1,7 +1,7 @@ use lichess_api::model::*; -use serde::de::DeserializeOwned; use serde::Serialize; +use serde::de::DeserializeOwned; use std::fs; @@ -54,28 +54,33 @@ pub fn games_export() { } #[test] -pub fn puzzle() { - test_response_model::<puzzles::PuzzleAndGame>("puzzle"); +pub fn puzzle_and_game() { + test_response_model::<puzzles::PuzzleAndGame>("puzzle_and_game"); } #[test] -pub fn puzzle_round() { - test_response_model::<puzzles::activity::PuzzleRoundJson>("puzzle_round"); +pub fn puzzle_activity() { + test_response_model::<puzzles::activity::PuzzleActivity>("puzzle_activity"); } #[test] pub fn puzzle_race() { - test_response_model::<puzzles::race::PuzzleRaceJson>("puzzle_race"); + test_response_model::<puzzles::race::PuzzleRacer>("puzzle_racer"); } #[test] pub fn puzzle_dashboard() { - test_response_model::<puzzles::dashboard::PuzzleDashboardJson>("puzzle_dashboard"); + test_response_model::<puzzles::dashboard::PuzzleDashboard>("puzzle_dashboard"); +} + +#[test] +pub fn puzzle_replay() { + test_response_model::<puzzles::replay::Replay>("puzzle_replay"); } #[test] pub fn storm_dashboard() { - test_response_model::<puzzles::storm_dashboard::StormDashboardJson>("storm_dashboard"); + test_response_model::<puzzles::storm_dashboard::StormDashboard>("storm_dashboard"); } #[test] @@ -93,13 +98,28 @@ pub fn tv() { #[test] pub fn users() { - test_response_model::<users::Leaderboards>("players"); - test_response_model::<Vec<users::rating_history::RatingEntry>>("rating-history"); - test_response_model::<users::rating_history::RatingHistory>("rating-history"); - test_response_model::<users::performance::Performance>("performance"); - test_response_model::<Vec<users::activity::Activity>>("activities"); + test_response_model::<users::Top10s>("players"); + test_response_model::<Vec<users::rating_history::RatingEntry>>("rating_history"); + test_response_model::<users::rating_history::RatingHistory>("rating_history"); + test_response_model::<users::performance::PerfStat>("user_performance"); test_response_model::<Vec<users::StreamingUser>>("streamers"); - test_response_model::<Vec<users::Note>>("notes"); + test_response_model::<Vec<users::UserNote>>("notes"); + test_response_model::<Vec<users::activity::UserActivity>>("user_activity"); + test_response_model::<Vec<users::activity::UserActivity>>("user_activities"); + test_response_model::<users::UserExtended>("user_extended"); + test_response_model::<Vec<users::status::User>>("user_statuses"); + test_response_model::<Vec<users::StreamingUser>>("live_streamers"); + test_response_model::<users::autocomplete::Autocompletions>("user_autocompletions"); +} + +#[test] +pub fn challenges() { + test_response_model::<challenges::ChallengeJson>("challenge_json"); + test_response_model::<challenges::ChallengeOpenJson>("challenge_open_json"); + test_response_model::<challenges::ChallengeDeclinedJson>("challenge_declined_json"); + test_response_model::<challenges::ChallengeEvent>("challenge_event"); + test_response_model::<challenges::ChallengeCanceledEvent>("challenge_canceled_event"); + test_response_model::<challenges::ChallengeDeclinedEvent>("challenge_declined_event"); } fn test_response_model<Model: Serialize + DeserializeOwned>(file_name: &str) { @@ -108,13 +128,13 @@ fn test_response_model<Model: Serialize + DeserializeOwned>(file_name: &str) { } fn test_model<Model: Serialize + DeserializeOwned>(path: String) { - let model_string = fs::read_to_string(path).expect("Unable to read file."); - let model_json: serde_json::Value = - serde_json::from_str(&model_string).expect("Unable to serialize model into json value."); - let model: Model = - serde_json::from_str(&model_string).expect("Unable to deserialize json string to model."); + let model_string = fs::read_to_string(&path).expect("Unable to read file."); + let model_json: serde_json::Value = serde_json::from_str(&model_string) + .expect("Unable to deserialize model string into json value."); + let model: Model = serde_json::from_str(&model_string) + .expect("Unable to deserialize model string into model."); let reserialized_model_json: serde_json::Value = - serde_json::to_value(&model).expect("Unable to serialize model to json value."); + serde_json::to_value(&model).expect("Unable to serialize model into json value."); assert_eq!(model_json, reserialized_model_json); } diff --git a/tests/online.rs b/lib/tests/online.rs similarity index 98% rename from tests/online.rs rename to lib/tests/online.rs index 31b2de1..a61d7a3 100644 --- a/tests/online.rs +++ b/lib/tests/online.rs @@ -1,4 +1,3 @@ -use futures::StreamExt; use lichess_api::client::*; use reqwest; use tokio; diff --git a/tests/tests.rs b/lib/tests/tests.rs similarity index 100% rename from tests/tests.rs rename to lib/tests/tests.rs diff --git a/src/model/users/activity.rs b/src/model/users/activity.rs deleted file mode 100644 index f25422c..0000000 --- a/src/model/users/activity.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::model::{Color, PerfType, Request}; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, ops::Range}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Interval { - pub from: u64, - pub to: u64, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct RatingProgress { - pub before: u32, - pub after: u32, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Score { - pub win: u32, - pub loss: u32, - pub draw: u32, - - #[serde(rename = "rp")] - pub rating_progress: RatingProgress, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Puzzles { - pub score: Score, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Followers { - pub ids: Vec<String>, - - #[serde(rename = "nb", skip_serializing_if = "Option::is_none")] - pub count: Option<u32>, -} - -#[serde_with::skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Follows { - #[serde(rename = "in")] - pub gained: Option<Followers>, - - #[serde(rename = "out")] - pub lost: Option<Followers>, -} - -#[serde_with::skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CorrespondenceOpponent { - pub user: Option<String>, - pub rating: Option<u32>, -} - -#[serde_with::skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CorrespondenceGame { - pub id: String, - pub color: Color, - pub url: String, - pub opponent: CorrespondenceOpponent, - pub variant: Option<String>, - pub speed: Option<String>, - pub perf: Option<String>, - pub rated: Option<bool>, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CorrespondenceMoves { - #[serde(rename = "nb")] - pub move_count: u32, - pub games: Vec<CorrespondenceGame>, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CorrespondenceEnds { - pub score: Score, - pub games: Vec<CorrespondenceGame>, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct TournamentId { - pub id: String, - pub name: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Tournament { - pub tournament: TournamentId, - - #[serde(rename = "nbGames")] - pub game_count: u32, - pub score: u32, - pub rank: u32, - pub rank_percent: u32, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Tournaments { - #[serde(rename = "nb")] - pub count: u32, - pub best: Vec<Tournament>, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Post { - pub url: String, - pub text: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Topic { - pub topic_url: String, - pub topic_name: String, - pub posts: Vec<Post>, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Practice { - pub url: String, - pub name: String, - - #[serde(rename = "nbPositions")] - pub positions_count: u32, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Team { - pub url: String, - pub name: String, -} - -#[serde_with::skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Activity { - pub interval: Range<u64>, - pub games: Option<HashMap<PerfType, Score>>, - pub puzzles: Option<Puzzles>, - pub follows: Option<Follows>, - pub correspondence_moves: Option<CorrespondenceMoves>, - pub correspondence_ends: Option<CorrespondenceEnds>, - pub tournaments: Option<Tournaments>, - pub stream: Option<bool>, - pub posts: Option<Vec<Topic>>, - pub practice: Option<Vec<Practice>>, - pub teams: Option<Vec<Team>>, -} - -#[derive(Default, Clone, Debug, Serialize)] -pub struct GetQuery; - -pub type GetRequest = Request<GetQuery>; - -impl GetRequest { - pub fn new(username: &str, perf: PerfType) -> Self { - Self::get(format!("/api/user/{username}/perf/{perf}"), None, None) - } -} diff --git a/tests/data/response/puzzle.json b/tests/data/response/puzzle.json deleted file mode 100644 index 1135c09..0000000 --- a/tests/data/response/puzzle.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "game": { - "clock": "10+0", - "id": "VpVdGbna", - "perf": { - "key": "rapid", - "name": "Rapid" - }, - "pgn": "d4 Nf6 Nf3 g6 Nc3 d6 e4 c5 Be3 cxd4 Bxd4 Nc6 Be3 Qa5 Bd2 Bg7 Be2 O-O O-O Qb6 Rb1 Bg4 h3 Bxf3 Bxf3 Nd4 Be3 Nxf3+ Qxf3 Qc6 Bd4 a6 Bxf6 Bxf6 Nd5 Qxc2 Nxf6+ exf6 Qxf6 Qxe4 Qxd6 Rad8 Qb6 Rfe8 Rfe1 Qxe1+ Rxe1 Rxe1+ Kh2 Rd2 Kg3 Ree2 Qxb7 Rxb2 Qxa6 Rxa2 Qc8+ Kg7 Qc3+ Kg8 Qc5 Rxf2 Qc8+ Kg7 Qc3+ Kh6 Qe3+ Kg7 Qe5+ Kf8 Qh8+ Ke7 Qe5+ Kf8 Qb8+ Kg7 Qe5+ f6 Qe7+ Kh6 Qf8+ Kg5 h4+ Kh5 Qc5+ f5 Qc1 Rxg2+ Kh3 Rh2+ Kg3 Rag2+ Kf3 Rg4 Qd1 Rhxh4 Kf2 Rh2+ Kf3 Rh3+ Ke2 Rg2+ Kf1+ Rg4 Kf2 g5 Qd8 h6 Qe8+ Kh4 Kf1 h5 Qe1+ Rhg3 Qe5 f4 Qe1 f3 Kf2 Rf4 Qh1+ Rh3 Qe1 g4", - "players": [ - { - "color": "white", - "name": "borska (2013)", - "userId": "borska" - }, - { - "color": "black", - "name": "Xxn00bkillar69xX (1990)", - "userId": "xxn00bkillar69xx" - } - ], - "rated": true - }, - "puzzle": { - "id": "K69di", - "initialPly": 123, - "plays": 1970, - "rating": 2022, - "solution": [ - "e1e7", - "f4f6", - "e7f6" - ], - "themes": [ - "short", - "queenRookEndgame", - "endgame", - "mateIn2" - ] - } -}