Skip to main content

opentalk_roomserver_client/api/
command.rs

1// SPDX-License-Identifier: EUPL-1.2
2// SPDX-FileCopyrightText: OpenTalk Team <mail@opentalk.eu>
3
4use opentalk_roomserver_types_recording::{RECORDING_MODULE_ID, command::RecordingCommand};
5use opentalk_roomserver_types_transcription::{
6    TRANSCRIPTION_MODULE_ID, command::TranscriptionCommand,
7};
8use opentalk_types_common::modules::ModuleId;
9use serde::{Deserialize, Serialize};
10// reexport commands for easier usage
11pub use {
12    opentalk_roomserver_types::{
13        breakout::{BREAKOUT_MODULE_ID, command::BreakoutCommand},
14        core::{CORE_MODULE_ID, CoreCommand},
15    },
16    opentalk_roomserver_types_automod::{AUTOMOD_MODULE_ID, command::AutomodCommand},
17    opentalk_roomserver_types_chat::{CHAT_MODULE_ID, command::ChatCommand},
18    opentalk_roomserver_types_e2ee::{E2EE_MODULE_ID, E2eeCommand},
19    opentalk_roomserver_types_echo::{ECHO_MODULE_ID, command::EchoCommand},
20    opentalk_roomserver_types_excalidraw::{EXCALIDRAW_MODULE_ID, ExcalidrawCommand},
21    opentalk_roomserver_types_legal_vote::{LEGAL_VOTE_MODULE_ID, LegalVoteCommand},
22    opentalk_roomserver_types_livekit::{
23        LIVEKIT_MODULE_ID, LiveKitCommand, MicrophoneRestrictionState,
24    },
25    opentalk_roomserver_types_meeting_notes::{MEETING_NOTES_MODULE_ID, MeetingNotesCommand},
26    opentalk_roomserver_types_meeting_report::{
27        MEETING_REPORT_MODULE_ID, command::MeetingReportCommand,
28    },
29    opentalk_roomserver_types_moderation::{MODERATION_MODULE_ID, command::ModerationCommand},
30    opentalk_roomserver_types_polls::{POLLS_MODULE_ID, command::PollsCommand},
31    opentalk_roomserver_types_raise_hands::{RAISE_HANDS_MODULE_ID, command::RaiseHandsCommand},
32    opentalk_roomserver_types_reaction::{REACTION_MODULE_ID, ReactionCommand},
33    opentalk_roomserver_types_shared_folder::{
34        SHARED_FOLDER_MODULE_ID, command::SharedFolderCommand,
35    },
36    opentalk_roomserver_types_subroom_audio::{
37        SUBROOM_AUDIO_MODULE_ID, command::SubroomAudioCommand,
38    },
39    opentalk_roomserver_types_timer::{TIMER_MODULE_ID, TimerCommand},
40    opentalk_roomserver_types_training_participation_report::{
41        TRAINING_PARTICIPATION_REPORT_MODULE_ID, command::TrainingParticipationReportCommand,
42    },
43    opentalk_roomserver_types_whiteboard::{WHITEBOARD_MODULE_ID, WhiteboardCommand},
44};
45
46#[derive(Debug, Serialize, Deserialize)]
47pub struct SignalingCommand {
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub transaction_id: Option<u64>,
50    #[serde(flatten)]
51    pub payload: SignalingModuleCommand,
52}
53
54impl SignalingCommand {
55    pub fn namespace(&self) -> ModuleId {
56        self.payload.namespace()
57    }
58}
59
60impl From<SignalingModuleCommand> for SignalingCommand {
61    fn from(payload: SignalingModuleCommand) -> Self {
62        Self {
63            transaction_id: None,
64            payload,
65        }
66    }
67}
68
69#[derive(Debug, Serialize, Deserialize)]
70#[serde(tag = "namespace", content = "payload", rename_all = "snake_case")]
71pub enum SignalingModuleCommand {
72    Core(CoreCommand),
73    Automod(AutomodCommand),
74    Breakout(BreakoutCommand),
75    Echo(EchoCommand),
76    Chat(ChatCommand),
77
78    #[serde(rename = "livekit")]
79    LiveKit(LiveKitCommand),
80
81    E2ee(E2eeCommand),
82
83    Timer(TimerCommand),
84    Polls(PollsCommand),
85    SharedFolder(SharedFolderCommand),
86    MeetingReport(MeetingReportCommand),
87    Moderation(ModerationCommand),
88    RaiseHands(RaiseHandsCommand),
89    SubroomAudio(SubroomAudioCommand),
90    MeetingNotes(MeetingNotesCommand),
91    Whiteboard(WhiteboardCommand),
92    Excalidraw(ExcalidrawCommand),
93    LegalVote(LegalVoteCommand),
94    TrainingParticipationReport(TrainingParticipationReportCommand),
95    Recording(RecordingCommand),
96    Transcription(TranscriptionCommand),
97    Reaction(ReactionCommand),
98}
99
100impl SignalingModuleCommand {
101    pub fn namespace(&self) -> ModuleId {
102        match self {
103            Self::Core(..) => CORE_MODULE_ID,
104            Self::Automod(..) => AUTOMOD_MODULE_ID,
105            Self::Breakout(..) => BREAKOUT_MODULE_ID,
106            Self::Echo(..) => ECHO_MODULE_ID,
107            Self::Chat(..) => CHAT_MODULE_ID,
108            Self::LiveKit(..) => LIVEKIT_MODULE_ID,
109            Self::E2ee(..) => E2EE_MODULE_ID,
110            Self::Timer(..) => TIMER_MODULE_ID,
111            Self::Polls(..) => POLLS_MODULE_ID,
112            Self::SharedFolder(..) => SHARED_FOLDER_MODULE_ID,
113            Self::MeetingReport(..) => MEETING_REPORT_MODULE_ID,
114            Self::Moderation(..) => MODERATION_MODULE_ID,
115            Self::RaiseHands(..) => RAISE_HANDS_MODULE_ID,
116            Self::SubroomAudio(..) => SUBROOM_AUDIO_MODULE_ID,
117            Self::MeetingNotes(..) => MEETING_NOTES_MODULE_ID,
118            Self::Whiteboard(..) => WHITEBOARD_MODULE_ID,
119            Self::Excalidraw(..) => EXCALIDRAW_MODULE_ID,
120            Self::LegalVote(..) => LEGAL_VOTE_MODULE_ID,
121            Self::TrainingParticipationReport(..) => TRAINING_PARTICIPATION_REPORT_MODULE_ID,
122            Self::Recording(..) => RECORDING_MODULE_ID,
123            Self::Transcription(..) => TRANSCRIPTION_MODULE_ID,
124            Self::Reaction(..) => REACTION_MODULE_ID,
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use std::collections::BTreeSet;
132
133    use insta::assert_snapshot;
134    use opentalk_roomserver_types::{breakout::command::BreakoutCommand, core::CoreCommand};
135    use opentalk_roomserver_types_automod::command::AutomodCommand;
136    use opentalk_roomserver_types_chat::command::ChatCommand;
137    use opentalk_roomserver_types_echo::command::EchoCommand;
138    use opentalk_roomserver_types_excalidraw::ExcalidrawCommand;
139    use opentalk_roomserver_types_legal_vote::{
140        LegalVoteCommand, cancel::CustomCancelReason, vote::LegalVoteId,
141    };
142    use opentalk_roomserver_types_livekit::LiveKitCommand;
143    use opentalk_roomserver_types_meeting_notes::MeetingNotesCommand;
144    use opentalk_roomserver_types_meeting_report::command::MeetingReportCommand;
145    use opentalk_roomserver_types_moderation::command::ModerationCommand;
146    use opentalk_roomserver_types_polls::{
147        ChoiceId, PollId,
148        command::{Choices, PollsCommand, Vote},
149    };
150    use opentalk_roomserver_types_raise_hands::command::RaiseHandsCommand;
151    use opentalk_roomserver_types_reaction::ReactionCommand;
152    use opentalk_roomserver_types_recording::command::RecordingCommand;
153    use opentalk_roomserver_types_timer::{TimerCommand, command::Kind};
154    use opentalk_roomserver_types_training_participation_report::TrainingParticipationReportCommand;
155    use opentalk_roomserver_types_transcription::command::TranscriptionCommand;
156    use opentalk_roomserver_types_whiteboard::WhiteboardCommand;
157    use opentalk_types_common::modules::ModuleId;
158    use opentalk_types_signaling::ParticipantId;
159    use serde::Deserialize;
160
161    use super::SignalingModuleCommand;
162    use crate::api::command::SignalingCommand;
163
164    #[derive(Debug, Clone, Deserialize)]
165    pub struct NamespaceOnly {
166        pub namespace: ModuleId,
167    }
168
169    #[test]
170    fn serialize_command_core() {
171        let command = SignalingCommand {
172            transaction_id: None,
173            payload: SignalingModuleCommand::Core(CoreCommand::EnterRoom { display_name: None }),
174        };
175        let raw = serde_json::to_string_pretty(&command).unwrap();
176        assert_snapshot!(raw, @r#"
177        {
178          "namespace": "core",
179          "payload": {
180            "action": "enter_room"
181          }
182        }
183        "#);
184    }
185
186    #[test]
187    fn serialize_command_automod() {
188        let command = SignalingCommand {
189            transaction_id: None,
190            payload: SignalingModuleCommand::Automod(AutomodCommand::Stop),
191        };
192        let raw = serde_json::to_string_pretty(&command).unwrap();
193
194        assert_snapshot!(raw, @r#"
195        {
196          "namespace": "automod",
197          "payload": {
198            "action": "stop"
199          }
200        }
201        "#);
202    }
203
204    #[test]
205    fn serialize_command_breakout() {
206        let command = SignalingCommand {
207            transaction_id: None,
208            payload: SignalingModuleCommand::Breakout(BreakoutCommand::Stop { delay: None }),
209        };
210        let raw = serde_json::to_string_pretty(&command).unwrap();
211
212        assert_snapshot!(raw, @r#"
213        {
214          "namespace": "breakout",
215          "payload": {
216            "action": "stop"
217          }
218        }
219        "#);
220
221        // Check that the ModuleId from the actual SignalingModule matches what we serialize.
222        let namespace_only: NamespaceOnly = serde_json::from_str(&raw).unwrap();
223        assert_eq!(namespace_only.namespace, command.namespace());
224    }
225
226    #[test]
227    fn serialize_command_chat() {
228        let command = SignalingCommand {
229            transaction_id: None,
230            payload: SignalingModuleCommand::Chat(ChatCommand::DisableChat),
231        };
232        let raw = serde_json::to_string_pretty(&command).unwrap();
233
234        assert_snapshot!(raw, @r#"
235        {
236          "namespace": "chat",
237          "payload": {
238            "action": "disable_chat"
239          }
240        }
241        "#);
242
243        // Check that the ModuleId from the actual SignalingModule matches what we serialize.
244        let namespace_only: NamespaceOnly = serde_json::from_str(&raw).unwrap();
245        assert_eq!(namespace_only.namespace, command.namespace());
246    }
247
248    #[test]
249    fn serialize_command_echo() {
250        let command = SignalingCommand {
251            transaction_id: None,
252            payload: SignalingModuleCommand::Echo(EchoCommand::Ping),
253        };
254        let raw = serde_json::to_string_pretty(&command).unwrap();
255
256        assert_snapshot!(raw, @r#"
257        {
258          "namespace": "echo",
259          "payload": {
260            "action": "ping"
261          }
262        }
263        "#);
264
265        // Check that the ModuleId from the actual SignalingModule matches what we serialize.
266        let namespace_only: NamespaceOnly = serde_json::from_str(&raw).unwrap();
267        assert_eq!(namespace_only.namespace, command.namespace());
268    }
269
270    #[test]
271    fn serialize_command_livekit() {
272        let command = SignalingCommand {
273            transaction_id: None,
274            payload: SignalingModuleCommand::LiveKit(LiveKitCommand::CreateNewAccessToken),
275        };
276        let raw = serde_json::to_string_pretty(&command).unwrap();
277
278        assert_snapshot!(raw, @r#"
279        {
280          "namespace": "livekit",
281          "payload": {
282            "action": "create_new_access_token"
283          }
284        }
285        "#);
286
287        // Check that the ModuleId from the actual SignalingModule matches what we serialize.
288        let namespace_only: NamespaceOnly = serde_json::from_str(&raw).unwrap();
289        assert_eq!(namespace_only.namespace, command.namespace());
290    }
291
292    #[test]
293    fn serialize_command_timer() {
294        let command = SignalingCommand {
295            transaction_id: None,
296            payload: SignalingModuleCommand::Timer(TimerCommand::Start {
297                kind: Kind::Stopwatch,
298                style: None,
299                title: None,
300                enable_ready_check: false,
301            }),
302        };
303        let raw = serde_json::to_string_pretty(&command).unwrap();
304
305        assert_snapshot!(raw, @r#"
306        {
307          "namespace": "timer",
308          "payload": {
309            "action": "start",
310            "kind": "stopwatch",
311            "style": null,
312            "title": null,
313            "enable_ready_check": false
314          }
315        }
316        "#);
317
318        // Check that the ModuleId from the actual SignalingModule matches what we serialize.
319        let namespace_only: NamespaceOnly = serde_json::from_str(&raw).unwrap();
320        assert_eq!(namespace_only.namespace, command.namespace());
321    }
322
323    #[test]
324    fn serialize_command_polls() {
325        let command = SignalingCommand {
326            transaction_id: None,
327            payload: SignalingModuleCommand::Polls(PollsCommand::Vote(Vote {
328                poll_id: PollId::nil(),
329                choices: Choices::Single {
330                    choice_id: ChoiceId::from_u32(0),
331                },
332            })),
333        };
334        let raw = serde_json::to_string_pretty(&command).unwrap();
335
336        assert_snapshot!(raw, @r#"
337        {
338          "namespace": "polls",
339          "payload": {
340            "action": "vote",
341            "poll_id": "00000000-0000-0000-0000-000000000000",
342            "choice_id": 0
343          }
344        }
345        "#);
346
347        // Check that the ModuleId from the actual SignalingModule matches what we serialize.
348        let namespace_only: NamespaceOnly = serde_json::from_str(&raw).unwrap();
349        assert_eq!(namespace_only.namespace, command.namespace());
350    }
351
352    #[test]
353    fn serialize_command_meeting_report() {
354        let command = SignalingCommand {
355            transaction_id: None,
356            payload: SignalingModuleCommand::MeetingReport(
357                MeetingReportCommand::GenerateAttendanceReport {
358                    include_email_addresses: false,
359                },
360            ),
361        };
362        let raw = serde_json::to_string_pretty(&command).unwrap();
363
364        assert_snapshot!(raw, @r#"
365        {
366          "namespace": "meeting_report",
367          "payload": {
368            "action": "generate_attendance_report",
369            "include_email_addresses": false
370          }
371        }
372        "#);
373    }
374
375    #[test]
376    fn serialize_command_moderation() {
377        let command = SignalingCommand {
378            transaction_id: None,
379            payload: SignalingModuleCommand::Moderation(ModerationCommand::Accept {
380                target: ParticipantId::nil(),
381            }),
382        };
383        let raw = serde_json::to_string_pretty(&command).unwrap();
384
385        assert_snapshot!(raw, @r#"
386        {
387          "namespace": "moderation",
388          "payload": {
389            "action": "accept",
390            "target": "00000000-0000-0000-0000-000000000000"
391          }
392        }
393        "#);
394    }
395
396    #[test]
397    fn serialize_command_raise_hands() {
398        let command = SignalingCommand {
399            transaction_id: None,
400            payload: SignalingModuleCommand::RaiseHands(RaiseHandsCommand::RaiseHand),
401        };
402        let raw = serde_json::to_string_pretty(&command).unwrap();
403
404        assert_snapshot!(raw, @r#"
405        {
406          "namespace": "raise_hands",
407          "payload": {
408            "action": "raise_hand"
409          }
410        }
411        "#);
412    }
413
414    #[test]
415    fn serialize_command_meeting_notes() {
416        let command = SignalingCommand {
417            transaction_id: None,
418            payload: SignalingModuleCommand::MeetingNotes(MeetingNotesCommand::GrantWriteAccess {
419                participant_ids: BTreeSet::new(),
420            }),
421        };
422        let raw = serde_json::to_string_pretty(&command).unwrap();
423
424        assert_snapshot!(raw, @r#"
425        {
426          "namespace": "meeting_notes",
427          "payload": {
428            "action": "grant_write_access",
429            "participant_ids": []
430          }
431        }
432        "#);
433    }
434
435    #[test]
436    fn serialize_command_whiteboard() {
437        let command = SignalingCommand {
438            transaction_id: None,
439            payload: SignalingModuleCommand::Whiteboard(WhiteboardCommand::Initialize),
440        };
441        let raw = serde_json::to_string_pretty(&command).unwrap();
442
443        assert_snapshot!(raw, @r#"
444        {
445          "namespace": "whiteboard",
446          "payload": {
447            "action": "initialize"
448          }
449        }
450        "#);
451    }
452
453    #[test]
454    fn serialize_command_excalidraw() {
455        let command = SignalingCommand {
456            transaction_id: None,
457            payload: SignalingModuleCommand::Excalidraw(ExcalidrawCommand::Follow {
458                participant_id: ParticipantId::nil(),
459            }),
460        };
461        let raw = serde_json::to_string_pretty(&command).unwrap();
462
463        assert_snapshot!(raw, @r#"
464        {
465          "namespace": "excalidraw",
466          "payload": {
467            "action": "follow",
468            "participant_id": "00000000-0000-0000-0000-000000000000"
469          }
470        }
471        "#);
472    }
473
474    #[test]
475    fn serialize_command_legal_vote() {
476        let command = SignalingCommand {
477            transaction_id: None,
478            payload: SignalingModuleCommand::LegalVote(LegalVoteCommand::Cancel {
479                legal_vote_id: LegalVoteId::from_u128(1),
480                reason: CustomCancelReason::try_from("Test Reason").unwrap(),
481            }),
482        };
483        let raw = serde_json::to_string_pretty(&command).unwrap();
484
485        assert_snapshot!(raw, @r#"
486        {
487          "namespace": "legal_vote",
488          "payload": {
489            "action": "cancel",
490            "legal_vote_id": "00000000-0000-0000-0000-000000000001",
491            "reason": "Test Reason"
492          }
493        }
494        "#);
495    }
496
497    #[test]
498    fn serialize_training_participation_report() {
499        let command = SignalingCommand {
500            transaction_id: None,
501            payload: SignalingModuleCommand::TrainingParticipationReport(
502                TrainingParticipationReportCommand::ConfirmPresence,
503            ),
504        };
505        let raw = serde_json::to_string_pretty(&command).unwrap();
506
507        assert_snapshot!(raw, @r#"
508        {
509          "namespace": "training_participation_report",
510          "payload": {
511            "action": "confirm_presence"
512          }
513        }
514        "#);
515    }
516
517    #[test]
518    fn serialize_recording() {
519        let command = SignalingCommand {
520            transaction_id: None,
521            payload: SignalingModuleCommand::Recording(RecordingCommand::StartRecording),
522        };
523        let raw = serde_json::to_string_pretty(&command).unwrap();
524
525        assert_snapshot!(raw, @r#"
526        {
527          "namespace": "recording",
528          "payload": {
529            "action": "start_recording"
530          }
531        }
532        "#);
533    }
534
535    #[test]
536    fn serialize_transcription() {
537        let command = SignalingCommand {
538            transaction_id: None,
539            payload: SignalingModuleCommand::Transcription(TranscriptionCommand::Start {
540                language: None,
541            }),
542        };
543
544        let raw = serde_json::to_string_pretty(&command).unwrap();
545
546        assert_snapshot!(raw, @r#"
547        {
548          "namespace": "transcription",
549          "payload": {
550            "action": "start"
551          }
552        }
553        "#);
554    }
555
556    #[test]
557    fn serialize_reaction() {
558        let command = SignalingCommand {
559            transaction_id: None,
560            payload: SignalingModuleCommand::Reaction(ReactionCommand::React {
561                reaction: opentalk_roomserver_types_reaction::Reaction::ThumbsUp,
562            }),
563        };
564
565        let raw = serde_json::to_string_pretty(&command).unwrap();
566
567        assert_snapshot!(raw, @r#"
568        {
569          "namespace": "reaction",
570          "payload": {
571            "action": "react",
572            "reaction": "thumbs_up"
573          }
574        }
575        "#);
576    }
577}