opentalk_roomserver_client/
lib.rs1use api::signaling::{SignalingConnection, SignalingError};
9use bytes::Bytes;
10use http::{HeaderValue, StatusCode, header::InvalidHeaderValue};
11use opentalk_roomserver_types::{
12 api::{RoomServerAccess, TokenRequestBody},
13 client_parameters::ClientParameters,
14 room_parameters::RoomParameters,
15 room_parameters_patch::RoomParametersPatch,
16};
17use opentalk_service_auth::{ApiKey, EncodingError};
18use opentalk_types_api_v1::assets::Quota;
19use opentalk_types_common::{rooms::RoomId, roomserver::Token, users::UserId};
20use reqwest::{Client as ReqwestClient, Response, header::AUTHORIZATION};
21use serde::Deserialize;
22use thiserror::Error;
23use url::Url;
24
25pub mod api;
26
27#[derive(Debug, Error)]
28pub enum InvalidApiToken {
29 #[error("Failed to encode JSON web token: {0:?}")]
30 EncodingError(#[from] EncodingError),
31 #[error("Failed to create authorization header {0:?} ")]
32 ParsingError(#[from] InvalidHeaderValue),
33}
34
35#[derive(Debug, Error)]
36pub enum Error<T> {
37 #[error("Failed to set authorization header: {0}")]
38 TokenError(#[from] InvalidApiToken),
39 #[error("Failed to parse URL: {0}")]
40 UrlParse(#[from] url::ParseError),
41 #[error("Reqwest error: {0}")]
42 Reqwest(#[from] reqwest::Error),
43 #[error("RoomServer returned an error: {0:#?}")]
44 ApiError(#[from] ApiError<T>),
45 #[error("RoomServer returned an unexpected response:\nstatus: {status}\nbody: {body}")]
46 Unexpected { status: StatusCode, body: String },
47}
48
49#[derive(Debug, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub struct ApiError<T> {
52 pub code: T,
53 pub message: String,
54}
55
56#[derive(Error, Debug, Deserialize, PartialEq, Eq)]
57#[serde(rename_all = "snake_case")]
58pub enum PutRoomError {
59 #[error("The provided API token is invalid")]
60 InvalidApiToken,
61 #[error("The room already exists, but is shutting down")]
62 NotFound,
63 #[error("An internal server error occurred")]
64 InternalServerError,
65}
66
67#[derive(Error, Debug, Deserialize, PartialEq, Eq)]
68#[serde(rename_all = "snake_case")]
69pub enum PatchRoomError {
70 #[error("The provided API token is invalid")]
71 InvalidApiToken,
72 #[error("The room does not exist")]
73 NotFound,
74}
75
76#[derive(Error, Debug, Deserialize, PartialEq, Eq)]
77#[serde(rename_all = "snake_case")]
78pub enum PostStorageQuotaError {
79 #[error("No rooms created by the specified user exist")]
80 NotFound,
81}
82
83#[derive(Error, Debug, Deserialize, PartialEq, Eq)]
84#[serde(rename_all = "snake_case")]
85pub enum RequestTokenError {
86 #[error("The provided API token is invalid")]
87 InvalidApiToken,
88 #[error("The requested room does not exist and no room parameters were provided")]
89 RoomParametersMissing,
90 #[error("An internal server error occurred")]
91 InternalServerError,
92 #[error("The requesting participant is banned from the room")]
93 Banned,
94}
95
96#[derive(Debug, Clone)]
97pub struct Client {
98 reqwest_client: ReqwestClient,
99 base_url: Url,
100 api_key: ApiKey,
101}
102
103impl Client {
104 pub fn new(base_url: Url, api_key: ApiKey) -> Client {
105 let reqwest_client = ReqwestClient::new();
106
107 Self {
108 reqwest_client,
109 base_url,
110 api_key,
111 }
112 }
113
114 fn auth_header(&self) -> Result<HeaderValue, InvalidApiToken> {
115 Ok(format!("Bearer {}", self.api_key.generate_jwt()?).parse()?)
116 }
117
118 #[tracing::instrument(skip(self, parameters))]
119 pub async fn put_room(
120 &self,
121 room_id: RoomId,
122 parameters: RoomParameters,
123 ) -> Result<(), Error<PutRoomError>> {
124 let url = self.base_url.join(&format!("v1/rooms/{room_id}"))?;
125 let response = self
126 .reqwest_client
127 .put(url)
128 .header(AUTHORIZATION, self.auth_header()?)
129 .json(¶meters)
130 .send()
131 .await?;
132
133 if response.status().is_success() {
134 return Ok(());
135 }
136
137 Err(Self::parse_api_error::<PutRoomError>(
138 response.status(),
139 &response.bytes().await?,
140 ))
141 }
142
143 #[tracing::instrument(skip(self))]
144 pub async fn patch_room(
145 &self,
146 room_id: RoomId,
147 patch: RoomParametersPatch,
148 ) -> Result<(), Error<PatchRoomError>> {
149 let url = self.base_url.join(&format!("v1/rooms/{room_id}"))?;
150 let response = self
151 .reqwest_client
152 .patch(url)
153 .header(AUTHORIZATION, self.auth_header()?)
154 .json(&patch)
155 .send()
156 .await?;
157
158 if response.status().is_success() {
159 return Ok(());
160 }
161
162 Err(Self::parse_api_error::<PatchRoomError>(
163 response.status(),
164 &response.bytes().await?,
165 ))
166 }
167
168 #[tracing::instrument(skip(self))]
169 pub async fn post_storage_quota(
170 &self,
171 user_id: UserId,
172 quota: Quota,
173 ) -> Result<(), Error<PostStorageQuotaError>> {
174 let url = self
175 .base_url
176 .join(&format!("v1/user/{user_id}/storage_quota"))?;
177 let response = self
178 .reqwest_client
179 .post(url)
180 .header(AUTHORIZATION, self.auth_header()?)
181 .json("a)
182 .send()
183 .await?;
184
185 if response.status().is_success() {
186 return Ok(());
187 }
188
189 Err(Self::parse_api_error::<PostStorageQuotaError>(
190 response.status(),
191 &response.bytes().await?,
192 ))
193 }
194
195 #[tracing::instrument(skip(self, client_parameters, room_parameters))]
196 pub async fn request_token(
197 &self,
198 room_id: RoomId,
199 client_parameters: ClientParameters,
200 room_parameters: Option<RoomParameters>,
201 ) -> Result<RoomServerAccess, Error<RequestTokenError>> {
202 let url = self.base_url.join(&format!("v1/rooms/{room_id}/token"))?;
203 let response = self
204 .reqwest_client
205 .post(url)
206 .header(AUTHORIZATION, &self.auth_header()?)
207 .json(&TokenRequestBody {
208 client_parameters,
209 room_parameters,
210 })
211 .send()
212 .await?;
213
214 Self::parse_api_response(response).await
215 }
216
217 async fn parse_api_response<T, E>(response: Response) -> Result<T, Error<E>>
218 where
219 T: for<'de> Deserialize<'de>,
220 E: for<'de> Deserialize<'de>,
221 {
222 let status = response.status();
223 let body = response.bytes().await?;
224
225 if status.is_success() {
226 let result = serde_json::from_slice(&body).map_err(|_| {
227 tracing::error!(
228 "Received unexpected response from RoomServer: {}",
229 String::from_utf8_lossy(&body)
230 );
231 Error::Unexpected {
232 status,
233 body: String::from_utf8_lossy(&body).into(),
234 }
235 })?;
236 return Ok(result);
237 }
238
239 Err(Self::parse_api_error::<E>(status, &body))
240 }
241
242 fn parse_api_error<E>(status: StatusCode, body: &Bytes) -> Error<E>
243 where
244 E: for<'de> Deserialize<'de>,
245 {
246 match serde_json::from_slice::<ApiError<E>>(body) {
247 Ok(err) => Error::ApiError(err),
248 Err(_) => {
249 tracing::error!(
250 "Received unexpected error response from RoomServer: {}",
251 String::from_utf8_lossy(body)
252 );
253 Error::Unexpected {
254 status,
255 body: String::from_utf8_lossy(body).into(),
256 }
257 }
258 }
259 }
260
261 #[tracing::instrument(skip_all)]
262 pub async fn open_signaling_connection(
263 &self,
264 url: Url,
265 token: Token,
266 ) -> Result<SignalingConnection, SignalingError> {
267 SignalingConnection::connect(url, token).await
268 }
269}