Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 279 additions & 0 deletions cli/src/commands/board.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
use clap::Subcommand;
use color_eyre::Result;
use futures::StreamExt;
use lichess_api::client::LichessApi;
use lichess_api::model::board::*;
use lichess_api::model::{Color, Room, VariantKey};
use reqwest;

type Lichess = LichessApi<reqwest::Client>;

#[derive(Debug, Subcommand)]
pub enum BoardCommand {
/// Abort a game
Abort {
/// Game ID
game_id: String,
},
/// Go berserk on an arena tournament game
Berserk {
/// Game ID
game_id: String,
},
/// Stream the messages of a game chat
StreamChat {
/// Game ID
game_id: String,
},
/// Write in the chat of a game
WriteChat {
/// Game ID
game_id: String,
/// Room (player or spectator)
#[arg(long, default_value = "player")]
room: String,
/// Message text
text: String,
},
/// Claim victory when the opponent has left the game for a while
ClaimVictory {
/// Game ID
game_id: String,
},
/// Create/accept/decline draw offers
HandleDraw {
/// Game ID
game_id: String,
/// Accept a draw offer
#[arg(long)]
accept: bool,
},
/// Make a move in a game
MakeMove {
/// Game ID
game_id: String,
/// Move in UCI format (e.g., e2e4)
r#move: String,
/// Whether to offer a draw
#[arg(long)]
offering_draw: bool,
},
/// Resign a game
Resign {
/// Game ID
game_id: String,
},
/// Create a seek to get paired with another player
CreateSeek {
/// Whether the game is rated
#[arg(long)]
rated: bool,
/// Clock time in minutes
#[arg(long)]
time: Option<f32>,
/// Clock increment in seconds
#[arg(long)]
increment: Option<u32>,
/// Days per turn for correspondence games
#[arg(long)]
days: Option<u32>,
/// Chess variant
#[arg(long, default_value = "standard")]
variant: String,
/// Color preference (random, white, black)
#[arg(long, default_value = "random")]
color: String,
/// Rating range minimum
#[arg(long)]
rating_range_min: Option<u32>,
/// Rating range maximum
#[arg(long)]
rating_range_max: Option<u32>,
},
/// Stream incoming events (challenges, game starts)
StreamEvents,
/// Stream the state of a game being played
StreamGame {
/// Game ID
game_id: String,
},
/// Propose/accept/decline takebacks
HandleTakeback {
/// Game ID
game_id: String,
/// Accept a takeback offer
#[arg(long)]
accept: bool,
},
}

impl BoardCommand {
pub async fn run(self, lichess: Lichess) -> Result<()> {
match self {
BoardCommand::Abort { game_id } => {
let request = abort::PostRequest::new(&game_id);
let result = lichess.board_abort_game(request).await?;
println!("Game aborted: {}", result);
Ok(())
}
BoardCommand::Berserk { game_id } => {
let request = berserk::PostRequest::new(&game_id);
let result = lichess.board_berserk_game(request).await?;
println!("Berserk activated: {}", result);
Ok(())
}
BoardCommand::StreamChat { game_id } => {
let request = chat::GetRequest::new(&game_id);
let mut stream = lichess.board_stream_game_chat(request).await?;
println!("Streaming chat messages:");
while let Some(Ok(messages)) = stream.next().await {
for chat_line in messages {
println!("{}: {}", chat_line.user, chat_line.text);
}
}
Ok(())
}
BoardCommand::WriteChat {
game_id,
room,
text,
} => {
let room_enum = match room.as_str() {
"spectator" => Room::Spectator,
_ => Room::Player,
};
let request = chat::PostRequest::new(&game_id, room_enum, &text);
let result = lichess.board_write_in_chat(request).await?;
println!("Message sent: {}", result);
Ok(())
}
BoardCommand::ClaimVictory { game_id } => {
let request = claim_victory::PostRequest::new(&game_id);
let result = lichess.board_claim_victory(request).await?;
println!("Victory claimed: {}", result);
Ok(())
}
BoardCommand::HandleDraw { game_id, accept } => {
let request = draw::PostRequest::new(&game_id, accept);
let result = lichess.board_handle_draw(request).await?;
println!("Draw handled: {}", result);
Ok(())
}
BoardCommand::MakeMove {
game_id,
r#move,
offering_draw,
} => {
let request = r#move::PostRequest::new(&game_id, &r#move, offering_draw);
let result = lichess.board_make_move(request).await?;
println!("Move made: {}", result);
Ok(())
}
BoardCommand::Resign { game_id } => {
let request = resign::PostRequest::new(&game_id);
let result = lichess.board_resign_game(request).await?;
println!("Game resigned: {}", result);
Ok(())
}
BoardCommand::CreateSeek {
rated,
time,
increment,
days,
variant,
color,
rating_range_min,
rating_range_max,
} => {
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,
_ => VariantKey::Standard,
};

let color_choice = match color.as_str() {
"white" => Color::White,
"black" => Color::Black,
_ => Color::Random,
};

let seek_type = if let Some(days) = days {
seek::SeekType::Correspondence { days: days.into() }
} else {
let time_mins = time.unwrap_or(10.0);
let time_secs = (time_mins * 60.0) as u32;
seek::SeekType::RealTime {
time: time_secs,
increment: increment.unwrap_or(0),
}
};

let rating_range = if rating_range_min.is_some() || rating_range_max.is_some() {
format!(
"{}-{}",
rating_range_min.unwrap_or(800),
rating_range_max.unwrap_or(2800)
)
} else {
"".to_string()
};

let query = seek::PostQuery {
seek_type,
rated,
variant: variant_key,
color: color_choice,
rating_range,
};

let request = seek::PostRequest::new(query);
let mut stream = lichess.board_create_a_seek(request).await?;
println!("Creating seek:");
while let Some(event) = stream.next().await {
match event {
Ok(json) => println!("Event: {}", json),
Err(e) => eprintln!("Error: {}", e),
}
}
Ok(())
}
BoardCommand::StreamEvents => {
let request = stream::events::GetRequest::new();
let mut stream = lichess.board_stream_incoming_events(request).await?;
println!("Streaming incoming events:");
while let Some(event) = stream.next().await {
match event {
Ok(event) => println!("Event: {:#?}", event),
Err(e) => eprintln!("Error: {}", e),
}
}
Ok(())
}
BoardCommand::StreamGame { game_id } => {
let request = stream::game::GetRequest::new(&game_id);
let mut stream = lichess.board_stream_board_state(request).await?;
println!("Streaming game state:");
while let Some(event) = stream.next().await {
match event {
Ok(event) => println!("Event: {:#?}", event),
Err(e) => eprintln!("Error: {}", e),
}
}
Ok(())
}
BoardCommand::HandleTakeback { game_id, accept } => {
let request = takeback::PostRequest::new(&game_id, accept);
let result = lichess.board_handle_takeback(request).await?;
println!("Takeback handled: {}", result);
Ok(())
}
}
}
}
2 changes: 2 additions & 0 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
pub mod board;
pub mod challenges;
pub mod external_engine;
pub mod puzzles;
pub mod users;

pub use board::BoardCommand;
pub use challenges::ChallengesCommand;
pub use external_engine::ExternalEngineCommand;
pub use puzzles::PuzzlesCommand;
Expand Down
9 changes: 8 additions & 1 deletion cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use clap::builder::Styles;
use clap::builder::styling::AnsiColor;
use clap::{Parser, Subcommand};
use color_eyre::Result;
use commands::{ChallengesCommand, ExternalEngineCommand, PuzzlesCommand, UsersCommand};
use commands::{
BoardCommand, ChallengesCommand, ExternalEngineCommand, PuzzlesCommand, UsersCommand,
};
use lichess_api::client::LichessApi;
use reqwest;
use tracing::level_filters::LevelFilter;
Expand Down Expand Up @@ -33,6 +35,10 @@ struct Cli {

#[derive(Debug, Subcommand)]
enum Command {
Board {
#[clap(subcommand)]
command: BoardCommand,
},
Puzzles {
#[clap(subcommand)]
command: PuzzlesCommand,
Expand Down Expand Up @@ -94,6 +100,7 @@ impl App {

async fn run(self, args: Cli) -> Result<()> {
match args.command {
Command::Board { command } => command.run(self.lichess).await,
Command::Puzzles { command } => command.run(self.lichess).await,
Command::Engine { command } => command.run(self.lichess).await,
Command::Challenges { command } => command.run(self.lichess).await,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/api/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ impl LichessApi<reqwest::Client> {
pub async fn board_stream_game_chat(
&self,
request: impl Into<chat::GetRequest>,
) -> Result<impl StreamExt<Item = Result<chat::ChatLine>>> {
) -> Result<impl StreamExt<Item = Result<Vec<chat::ChatLine>>>> {
self.get_streamed_models(request.into()).await
}

Expand Down
10 changes: 9 additions & 1 deletion lib/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,15 @@ impl LichessApi<reqwest::Client> {

let convert_err = |e: reqwest::Error| Error::Request(e.to_string());
let request = reqwest::Request::try_from(http_request).map_err(convert_err)?;
debug!(?request, "sending");
let body_text = if let Some(body) = request.body() {
match body.as_bytes() {
Some(bytes) => String::from_utf8_lossy(bytes).to_string(),
None => "<streaming body>".to_string(),
}
} else {
"<empty body>".to_string()
};
debug!(?request, body = %body_text, "sending");
let response = self.client.execute(request).await;
debug!(?response, "received");
let stream = response
Expand Down
Loading