use std::sync::Arc;
use futures_util::Stream;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::client::{CallOptions, Client};
use crate::common::{string_enum, Region, ResourceLinks};
use crate::error::Result;
use crate::pagination::{auto_page, paginate, ListParams, Page, PageFetcher};
use crate::query::QueryBuilder;
use crate::resources::escape;
const PATH: &str = "/v2/rooms";
string_enum! {
AutoCloseType {
SESSION_ENDS => "session-ends",
SESSION_END_AND_DEACTIVATE => "session-end-and-deactivate",
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoomWebhook {
#[serde(rename = "endPoint")]
pub end_point: String,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub events: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoCloseConfig {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub kind: Option<AutoCloseType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MultiComposition {
#[serde(skip_serializing_if = "Option::is_none")]
pub recording: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rtmp: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AutoStartConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub recording: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hls: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub composite: Option<Value>,
}
#[derive(Debug, Clone)]
pub struct RoomWebhookInput {
pub url: String,
pub events: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct AutoCloseConfigInput {
pub after_inactive_seconds: Option<u32>,
pub kind: Option<AutoCloseType>,
}
#[derive(Debug, Clone, Default)]
pub struct RoomCreateParams {
pub custom_room_id: Option<String>,
pub geo_fence: Option<Region>,
pub webhook: Option<RoomWebhookInput>,
pub auto_close_config: Option<AutoCloseConfigInput>,
pub auto_start_config: Option<AutoStartConfig>,
pub multi_composition: Option<MultiComposition>,
pub allowed_participant_ids: Option<Vec<String>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct RoomCreateWire {
#[serde(skip_serializing_if = "Option::is_none")]
custom_room_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
geo_fence: Option<Region>,
#[serde(skip_serializing_if = "Option::is_none")]
auto_start_config: Option<AutoStartConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
multi_composition: Option<MultiComposition>,
#[serde(skip_serializing_if = "Option::is_none")]
allowed_participant_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
webhook: Option<RoomWebhook>,
#[serde(skip_serializing_if = "Option::is_none")]
auto_close_config: Option<AutoCloseConfig>,
}
impl From<RoomCreateParams> for RoomCreateWire {
fn from(params: RoomCreateParams) -> Self {
Self {
custom_room_id: params.custom_room_id,
geo_fence: params.geo_fence,
auto_start_config: params.auto_start_config,
multi_composition: params.multi_composition,
allowed_participant_ids: params.allowed_participant_ids,
webhook: params.webhook.map(|webhook| RoomWebhook {
end_point: webhook.url,
events: webhook.events,
}),
auto_close_config: params.auto_close_config.map(|config| AutoCloseConfig {
kind: Some(config.kind.unwrap_or(AutoCloseType::SESSION_ENDS)),
duration: config.after_inactive_seconds,
}),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Room {
pub id: String,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub room_id: String,
pub custom_room_id: Option<String>,
pub api_key: Option<String>,
pub geo_fence: Option<Region>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub disabled: bool,
pub webhook: Option<RoomWebhook>,
pub auto_close_config: Option<AutoCloseConfig>,
pub auto_start_config: Option<Value>,
pub multi_composition: Option<Value>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub allowed_participant_ids: Vec<String>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub links: ResourceLinks,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone)]
pub struct RoomValidation {
pub valid: bool,
pub room_id: String,
pub room: Option<Room>,
}
#[derive(Debug, Clone, Default)]
pub struct RoomListParams {
pub page: Option<u32>,
pub per_page: Option<u32>,
pub cursor: Option<String>,
pub query: Option<String>,
pub user_id: Option<String>,
}
impl RoomListParams {
fn pagination(&self) -> ListParams {
ListParams {
page: self.page,
per_page: self.per_page,
cursor: self.cursor.clone(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RoomsResource<'a> {
client: &'a Client,
}
impl<'a> RoomsResource<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn create(&self, params: RoomCreateParams) -> Result<Room> {
let body = RoomCreateWire::from(params);
self.client
.json(Method::POST, PATH, CallOptions::json(&body)?)
.await
}
pub async fn list(&self, params: RoomListParams) -> Result<Page<Room>> {
paginate(self.fetcher(¶ms), ¶ms.pagination(), "data", None).await
}
pub fn list_stream(&self, params: RoomListParams) -> impl Stream<Item = Result<Room>> + Send {
auto_page(self.fetcher(¶ms), params.pagination(), "data", None)
}
pub async fn get(&self, room_id: &str) -> Result<Room> {
let path = format!("{PATH}/{}", escape(room_id));
self.client
.json(Method::GET, &path, CallOptions::new())
.await
}
pub async fn validate(&self, room_id: &str) -> Result<RoomValidation> {
let path = format!("{PATH}/validate/{}", escape(room_id));
match self
.client
.json::<Room>(Method::GET, &path, CallOptions::new())
.await
{
Ok(room) => Ok(RoomValidation {
valid: true,
room_id: if room.room_id.is_empty() {
room_id.to_string()
} else {
room.room_id.clone()
},
room: Some(room),
}),
Err(err) if matches!(err.status(), Some(400 | 402 | 404)) => Ok(RoomValidation {
valid: false,
room_id: room_id.to_string(),
room: None,
}),
Err(err) => Err(err),
}
}
pub async fn end(&self, room_id: &str) -> Result<Room> {
let body = serde_json::json!({ "roomId": room_id });
self.client
.json(
Method::POST,
"/v2/rooms/deactivate",
CallOptions::json(&body)?,
)
.await
}
fn fetcher(&self, params: &RoomListParams) -> PageFetcher {
let client = self.client.clone();
let query = params.query.clone();
let user_id = params.user_id.clone();
Arc::new(move |page, per_page| {
let client = client.clone();
let query = query.clone();
let user_id = user_id.clone();
Box::pin(async move {
let params = QueryBuilder::new()
.opt("page", page)
.opt("perPage", per_page)
.opt_str("query", query.as_deref())
.opt_str("userId", user_id.as_deref())
.into_pairs();
client
.json::<Value>(Method::GET, PATH, CallOptions::new().query(params))
.await
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn wire(params: RoomCreateParams) -> Value {
serde_json::to_value(RoomCreateWire::from(params)).unwrap()
}
#[test]
fn an_empty_create_sends_an_empty_body() {
assert_eq!(wire(RoomCreateParams::default()), json!({}));
}
#[test]
fn webhook_url_is_renamed_to_end_point() {
let body = wire(RoomCreateParams {
webhook: Some(RoomWebhookInput {
url: "https://example.com/hook".into(),
events: vec!["session-started".into()],
}),
..Default::default()
});
assert_eq!(
body["webhook"],
json!({"endPoint": "https://example.com/hook", "events": ["session-started"]})
);
assert!(body["webhook"].get("url").is_none());
}
#[test]
fn auto_close_seconds_are_renamed_to_duration_with_a_default_type() {
let body = wire(RoomCreateParams {
auto_close_config: Some(AutoCloseConfigInput {
after_inactive_seconds: Some(300),
kind: None,
}),
..Default::default()
});
assert_eq!(
body["autoCloseConfig"],
json!({"type": "session-ends", "duration": 300})
);
}
#[test]
fn an_explicit_auto_close_type_is_preserved() {
let body = wire(RoomCreateParams {
auto_close_config: Some(AutoCloseConfigInput {
after_inactive_seconds: None,
kind: Some(AutoCloseType::SESSION_END_AND_DEACTIVATE),
}),
..Default::default()
});
assert_eq!(
body["autoCloseConfig"],
json!({"type": "session-end-and-deactivate"})
);
}
#[test]
fn remaining_fields_pass_through_camel_cased() {
let body = wire(RoomCreateParams {
custom_room_id: Some("my-room".into()),
geo_fence: Some(Region::IN002),
allowed_participant_ids: Some(vec!["p-1".into()]),
multi_composition: Some(MultiComposition {
recording: Some(true),
..Default::default()
}),
..Default::default()
});
assert_eq!(
body,
json!({
"customRoomId": "my-room",
"geoFence": "in002",
"allowedParticipantIds": ["p-1"],
"multiComposition": {"recording": true},
})
);
}
#[test]
fn a_room_keeps_unmodeled_fields_in_extra() {
let room: Room = serde_json::from_value(json!({
"id": "db-1",
"roomId": "abcd-efgh-ijkl",
"somethingNew": 42,
}))
.unwrap();
assert_eq!(room.room_id, "abcd-efgh-ijkl");
assert!(!room.disabled);
assert_eq!(room.extra.get("somethingNew"), Some(&json!(42)));
}
}