diff --git a/crates/lavina-core/src/player.rs b/crates/lavina-core/src/player.rs index 9a2207b..87bc0ac 100644 --- a/crates/lavina-core/src/player.rs +++ b/crates/lavina-core/src/player.rs @@ -281,6 +281,19 @@ impl PlayerRegistry { inner.players.get(id).map(|(handle, _)| handle.clone()) } + pub async fn stop_player(&self, id: &PlayerId) -> Result> { + let mut inner = self.0.write().await; + if let Some((handle, fiber)) = inner.players.remove(id) { + handle.send(ActorCommand::Stop).await; + drop(handle); + fiber.await?; + inner.metric_active_players.dec(); + Ok(Some(())) + } else { + Ok(None) + } + } + pub async fn get_or_launch_player(&mut self, id: &PlayerId) -> PlayerHandle { let inner = self.0.read().await; if let Some((handle, _)) = inner.players.get(id) { diff --git a/crates/mgmt-api/src/lib.rs b/crates/mgmt-api/src/lib.rs index cfe5b69..c21ff85 100644 --- a/crates/mgmt-api/src/lib.rs +++ b/crates/mgmt-api/src/lib.rs @@ -11,6 +11,11 @@ pub struct CreatePlayerRequest<'a> { pub name: &'a str, } +#[derive(Serialize, Deserialize)] +pub struct StopPlayerRequest<'a> { + pub name: &'a str, +} + #[derive(Serialize, Deserialize)] pub struct ChangePasswordRequest<'a> { pub player_name: &'a str, @@ -19,6 +24,7 @@ pub struct ChangePasswordRequest<'a> { pub mod paths { pub const CREATE_PLAYER: &'static str = "/mgmt/create_player"; + pub const STOP_PLAYER: &'static str = "/mgmt/stop_player"; pub const SET_PASSWORD: &'static str = "/mgmt/set_password"; } diff --git a/src/http.rs b/src/http.rs index b39a6f2..84dbfdc 100644 --- a/src/http.rs +++ b/src/http.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; use lavina_core::auth::{Authenticator, UpdatePasswordResult}; +use lavina_core::player::{PlayerId, PlayerRegistry}; use lavina_core::prelude::*; use lavina_core::repo::Storage; use lavina_core::room::RoomRegistry; @@ -85,6 +86,7 @@ async fn route( (&Method::GET, "/metrics") => endpoint_metrics(registry), (&Method::GET, "/rooms") => endpoint_rooms(core.rooms).await, (&Method::POST, paths::CREATE_PLAYER) => endpoint_create_player(request, storage).await.or5xx(), + (&Method::POST, paths::STOP_PLAYER) => endpoint_stop_player(request, storage).await.or5xx(), (&Method::POST, paths::SET_PASSWORD) => endpoint_set_password(request, storage).await.or5xx(), _ => not_found(), }; @@ -120,6 +122,39 @@ async fn endpoint_create_player( Ok(response) } +async fn endpoint_stop_player( + request: Request, + players: PlayerRegistry, +) -> Result>> { + let str = request.collect().await?.to_bytes(); + let Ok(res) = serde_json::from_slice::(&str[..]) else { + return Ok(malformed_request()); + }; + let Ok(player_id) = PlayerId::from(res.name) else { + let payload = ErrorResponse { + code: errors::PLAYER_NOT_FOUND, + message: "No such player exists", + } + .to_body(); + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::UNPROCESSABLE_ENTITY; + return Ok(response); + }; + let Some(()) = players.stop_player(&player_id).await? else { + let payload = ErrorResponse { + code: errors::PLAYER_NOT_FOUND, + message: "No such player exists", + } + .to_body(); + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::UNPROCESSABLE_ENTITY; + return Ok(response); + }; + let mut response = Response::new(Full::::default()); + *response.status_mut() = StatusCode::CREATED; + Ok(response) +} + async fn endpoint_set_password( request: Request, storage: Storage, @@ -174,6 +209,7 @@ fn malformed_request() -> Response> { trait Or5xx { fn or5xx(self) -> Response>; } + impl Or5xx for Result>> { fn or5xx(self) -> Response> { self.unwrap_or_else(|e| { @@ -187,6 +223,7 @@ impl Or5xx for Result>> { trait ToBody { fn to_body(&self) -> Full; } + impl ToBody for T where T: Serialize,