1use std::borrow::Cow;
23
24use serde::{Deserialize, Deserializer, Serialize, de};
25use serde_json::Value;
26
27pub type JsonPayload = Value;
33
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
47#[serde(untagged)]
48pub enum UserId {
49 Integer(i64),
50 String(#[serde(deserialize_with = "UserId::deserialize_string")] String),
51}
52
53impl From<i64> for UserId {
54 fn from(value: i64) -> Self {
55 Self::Integer(value)
56 }
57}
58
59impl From<&str> for UserId {
60 fn from(value: &str) -> Self {
61 Self::String(value.to_owned())
62 }
63}
64
65impl From<String> for UserId {
66 fn from(value: String) -> Self {
67 Self::String(value)
68 }
69}
70
71impl UserId {
72 pub const MAX_STRING_BYTES: usize = 256;
74
75 fn deserialize_string<'de, D>(deserializer: D) -> Result<String, D::Error>
76 where
77 D: Deserializer<'de>,
78 {
79 let value = String::deserialize(deserializer)?;
80 if value.len() > Self::MAX_STRING_BYTES {
82 return Err(de::Error::custom("user identity exceeds 256 UTF-8 bytes"));
83 }
84 Ok(value)
85 }
86
87 #[must_use]
90 pub fn path_segment(&self) -> Cow<'_, str> {
91 match self {
92 Self::Integer(value) => Cow::Owned(value.to_string()),
93 Self::String(value) => Cow::Borrowed(value),
94 }
95 }
96
97 #[must_use]
104 pub fn normalized_for_runtime(self) -> Self {
105 match self {
106 Self::String(value) => value
107 .parse::<i64>()
108 .map_or(Self::String(value), Self::Integer),
109 Self::Integer(value) => Self::Integer(value),
110 }
111 }
112
113 #[must_use]
118 pub fn runtime_normalized(&self) -> Self {
119 self.clone().normalized_for_runtime()
120 }
121}
122
123#[expect(
130 clippy::struct_excessive_bools,
131 reason = "feature flags mirror the compatibility startup surface with explicit optional room capabilities"
132)]
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct AvailableFeatures {
136 pub rtc: bool,
138 pub transcription: bool,
139 pub audio_recording: bool,
140 pub video_recording: bool,
141}
142
143#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct RecordingState {
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub recording: Option<bool>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub audio: Option<bool>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub transcription: Option<bool>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub video: Option<bool>,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168pub enum StopCode {
169 #[serde(rename = "user_request")]
170 UserRequest,
171 #[serde(rename = "channel_closed")]
172 ChannelClosed,
173 #[serde(rename = "recording_timeout")]
174 RecordingTimeout,
175 #[serde(rename = "recording_failed")]
176 RecordingFailed,
177 #[serde(rename = "disk_space_exhausted")]
178 DiskSpaceExhausted,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct RecordingStateUpdate {
187 pub state: RecordingState,
188 #[serde(rename = "stopCode", skip_serializing_if = "Option::is_none")]
190 pub stop_code: Option<StopCode>,
191}
192
193#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct UserPermissions {
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub transcription: Option<bool>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub audio_recording: Option<bool>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub video_recording: Option<bool>,
206}
207
208#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct UserInfo {
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub is_talking: Option<bool>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub is_featured: Option<bool>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub is_camera_on: Option<bool>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub is_screen_sharing_on: Option<bool>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub is_self_muted: Option<bool>,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub is_deaf: Option<bool>,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub is_raising_hand: Option<bool>,
232}
233
234impl UserInfo {
235 #[must_use]
241 pub fn snapshot_complete(self) -> Self {
242 Self {
243 is_talking: Some(self.is_talking.unwrap_or(false)),
244 is_featured: Some(self.is_featured.unwrap_or(false)),
245 is_camera_on: Some(self.is_camera_on.unwrap_or(false)),
246 is_screen_sharing_on: Some(self.is_screen_sharing_on.unwrap_or(false)),
247 is_self_muted: Some(self.is_self_muted.unwrap_or(false)),
248 is_deaf: Some(self.is_deaf.unwrap_or(false)),
249 is_raising_hand: Some(self.is_raising_hand.unwrap_or(false)),
250 }
251 }
252
253 pub fn apply_partial_update(&mut self, update: &Self) {
258 *self = Self {
259 is_talking: update.is_talking.or(self.is_talking),
260 is_featured: update.is_featured.or(self.is_featured),
261 is_camera_on: update.is_camera_on.or(self.is_camera_on),
262 is_screen_sharing_on: update.is_screen_sharing_on.or(self.is_screen_sharing_on),
263 is_self_muted: update.is_self_muted.or(self.is_self_muted),
264 is_deaf: update.is_deaf.or(self.is_deaf),
265 is_raising_hand: update.is_raising_hand.or(self.is_raising_hand),
266 };
267 }
268
269 #[must_use]
271 pub fn with_featured(mut self, is_featured: Option<bool>) -> Self {
272 self.is_featured = is_featured;
273 self
274 }
275}
276
277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283pub struct PeerSnapshot {
284 #[serde(rename = "sessionId")]
285 pub user_id: UserId,
286 #[serde(default)]
287 pub info: UserInfo,
288}
289
290#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
296pub struct DownloadStates {
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub audio: Option<bool>,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub camera: Option<bool>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub screen: Option<bool>,
303 #[serde(rename = "cameraLayout", skip_serializing_if = "Option::is_none")]
304 pub camera_layout: Option<VideoLayoutIntent>,
305 #[serde(rename = "screenLayout", skip_serializing_if = "Option::is_none")]
306 pub screen_layout: Option<VideoLayoutIntent>,
307}
308
309impl DownloadStates {
310 pub fn apply_partial_update(&mut self, update: &Self) {
311 *self = Self {
312 audio: update.audio.or(self.audio),
313 camera: update.camera.or(self.camera),
314 screen: update.screen.or(self.screen),
315 camera_layout: update.camera_layout.or(self.camera_layout),
316 screen_layout: update.screen_layout.or(self.screen_layout),
317 };
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "snake_case")]
328pub enum VideoLayoutIntent {
329 Featured,
331 Pinned,
333 VisibleThumbnail,
335 Hidden,
337 Overflow,
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
350pub enum StreamType {
351 #[serde(rename = "audio")]
352 Audio,
353 #[serde(rename = "camera")]
354 Camera,
355 #[serde(rename = "screen")]
356 Screen,
357}
358
359#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
365pub struct RecordingOptions {
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub audio: Option<bool>,
368 #[serde(skip_serializing_if = "Option::is_none")]
369 pub transcription: Option<bool>,
370 #[serde(skip_serializing_if = "Option::is_none")]
371 pub video: Option<bool>,
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380#[repr(u16)]
381pub enum WebSocketCloseCode {
382 Clean = 1000,
384 Leaving = 1001,
386 ProtocolError = 1002,
388 Error = 1011,
390
391 AuthFailed = 4106,
393 AuthTimeout = 4107,
395 Kicked = 4108,
397 RoomFull = 4109,
399 Overloaded = 4110,
401}
402
403impl WebSocketCloseCode {
404 #[must_use]
410 pub const fn from_u16(value: u16) -> Option<Self> {
411 match value {
412 1000 => Some(Self::Clean),
413 1001 => Some(Self::Leaving),
414 1002 => Some(Self::ProtocolError),
415 1011 => Some(Self::Error),
416 4106 => Some(Self::AuthFailed),
417 4107 => Some(Self::AuthTimeout),
418 4108 => Some(Self::Kicked),
419 4109 => Some(Self::RoomFull),
420 4110 => Some(Self::Overloaded),
421 _ => None,
422 }
423 }
424}
425
426impl From<WebSocketCloseCode> for u16 {
427 fn from(value: WebSocketCloseCode) -> Self {
428 match value {
429 WebSocketCloseCode::Clean => 1000,
430 WebSocketCloseCode::Leaving => 1001,
431 WebSocketCloseCode::ProtocolError => 1002,
432 WebSocketCloseCode::Error => 1011,
433 WebSocketCloseCode::AuthFailed => 4106,
434 WebSocketCloseCode::AuthTimeout => 4107,
435 WebSocketCloseCode::Kicked => 4108,
436 WebSocketCloseCode::RoomFull => 4109,
437 WebSocketCloseCode::Overloaded => 4110,
438 }
439 }
440}
441
442#[cfg(test)]
443#[path = "TESTS/lib.rs"]
444mod tests;