Skip to main content

opentalk_roomserver_client/
lib.rs

1// SPDX-License-Identifier: EUPL-1.2
2// SPDX-FileCopyrightText: OpenTalk Team <mail@opentalk.eu>
3
4//! # RoomServer API Requests
5//!
6//! This crate provides the API requests to interact with the roomserver.
7
8use 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(&parameters)
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(&quota)
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}