diff --git a/junowen-lib/src/signaling_server.rs b/junowen-lib/src/signaling_server.rs index 842088d..fdfff5f 100644 --- a/junowen-lib/src/signaling_server.rs +++ b/junowen-lib/src/signaling_server.rs @@ -1,2 +1,3 @@ pub mod custom; +pub mod reserved_room; pub mod room; diff --git a/junowen-lib/src/signaling_server/reserved_room.rs b/junowen-lib/src/signaling_server/reserved_room.rs new file mode 100644 index 0000000..3da61ab --- /dev/null +++ b/junowen-lib/src/signaling_server/reserved_room.rs @@ -0,0 +1,148 @@ +use anyhow::bail; +use anyhow::Result; +use derive_new::new; +use getset::Getters; +use http::StatusCode; +use serde::Deserialize; +use serde::Serialize; + +use crate::connection::signaling::CompressedSdp; + +use super::room::PostRoomKeepResponse; +use super::room::PutRoomResponse; + +// PUT /reserved-room/{name} + +#[derive(Debug, Deserialize, Serialize, new)] +pub struct PutReservedRoomResponseConflictBody { + opponent_offer: Option, +} + +impl PutReservedRoomResponseConflictBody { + pub fn into_offer(self) -> Option { + self.opponent_offer + } +} + +pub type PutReservedRoomResponse = PutRoomResponse; + +// GET /reserved-room/{name} + +#[derive(Debug, Deserialize, Serialize, new)] +pub struct GetReservedRoomResponseOkBody { + opponent_offer: Option, + spectator_offer: Option, +} + +impl GetReservedRoomResponseOkBody { + pub fn opponent_offer(&self) -> Option<&CompressedSdp> { + self.opponent_offer.as_ref() + } + + pub fn into_spectator_offer(self) -> Option { + self.spectator_offer + } +} + +pub enum GetReservedRoomResponse { + Ok(GetReservedRoomResponseOkBody), + NotFound, +} + +impl GetReservedRoomResponse { + pub fn parse(status: StatusCode, text: Option<&str>) -> Result { + match (status, text) { + (StatusCode::OK, Some(text)) => { + if let Ok(body) = serde_json::from_str::(text) { + return Ok(Self::Ok(body)); + } + } + (StatusCode::NOT_FOUND, _) => return Ok(Self::NotFound), + _ => {} + } + bail!("invalid response") + } + + pub fn status_code(&self) -> StatusCode { + match self { + Self::Ok(_) => StatusCode::OK, + Self::NotFound => StatusCode::NOT_FOUND, + } + } + + pub fn to_body(&self) -> Option { + match self { + Self::Ok(body) => Some(serde_json::to_string(&body).unwrap()), + Self::NotFound => None, + } + } +} + +// POST /reserved-room/{name}/keep + +#[derive(Deserialize, Serialize, Getters, new)] +pub struct PostReservedRoomKeepRequestBody { + key: String, + spectator_offer: Option, +} + +impl PostReservedRoomKeepRequestBody { + pub fn into_inner(self) -> (String, Option) { + (self.key, self.spectator_offer) + } +} + +#[derive(Debug, Deserialize, Serialize, new)] +pub struct PostReservedRoomKeepResponseOkOpponentAnswerBody { + opponent_answer: CompressedSdp, +} + +impl PostReservedRoomKeepResponseOkOpponentAnswerBody { + pub fn into_opponent_answer(self) -> CompressedSdp { + self.opponent_answer + } +} + +#[derive(Debug, Deserialize, Serialize, new)] +pub struct PostReservedRoomKeepResponseOkSpectatorAnswerBody { + spectator_answer: CompressedSdp, +} + +impl PostReservedRoomKeepResponseOkSpectatorAnswerBody { + pub fn into_spectator_answer(self) -> CompressedSdp { + self.spectator_answer + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum PostReservedRoomKeepResponseOkBody { + OpponentAnswer(PostReservedRoomKeepResponseOkOpponentAnswerBody), + SpectatorAnswer(PostReservedRoomKeepResponseOkSpectatorAnswerBody), +} + +impl From for PostReservedRoomKeepResponseOkBody { + fn from(body: PostReservedRoomKeepResponseOkOpponentAnswerBody) -> Self { + Self::OpponentAnswer(body) + } +} + +impl From + for PostReservedRoomKeepResponseOkBody +{ + fn from(body: PostReservedRoomKeepResponseOkSpectatorAnswerBody) -> Self { + Self::SpectatorAnswer(body) + } +} + +pub type PostReservedRoomKeepResponse = PostRoomKeepResponse; + +impl From for PostReservedRoomKeepResponse { + fn from(body: PostReservedRoomKeepResponseOkBody) -> Self { + Self::Ok(body) + } +} + +// POST /reserved-room/{name}/join + +pub use super::room::PostRoomJoinRequestBody as PostReservedRoomSpectateRequestBody; +pub use super::room::PostRoomJoinResponse as PostReservedRoomSpectateResponse; diff --git a/junowen-server/README.md b/junowen-server/README.md index 260c0fa..1d1d675 100644 --- a/junowen-server/README.md +++ b/junowen-server/README.md @@ -1,6 +1,8 @@ +# junowen-server + ## create -``` +```sh cargo lambda deploy \ --binary-name junowen-server \ --enable-function-url \ @@ -12,7 +14,7 @@ cargo lambda deploy \ ## Dynamo DB definition * env = dev | prod -* table_name = Offer | Answer +* table_name = Offer | Answer | ReservedRoom | ReservedRoomOpponentAnswer | ReservedRoomSpectatorAnswer ### {env}.{table_name} diff --git a/junowen-server/src/database.rs b/junowen-server/src/database.rs index 5f21fd8..d2f8aa3 100644 --- a/junowen-server/src/database.rs +++ b/junowen-server/src/database.rs @@ -6,7 +6,7 @@ pub use dynamodb::DynamoDB; pub use file::File; use anyhow::Result; -use getset::Getters; +use getset::{Getters, Setters}; use junowen_lib::connection::signaling::CompressedSdp; use serde::{Deserialize, Serialize}; @@ -73,4 +73,73 @@ pub trait SharedRoomTables: Send + Sync + 'static { ) -> Result>; } -pub trait Database: SharedRoomTables {} +#[derive(Clone, Debug, Deserialize, Getters, Setters, Serialize, new)] +pub struct ReservedRoom { + /// primary + #[get = "pub"] + name: String, + /// ルームの所有者であることを証明する為のキー + #[get = "pub"] + key: String, + #[getset(get = "pub", set = "pub")] + opponent_offer_sdp: Option, + #[get = "pub"] + spectator_offer_sdp: Option, + ttl_sec: u64, +} + +impl ReservedRoom { + pub fn into_opponent_offer_sdp(self) -> Option { + self.opponent_offer_sdp + } + + pub fn into_opponent_offer_sdp_spectator_offer_sdp( + self, + ) -> (Option, Option) { + (self.opponent_offer_sdp, self.spectator_offer_sdp) + } + + pub fn is_expired(&self, now_sec: u64) -> bool { + now_sec > self.ttl_sec + } +} + +#[derive(Serialize, Deserialize)] +pub struct ReservedRoomOpponentAnswer(pub Answer); +#[derive(Serialize, Deserialize)] +pub struct ReservedRoomSpectatorAnswer(pub Answer); + +pub trait ReservedRoomTables: Send + Sync + 'static { + async fn put_room(&self, offer: ReservedRoom) -> Result<(), PutError>; + async fn find_room(&self, name: String) -> Result>; + async fn keep_room( + &self, + name: String, + key: String, + spectator_offer_sdp: Option, + ttl_sec: u64, + ) -> Result>; + async fn remove_opponent_offer_sdp_in_room(&self, name: String) -> Result; + async fn remove_spectator_offer_sdp_in_room(&self, name: String) -> Result; + async fn remove_room(&self, name: String, key: Option) -> Result; + + async fn put_room_opponent_answer( + &self, + answer: ReservedRoomOpponentAnswer, + ) -> Result<(), PutError>; + async fn remove_room_opponent_answer( + &self, + name: String, + ) -> Result>; + + async fn put_room_spectator_answer( + &self, + answer: ReservedRoomSpectatorAnswer, + ) -> Result<(), PutError>; + async fn remove_room_spectator_answer( + &self, + name: String, + ) -> Result>; +} + +pub trait Database: SharedRoomTables + ReservedRoomTables {} diff --git a/junowen-server/src/database/dynamodb.rs b/junowen-server/src/database/dynamodb.rs index db02d7b..381d21e 100644 --- a/junowen-server/src/database/dynamodb.rs +++ b/junowen-server/src/database/dynamodb.rs @@ -1,3 +1,4 @@ +mod reserved_room; mod shared_room; use std::env; @@ -16,6 +17,9 @@ pub struct DynamoDB { client: aws_sdk_dynamodb::Client, table_name_shared_room: String, table_name_shared_room_opponent_answer: String, + table_name_reserved_room: String, + table_name_reserved_room_opponent_answer: String, + table_name_reserved_room_spectator_answer: String, } impl DynamoDB { @@ -25,6 +29,15 @@ impl DynamoDB { client: aws_sdk_dynamodb::Client::new(&config), table_name_shared_room: format!("{}.Offer", env::var("ENV").unwrap()), table_name_shared_room_opponent_answer: format!("{}.Answer", env::var("ENV").unwrap()), + table_name_reserved_room: format!("{}.ReservedRoom", env::var("ENV").unwrap()), + table_name_reserved_room_opponent_answer: format!( + "{}.ReservedRoomOpponentAnswer", + env::var("ENV").unwrap() + ), + table_name_reserved_room_spectator_answer: format!( + "{}.ReservedRoomSpectatorAnswer", + env::var("ENV").unwrap() + ), } } diff --git a/junowen-server/src/database/dynamodb/reserved_room.rs b/junowen-server/src/database/dynamodb/reserved_room.rs new file mode 100644 index 0000000..01c1e07 --- /dev/null +++ b/junowen-server/src/database/dynamodb/reserved_room.rs @@ -0,0 +1,150 @@ +use anyhow::{anyhow, Result}; +use aws_sdk_dynamodb::{ + error::SdkError, + types::{AttributeValue, ReturnValue}, +}; +use serde_dynamo::from_item; + +use crate::database::{ + self, PutError, ReservedRoom, ReservedRoomOpponentAnswer, ReservedRoomSpectatorAnswer, +}; + +use super::DynamoDB; + +impl database::ReservedRoomTables for DynamoDB { + async fn put_room(&self, room: ReservedRoom) -> Result<(), PutError> { + self.put_item(&self.table_name_reserved_room, room).await + } + + async fn find_room(&self, name: String) -> Result> { + self.find_item_by_name(&self.table_name_reserved_room, name) + .await + } + + async fn keep_room( + &self, + name: String, + key: String, + spectator_offer_sdp: Option, + ttl_sec: u64, + ) -> Result> { + let mut builder = self + .client + .update_item() + .table_name(&self.table_name_reserved_room) + .return_values(ReturnValue::AllNew) + .key("name", AttributeValue::S(name)) + .condition_expression("#key = :key") + .update_expression("SET #ttl_sec = :ttl_sec") + .expression_attribute_names("#key", "key") + .expression_attribute_names("#ttl_sec", "ttl_sec") + .expression_attribute_values(":key", AttributeValue::S(key)) + .expression_attribute_values(":ttl_sec", AttributeValue::N((ttl_sec).to_string())); + if let Some(spectator_offer_sdp) = spectator_offer_sdp { + builder = builder + .expression_attribute_names("#spectator_offer_sdp", "spectator_offer_sdp") + .expression_attribute_values( + ":spectator_offer_sdp", + AttributeValue::S(spectator_offer_sdp), + ); + } + let result = builder.send().await; + match result { + Err(error) => { + if let SdkError::ServiceError(service_error) = &error { + if service_error.err().is_conditional_check_failed_exception() { + return Ok(None); + } + } + Err(error.into()) + } + Ok(output) => { + let item = output + .attributes() + .ok_or_else(|| anyhow!("attributes not found"))?; + Ok(Some(from_item(item.to_owned())?)) + } + } + } + + async fn remove_opponent_offer_sdp_in_room(&self, name: String) -> Result { + let result = self + .client + .update_item() + .table_name(&self.table_name_reserved_room) + .key("name", AttributeValue::S(name)) + .update_expression("SET #opponent_offer_sdp = :opponent_offer_sdp") + .expression_attribute_names("#opponent_offer_sdp", "opponent_offer_sdp") + .expression_attribute_values(":opponent_offer_sdp", AttributeValue::Null(true)) + .send() + .await; + if let Err(err) = result { + if let SdkError::ServiceError(service_error) = &err { + if service_error.err().is_conditional_check_failed_exception() { + return Ok(false); + } + } + return Err(err.into()); + } + Ok(true) + } + + async fn remove_spectator_offer_sdp_in_room(&self, name: String) -> Result { + let result = self + .client + .update_item() + .table_name(&self.table_name_reserved_room) + .key("name", AttributeValue::S(name)) + .update_expression("SET #spectator_offer_sdp = :spectator_offer_sdp") + .expression_attribute_names("#spectator_offer_sdp", "spectator_offer_sdp") + .expression_attribute_values(":spectator_offer_sdp", AttributeValue::Null(true)) + .send() + .await; + if let Err(err) = result { + if let SdkError::ServiceError(service_error) = &err { + if service_error.err().is_conditional_check_failed_exception() { + return Ok(false); + } + } + return Err(err.into()); + } + Ok(true) + } + + async fn remove_room(&self, name: String, key: Option) -> Result { + self.remove_item(&self.table_name_reserved_room, name, key) + .await + } + + async fn put_room_opponent_answer( + &self, + answer: ReservedRoomOpponentAnswer, + ) -> Result<(), PutError> { + self.put_item(&self.table_name_reserved_room_opponent_answer, answer) + .await + } + + async fn remove_room_opponent_answer( + &self, + name: String, + ) -> Result> { + self.remove_item_and_get_old(&self.table_name_reserved_room_opponent_answer, name) + .await + } + + async fn put_room_spectator_answer( + &self, + answer: ReservedRoomSpectatorAnswer, + ) -> Result<(), PutError> { + self.put_item(&self.table_name_reserved_room_spectator_answer, answer) + .await + } + + async fn remove_room_spectator_answer( + &self, + name: String, + ) -> Result> { + self.remove_item_and_get_old(&self.table_name_reserved_room_spectator_answer, name) + .await + } +} diff --git a/junowen-server/src/database/file.rs b/junowen-server/src/database/file.rs index f131fd6..398a801 100644 --- a/junowen-server/src/database/file.rs +++ b/junowen-server/src/database/file.rs @@ -2,7 +2,11 @@ use anyhow::Result; use serde_json::Value; use tokio::fs; -use super::{Answer, Database, PutError, SharedRoom, SharedRoomOpponentAnswer, SharedRoomTables}; +use super::{ + Answer, Database, PutError, ReservedRoom, ReservedRoomOpponentAnswer, + ReservedRoomSpectatorAnswer, ReservedRoomTables, SharedRoom, SharedRoomOpponentAnswer, + SharedRoomTables, +}; pub struct File; @@ -129,4 +133,64 @@ impl SharedRoomTables for File { } } +impl ReservedRoomTables for File { + async fn put_room(&self, _offer: ReservedRoom) -> Result<(), PutError> { + unimplemented!(); + } + + async fn find_room(&self, _name: String) -> Result> { + unimplemented!(); + } + + async fn remove_room(&self, _name: String, _key: Option) -> Result { + unimplemented!(); + } + + async fn remove_opponent_offer_sdp_in_room(&self, _name: String) -> Result { + unimplemented!() + } + + async fn remove_spectator_offer_sdp_in_room(&self, _name: String) -> Result { + unimplemented!() + } + + async fn put_room_opponent_answer( + &self, + _answer: ReservedRoomOpponentAnswer, + ) -> Result<(), PutError> { + unimplemented!() + } + + async fn remove_room_opponent_answer( + &self, + _name: String, + ) -> Result> { + unimplemented!() + } + + async fn put_room_spectator_answer( + &self, + _answer: ReservedRoomSpectatorAnswer, + ) -> Result<(), PutError> { + unimplemented!() + } + + async fn remove_room_spectator_answer( + &self, + _name: String, + ) -> Result> { + unimplemented!() + } + + async fn keep_room( + &self, + _name: String, + _key: String, + _spectator_offer_sdp: Option, + _ttl_sec: u64, + ) -> Result> { + unimplemented!() + } +} + impl Database for File {} diff --git a/junowen-server/src/routes.rs b/junowen-server/src/routes.rs index d91c07a..dadae17 100644 --- a/junowen-server/src/routes.rs +++ b/junowen-server/src/routes.rs @@ -1,4 +1,5 @@ mod custom; +mod reserved_room; mod room_utils; use std::hash::{DefaultHasher, Hash, Hasher}; @@ -85,5 +86,10 @@ pub async fn routes(req: &Request, db: &impl Database) -> Result Result> { + let regex = Regex::new(r"^([^/]+)$").unwrap(); + if let Some(c) = regex.captures(relative_uri) { + return Ok(match *req.method() { + Method::PUT => match try_parse(req.body()) { + Err(err) => { + debug!("{:?}", err); + to_response(StatusCode::BAD_REQUEST, None, Body::Empty) + } + Ok(body) => { + let res = put_room(db, &c[1], body).await?; + from_put_room_response(res) + } + }, + Method::GET => { + let res = get_room(db, &c[1]).await?; + to_response( + res.status_code(), + Some(RETRY_AFTER_INTERVAL_SEC), + res.to_body().map(Body::Text).unwrap_or_else(|| Body::Empty), + ) + } + Method::DELETE => match try_parse(req.body()) { + Err(err) => { + debug!("{:?}", err); + to_response(StatusCode::BAD_REQUEST, None, Body::Empty) + } + Ok(body) => { + let res = delete_room(db, &c[1], body).await?; + to_response(res.status_code(), None, Body::Empty) + } + }, + _ => to_response(StatusCode::METHOD_NOT_ALLOWED, None, Body::Empty), + }); + } + let regex = Regex::new(r"^([^/]+)/join$").unwrap(); + if let Some(c) = regex.captures(relative_uri) { + return Ok(match *req.method() { + Method::POST => match try_parse(req.body()) { + Err(err) => { + debug!("{:?}", err); + to_response(StatusCode::BAD_REQUEST, None, Body::Empty) + } + Ok(body) => { + let res = post_room_join(db, &c[1], body).await?; + to_response(res.status_code_old(), None, Body::Empty) + } + }, + _ => to_response(StatusCode::METHOD_NOT_ALLOWED, None, Body::Empty), + }); + } + let regex = Regex::new(r"^([^/]+)/spectate$").unwrap(); + if let Some(c) = regex.captures(relative_uri) { + return Ok(match *req.method() { + Method::POST => match try_parse(req.body()) { + Err(err) => { + debug!("{:?}", err); + to_response(StatusCode::BAD_REQUEST, None, Body::Empty) + } + Ok(body) => { + let res = post_room_spectate(db, &c[1], body).await?; + to_response(res.status_code_old(), None, Body::Empty) + } + }, + _ => to_response(StatusCode::METHOD_NOT_ALLOWED, None, Body::Empty), + }); + } + let regex = Regex::new(r"^([^/]+)/keep$").unwrap(); + if let Some(c) = regex.captures(relative_uri) { + return Ok(match *req.method() { + Method::POST => match try_parse(req.body()) { + Err(err) => { + debug!("{:?}", err); + to_response(StatusCode::BAD_REQUEST, None, Body::Empty) + } + Ok(body) => { + let res = post_room_keep(db, &c[1], body).await?; + from_post_room_keep_response(res) + } + }, + _ => to_response(StatusCode::METHOD_NOT_ALLOWED, None, Body::Empty), + }); + } + Ok(to_response(StatusCode::NOT_FOUND, None, Body::Empty)) +} diff --git a/junowen-server/src/routes/reserved_room/create.rs b/junowen-server/src/routes/reserved_room/create.rs new file mode 100644 index 0000000..1049dca --- /dev/null +++ b/junowen-server/src/routes/reserved_room/create.rs @@ -0,0 +1,58 @@ +use anyhow::{bail, Result}; +use junowen_lib::signaling_server::{ + reserved_room::{PutReservedRoomResponse, PutReservedRoomResponseConflictBody}, + room::{PutRoomRequestBody, PutRoomResponseAnswerBody, PutRoomResponseWaitingBody}, +}; +use tracing::info; +use uuid::Uuid; + +use crate::{ + database::{PutError, ReservedRoom, ReservedRoomTables}, + routes::{ + reserved_room::{read::find_valid_room, update::find_opponent}, + room_utils::{now_sec, ttl_sec, RETRY_AFTER_INTERVAL_SEC}, + }, +}; + +pub async fn put_room( + db: &impl ReservedRoomTables, + name: &str, + body: PutRoomRequestBody, +) -> Result { + let now_sec = now_sec(); + let key = Uuid::new_v4().to_string(); + let room = ReservedRoom::new( + name.to_owned(), + key.clone(), + Some(body.offer().clone()), + None, + ttl_sec(now_sec), + ); + for retry in 0.. { + if let Some(room) = find_valid_room(db, now_sec, name.to_owned()).await? { + let body = PutReservedRoomResponseConflictBody::new(room.into_opponent_offer_sdp()); + let response = PutReservedRoomResponse::conflict(RETRY_AFTER_INTERVAL_SEC, body); + return Ok(response); + } + match db.put_room(room.clone()).await { + Ok(()) => break, + Err(PutError::Conflict) => { + if retry >= 2 { + panic!(); + } + continue; + } + Err(PutError::Unknown(err)) => bail!("{:?}", err), + } + } + info!("[Reserved Room] Created: {}", name); + Ok( + if let Some(answer) = find_opponent(db, name.to_owned()).await? { + let body = PutRoomResponseAnswerBody::new(answer.0.into_sdp()); + PutReservedRoomResponse::created_with_answer(RETRY_AFTER_INTERVAL_SEC, body) + } else { + let body = PutRoomResponseWaitingBody::new(key); + PutReservedRoomResponse::created_with_key(RETRY_AFTER_INTERVAL_SEC, body) + }, + ) +} diff --git a/junowen-server/src/routes/reserved_room/delete.rs b/junowen-server/src/routes/reserved_room/delete.rs new file mode 100644 index 0000000..231cc99 --- /dev/null +++ b/junowen-server/src/routes/reserved_room/delete.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use junowen_lib::signaling_server::room::{DeleteRoomRequestBody, DeleteRoomResponse}; +use tracing::info; + +use crate::database::ReservedRoomTables; + +pub async fn delete_room( + db: &impl ReservedRoomTables, + name: &str, + body: DeleteRoomRequestBody, +) -> Result { + if !db + .remove_room(name.to_owned(), Some(body.into_key())) + .await? + { + Ok(DeleteRoomResponse::BadRequest) + } else { + db.remove_room_opponent_answer(name.to_owned()).await?; + info!("[Reserved Room] Removed: {}", name); + Ok(DeleteRoomResponse::NoContent) + } +} diff --git a/junowen-server/src/routes/reserved_room/read.rs b/junowen-server/src/routes/reserved_room/read.rs new file mode 100644 index 0000000..09a5b36 --- /dev/null +++ b/junowen-server/src/routes/reserved_room/read.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use junowen_lib::signaling_server::reserved_room::{ + GetReservedRoomResponse, GetReservedRoomResponseOkBody, +}; + +use crate::{ + database::{ReservedRoom, ReservedRoomTables}, + routes::room_utils::now_sec, +}; + +pub async fn find_valid_room( + db: &impl ReservedRoomTables, + now_sec: u64, + name: String, +) -> Result> { + let Some(offer) = db.find_room(name.to_owned()).await? else { + return Ok(None); + }; + if !offer.is_expired(now_sec) { + return Ok(Some(offer)); + } + db.remove_room(offer.name().clone(), None).await?; + db.remove_room_opponent_answer(offer.name().clone()).await?; + db.remove_room_spectator_answer(offer.name().clone()) + .await?; + Ok(None) +} + +pub async fn get_room(db: &impl ReservedRoomTables, name: &str) -> Result { + let now_sec = now_sec(); + let Some(room) = find_valid_room(db, now_sec, name.to_owned()).await? else { + return Ok(GetReservedRoomResponse::NotFound); + }; + let (opponent_offer_sdp, spectator_offer_sdp) = + room.into_opponent_offer_sdp_spectator_offer_sdp(); + let body = GetReservedRoomResponseOkBody::new(opponent_offer_sdp, spectator_offer_sdp); + Ok(GetReservedRoomResponse::Ok(body)) +} diff --git a/junowen-server/src/routes/reserved_room/update.rs b/junowen-server/src/routes/reserved_room/update.rs new file mode 100644 index 0000000..7acb976 --- /dev/null +++ b/junowen-server/src/routes/reserved_room/update.rs @@ -0,0 +1,127 @@ +use anyhow::Result; +use junowen_lib::signaling_server::{ + reserved_room::{ + PostReservedRoomKeepRequestBody, PostReservedRoomKeepResponse, + PostReservedRoomKeepResponseOkBody, PostReservedRoomKeepResponseOkOpponentAnswerBody, + PostReservedRoomKeepResponseOkSpectatorAnswerBody, PostReservedRoomSpectateRequestBody, + PostReservedRoomSpectateResponse, + }, + room::{PostRoomJoinRequestBody, PostRoomJoinResponse}, +}; +use tracing::info; +use uuid::Uuid; + +use crate::{ + database::{ + Answer, PutError, ReservedRoomOpponentAnswer, ReservedRoomSpectatorAnswer, + ReservedRoomTables, + }, + routes::room_utils::{now_sec, ttl_sec, RETRY_AFTER_INTERVAL_SEC}, +}; + +pub async fn find_opponent( + db: &impl ReservedRoomTables, + name: String, +) -> Result> { + let Some(answer) = db.remove_room_opponent_answer(name.clone()).await? else { + return Ok(None); + }; + db.remove_opponent_offer_sdp_in_room(name).await?; + Ok(Some(answer)) +} + +pub async fn find_spectator( + db: &impl ReservedRoomTables, + name: String, +) -> Result> { + let Some(answer) = db.remove_room_spectator_answer(name.clone()).await? else { + return Ok(None); + }; + db.remove_spectator_offer_sdp_in_room(name).await?; + Ok(Some(answer)) +} + +pub async fn post_room_keep( + db: &impl ReservedRoomTables, + name: &str, + body: PostReservedRoomKeepRequestBody, +) -> Result { + let (key, spectator_offer) = body.into_inner(); + if Uuid::parse_str(&key).is_err() { + return Ok(PostReservedRoomKeepResponse::BadRequest); + } + let room = db + .keep_room(name.to_owned(), key, spectator_offer, ttl_sec(now_sec())) + .await?; + let Some(room) = room else { + return Ok(PostReservedRoomKeepResponse::BadRequest); + }; + if room.opponent_offer_sdp().is_some() { + return Ok( + if let Some(answer) = find_opponent(db, name.to_owned()).await? { + PostReservedRoomKeepResponseOkBody::from( + PostReservedRoomKeepResponseOkOpponentAnswerBody::new(answer.0.into_sdp()), + ) + .into() + } else { + let retry_after = RETRY_AFTER_INTERVAL_SEC; + PostReservedRoomKeepResponse::NoContent { retry_after } + }, + ); + } + if room.spectator_offer_sdp().is_some() { + return Ok( + if let Some(answer) = find_spectator(db, name.to_owned()).await? { + PostReservedRoomKeepResponseOkBody::from( + PostReservedRoomKeepResponseOkSpectatorAnswerBody::new(answer.0.into_sdp()), + ) + .into() + } else { + let retry_after = RETRY_AFTER_INTERVAL_SEC; + PostReservedRoomKeepResponse::NoContent { retry_after } + }, + ); + } + let retry_after = RETRY_AFTER_INTERVAL_SEC; + Ok(PostReservedRoomKeepResponse::NoContent { retry_after }) +} + +pub async fn post_room_join( + db: &impl ReservedRoomTables, + name: &str, + body: PostRoomJoinRequestBody, +) -> Result { + let answer = ReservedRoomOpponentAnswer(Answer::new( + name.to_owned(), + body.into_answer(), + ttl_sec(now_sec()), + )); + match db.put_room_opponent_answer(answer).await { + Ok(()) => { + info!("[Reserved Room] Join: {}", name); + Ok(PostRoomJoinResponse::Ok) + } + Err(PutError::Conflict) => Ok(PostRoomJoinResponse::Conflict), + Err(PutError::Unknown(err)) => Err(err), + } +} + +pub async fn post_room_spectate( + db: &impl ReservedRoomTables, + name: &str, + body: PostReservedRoomSpectateRequestBody, +) -> Result { + let answer = ReservedRoomSpectatorAnswer(Answer::new( + name.to_owned(), + body.into_answer(), + ttl_sec(now_sec()), + )); + match db.put_room_spectator_answer(answer).await { + Ok(()) => { + info!("[Reserved Room] Spectate: {}", name); + Ok(PostRoomJoinResponse::Ok) + } + Err(PutError::Conflict) => Ok(PostRoomJoinResponse::Conflict), + Err(PutError::Unknown(err)) => Err(err), + } +}