1use alloc::collections::BTreeMap;
22use alloc::string::{String, ToString};
23use alloc::vec::Vec;
24use core::fmt::Write as _;
25
26use obby_proto::{CaseFolded, Casemapping};
27
28use crate::json::{Json, field_string};
29
30impl Json {
35 fn write(&self, out: &mut String) {
36 match self {
37 Json::Null => out.push_str("null"),
38 Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
39 Json::Num(n) => {
40 let _ = write!(out, "{n}");
41 }
42 Json::Str(s) => write_json_string(s, out),
43 Json::Array(items) => write_json_array(items, out),
44 Json::Object(fields) => write_json_object(fields, out),
45 }
46 }
47
48 fn as_u32(&self) -> Option<u32> {
49 match self {
50 Json::Num(n) => u32::try_from(*n).ok(),
51 _ => None,
52 }
53 }
54}
55
56fn write_json_array(items: &[Json], out: &mut String) {
57 out.push('[');
58 for (i, item) in items.iter().enumerate() {
59 if i > 0 {
60 out.push(',');
61 }
62 item.write(out);
63 }
64 out.push(']');
65}
66
67fn write_json_object(fields: &[(String, Json)], out: &mut String) {
68 out.push('{');
69 for (i, (key, value)) in fields.iter().enumerate() {
70 if i > 0 {
71 out.push(',');
72 }
73 write_json_string(key, out);
74 out.push(':');
75 value.write(out);
76 }
77 out.push('}');
78}
79
80fn write_json_string(s: &str, out: &mut String) {
81 out.push('"');
82 for c in s.chars() {
83 match c {
84 '"' => out.push_str("\\\""),
85 '\\' => out.push_str("\\\\"),
86 '\n' => out.push_str("\\n"),
87 '\r' => out.push_str("\\r"),
88 '\t' => out.push_str("\\t"),
89 c if (u32::from(c)) < 0x20 => {
90 let _ = write!(out, "\\u{:04x}", u32::from(c));
91 }
92 c => out.push(c),
93 }
94 }
95 out.push('"');
96}
97
98fn obj(pairs: Vec<(&str, Json)>) -> Json {
99 Json::Object(
100 pairs
101 .into_iter()
102 .map(|(key, value)| (key.to_string(), value))
103 .collect(),
104 )
105}
106
107fn opt_field(fields: &mut Vec<(String, Json)>, key: &str, value: Option<Json>) {
108 if let Some(value) = value {
109 fields.push((key.to_string(), value));
110 }
111}
112
113fn strings_json(items: &[String]) -> Json {
114 Json::Array(items.iter().map(|s| Json::Str(s.clone())).collect())
115}
116
117fn strings(value: &Json) -> Option<Vec<String>> {
119 value
120 .as_array()?
121 .iter()
122 .map(|item| item.as_str().map(str::to_string))
123 .collect()
124}
125
126fn field_string_opt(value: &Json, key: &str) -> Option<String> {
127 value.field(key).and_then(Json::as_str).map(str::to_string)
128}
129
130fn field_u32(value: &Json, key: &str) -> Option<u32> {
131 value.field(key)?.as_u32()
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
142#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
143pub enum OnOff {
144 On,
146 Off,
148}
149
150impl OnOff {
151 fn wire(self) -> &'static str {
152 match self {
153 OnOff::On => "on",
154 OnOff::Off => "off",
155 }
156 }
157
158 fn parse(text: &str) -> Option<Self> {
159 match text {
160 "on" => Some(OnOff::On),
161 "off" => Some(OnOff::Off),
162 _ => None,
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
170#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
171#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
172#[cfg_attr(feature = "ts", ts(rename = "VoiceRole"))]
173pub enum Role {
174 Publisher,
176 Viewer,
178}
179
180impl Role {
181 fn wire(self) -> &'static str {
182 match self {
183 Role::Publisher => "streamer",
184 Role::Viewer => "viewer",
185 }
186 }
187
188 fn parse(text: &str) -> Option<Self> {
189 match text {
190 "streamer" => Some(Role::Publisher),
191 "viewer" => Some(Role::Viewer),
192 _ => None,
193 }
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
201#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
202#[cfg_attr(feature = "ts", ts(rename = "VoiceRoomKind"))]
203pub enum RoomKind {
204 Publish,
206 Stream,
208}
209
210impl RoomKind {
211 fn wire(self) -> &'static str {
212 match self {
213 RoomKind::Publish => "voice",
214 RoomKind::Stream => "stream",
215 }
216 }
217
218 fn parse(text: &str) -> Option<Self> {
219 match text {
220 "voice" => Some(RoomKind::Publish),
221 "stream" => Some(RoomKind::Stream),
222 _ => None,
223 }
224 }
225
226 fn for_channel(channel: &str) -> Self {
228 if channel.starts_with('$') {
229 RoomKind::Stream
230 } else {
231 RoomKind::Publish
232 }
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
239#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
240#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
241#[cfg_attr(feature = "ts", ts(rename = "VoiceToggle"))]
242pub enum ToggleKind {
243 Mic,
245 Video,
247 Screen,
249 Hand,
251}
252
253impl ToggleKind {
254 fn wire(self) -> &'static str {
255 match self {
256 ToggleKind::Mic => "mic",
257 ToggleKind::Video => "video",
258 ToggleKind::Screen => "screen",
259 ToggleKind::Hand => "hand",
260 }
261 }
262
263 fn parse(text: &str) -> Option<Self> {
264 match text {
265 "mic" => Some(ToggleKind::Mic),
266 "video" => Some(ToggleKind::Video),
267 "screen" => Some(ToggleKind::Screen),
268 "hand" => Some(ToggleKind::Hand),
269 _ => None,
270 }
271 }
272
273 fn apply(self, participant: &mut Participant, state: OnOff) {
274 match self {
275 ToggleKind::Mic => participant.mic = state,
276 ToggleKind::Video => participant.video = state,
277 ToggleKind::Screen => participant.screen = state,
278 ToggleKind::Hand => participant.hand = state,
279 }
280 }
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
290#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
291#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
292#[cfg_attr(feature = "ts", ts(rename = "VoicePresence"))]
293pub enum PresenceState {
294 Joined,
296 Left,
298 On,
300 Off,
302 Speaking,
304 Silent,
306 DeafOn,
308 DeafOff,
310}
311
312impl PresenceState {
313 fn wire(self) -> &'static str {
314 match self {
315 PresenceState::Joined => "joined",
316 PresenceState::Left => "left",
317 PresenceState::On => "on",
318 PresenceState::Off => "off",
319 PresenceState::Speaking => "speaking",
320 PresenceState::Silent => "silent",
321 PresenceState::DeafOn => "deaf-on",
322 PresenceState::DeafOff => "deaf-off",
323 }
324 }
325
326 fn parse(text: &str) -> Option<Self> {
327 Some(match text {
328 "joined" => PresenceState::Joined,
329 "left" => PresenceState::Left,
330 "on" => PresenceState::On,
331 "off" => PresenceState::Off,
332 "speaking" => PresenceState::Speaking,
333 "silent" => PresenceState::Silent,
334 "deaf-on" => PresenceState::DeafOn,
335 "deaf-off" => PresenceState::DeafOff,
336 _ => return None,
337 })
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq)]
351#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
352#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
353#[cfg_attr(feature = "ts", ts(rename = "VoiceTrackHint"))]
354pub struct TrackHint {
355 pub mid: String,
357 pub member: String,
359}
360
361impl TrackHint {
362 fn to_json(&self) -> Json {
363 obj(alloc::vec![
364 ("mid", Json::Str(self.mid.clone())),
365 ("member", Json::Str(self.member.clone())),
366 ])
367 }
368
369 fn from_json(value: &Json) -> Option<Self> {
370 Some(Self {
371 mid: field_string(value, "mid")?,
372 member: field_string(value, "member")?,
373 })
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
385#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
386#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
387pub struct TurnCredentials {
388 pub urls: Vec<String>,
390 pub username: String,
392 pub password: String,
394}
395
396impl TurnCredentials {
397 fn to_json(&self) -> Json {
398 obj(alloc::vec![
399 ("urls", strings_json(&self.urls)),
400 ("username", Json::Str(self.username.clone())),
401 ("password", Json::Str(self.password.clone())),
402 ])
403 }
404
405 fn from_json(value: &Json) -> Option<Self> {
406 let urls = match value.field("urls")? {
407 Json::Str(one) => alloc::vec![one.clone()],
408 many @ Json::Array(_) => strings(many)?,
409 _ => return None,
410 };
411 Some(Self {
412 urls,
413 username: field_string(value, "username")?,
414 password: field_string(value, "password")?,
415 })
416 }
417}
418
419#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
421#[derive(Debug, Clone, PartialEq, Eq)]
422#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
423pub struct ChunkMeta {
424 pub id: String,
426 pub seq: u32,
428 pub total: u32,
430}
431
432fn decode_chunk(value: &Json) -> Option<ChunkMeta> {
433 Some(ChunkMeta {
434 id: field_string_opt(value, "id")?,
435 seq: field_u32(value, "seq")?,
436 total: field_u32(value, "total")?,
437 })
438}
439
440fn chunk_meta_fields(chunk: &ChunkMeta) -> Vec<(String, Json)> {
441 alloc::vec![
442 ("id".to_string(), Json::Str(chunk.id.clone())),
443 ("seq".to_string(), Json::Num(i64::from(chunk.seq))),
444 ("total".to_string(), Json::Num(i64::from(chunk.total))),
445 ]
446}
447
448#[derive(Debug, Clone, PartialEq, Eq)]
461#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
462#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
463#[cfg_attr(feature = "ts", ts(rename = "VoiceSignal"))]
464#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))]
465#[non_exhaustive]
466pub enum Signal {
467 Join {
469 channel: String,
471 },
472 Leave {
478 channel: String,
480 },
481 Joined {
483 members: Vec<String>,
485 mode: Option<RoomKind>,
487 role: Option<Role>,
489 streamers: Option<Vec<String>>,
491 turn: Option<TurnCredentials>,
493 tracks: Option<Vec<TrackHint>>,
495 },
496 Offer {
498 sdp: String,
500 tracks: Option<Vec<TrackHint>>,
502 chunk: Option<ChunkMeta>,
504 },
505 Answer {
507 sdp: String,
509 chunk: Option<ChunkMeta>,
511 },
512 Ice {
514 cand: String,
516 mid: Option<String>,
518 mlineidx: Option<u32>,
520 },
521 Presence {
523 member: String,
525 state: PresenceState,
527 kind: Option<ToggleKind>,
529 role: Option<Role>,
531 },
532 Mic {
534 state: OnOff,
536 },
537 Video {
539 state: OnOff,
541 },
542 Screen {
544 state: OnOff,
546 },
547 Hand {
549 state: OnOff,
551 },
552 Deaf {
558 state: OnOff,
560 },
561 Speaking,
563 Silent,
565 React {
568 member: Option<String>,
570 emoji: String,
572 },
573 Promote {
579 target: String,
581 },
582 Demote {
584 target: String,
586 },
587 Role {
589 member: String,
591 role: Role,
593 },
594 Error {
596 error: Option<String>,
598 },
599}
600
601impl Signal {
602 pub fn to_json(&self) -> String {
604 let mut out = String::new();
605 self.to_json_value().write(&mut out);
606 out
607 }
608
609 pub fn from_json(text: &str) -> Option<Self> {
614 Self::from_value(&Json::parse(text)?)
615 }
616
617 fn to_json_value(&self) -> Json {
618 let (kind, mut fields) = self.wire_fields();
619 fields.insert(0, ("type".to_string(), Json::Str(kind.to_string())));
620 Json::Object(fields)
621 }
622
623 fn wire_fields(&self) -> (&'static str, Vec<(String, Json)>) {
624 match self {
625 Signal::Join { channel } => ("join", channel_fields(channel)),
626 Signal::Leave { channel } => ("leave", channel_fields(channel)),
627 Signal::Joined {
628 members,
629 mode,
630 role,
631 streamers,
632 turn,
633 tracks,
634 } => (
635 "joined",
636 joined_fields(
637 members,
638 *mode,
639 *role,
640 streamers.as_deref(),
641 turn.as_ref(),
642 tracks.as_deref(),
643 ),
644 ),
645 Signal::Offer { sdp, tracks, chunk } => (
646 "offer",
647 offer_fields(sdp, tracks.as_deref(), chunk.as_ref()),
648 ),
649 Signal::Answer { sdp, chunk } => ("answer", answer_fields(sdp, chunk.as_ref())),
650 Signal::Ice {
651 cand,
652 mid,
653 mlineidx,
654 } => ("ice", ice_fields(cand, mid.as_deref(), *mlineidx)),
655 Signal::Presence {
656 member,
657 state,
658 kind,
659 role,
660 } => ("presence", presence_fields(member, *state, *kind, *role)),
661 Signal::Mic { state } => ("mic", state_field(*state)),
662 Signal::Video { state } => ("video", state_field(*state)),
663 Signal::Screen { state } => ("screen", state_field(*state)),
664 Signal::Hand { state } => ("hand", state_field(*state)),
665 Signal::Deaf { state } => ("deaf", state_field(*state)),
666 Signal::Speaking => ("speaking", literal_state_field("speaking")),
667 Signal::Silent => ("silent", literal_state_field("silent")),
668 Signal::React { member, emoji } => ("react", react_fields(member.as_deref(), emoji)),
669 Signal::Promote { target } => ("promote", target_fields(target)),
670 Signal::Demote { target } => ("demote", target_fields(target)),
671 Signal::Role { member, role } => ("role", role_fields(member, *role)),
672 Signal::Error { error } => ("error", error_fields(error.as_deref())),
673 }
674 }
675
676 fn from_value(value: &Json) -> Option<Self> {
677 let kind = value.field("type")?.as_str()?;
678 match kind {
679 "join" => Some(Signal::Join {
680 channel: field_string(value, "channel")?,
681 }),
682 "leave" => Some(Signal::Leave {
683 channel: field_string(value, "channel")?,
684 }),
685 "joined" => decode_joined(value),
686 "offer" => decode_offer(value),
687 "answer" => decode_answer(value),
688 "ice" => decode_ice(value),
689 "presence" => decode_presence(value),
690 "mic" => Some(Signal::Mic {
691 state: decode_on_off(value)?,
692 }),
693 "video" => Some(Signal::Video {
694 state: decode_on_off(value)?,
695 }),
696 "screen" => Some(Signal::Screen {
697 state: decode_on_off(value)?,
698 }),
699 "hand" => Some(Signal::Hand {
700 state: decode_on_off(value)?,
701 }),
702 "deaf" => Some(Signal::Deaf {
703 state: decode_on_off(value)?,
704 }),
705 "speaking" => Some(Signal::Speaking),
706 "silent" => Some(Signal::Silent),
707 "react" => decode_react(value),
708 "promote" => Some(Signal::Promote {
709 target: field_string(value, "target")?,
710 }),
711 "demote" => Some(Signal::Demote {
712 target: field_string(value, "target")?,
713 }),
714 "role" => decode_role(value),
715 "error" => Some(Signal::Error {
716 error: field_string_opt(value, "error"),
717 }),
718 _ => None,
719 }
720 }
721}
722
723fn channel_fields(channel: &str) -> Vec<(String, Json)> {
724 alloc::vec![("channel".to_string(), Json::Str(channel.to_string()))]
725}
726
727fn target_fields(target: &str) -> Vec<(String, Json)> {
728 alloc::vec![("target".to_string(), Json::Str(target.to_string()))]
729}
730
731fn state_field(state: OnOff) -> Vec<(String, Json)> {
732 alloc::vec![("state".to_string(), Json::Str(state.wire().to_string()))]
733}
734
735fn literal_state_field(state: &str) -> Vec<(String, Json)> {
736 alloc::vec![("state".to_string(), Json::Str(state.to_string()))]
737}
738
739fn role_fields(member: &str, role: Role) -> Vec<(String, Json)> {
740 alloc::vec![
741 ("member".to_string(), Json::Str(member.to_string())),
742 ("role".to_string(), Json::Str(role.wire().to_string())),
743 ]
744}
745
746fn react_fields(member: Option<&str>, emoji: &str) -> Vec<(String, Json)> {
747 let mut fields = Vec::new();
748 if let Some(member) = member {
749 fields.push(("member".to_string(), Json::Str(member.to_string())));
750 }
751 fields.push(("emoji".to_string(), Json::Str(emoji.to_string())));
752 fields
753}
754
755fn ice_fields(cand: &str, mid: Option<&str>, mlineidx: Option<u32>) -> Vec<(String, Json)> {
756 let mut fields = alloc::vec![("cand".to_string(), Json::Str(cand.to_string()))];
757 opt_field(&mut fields, "mid", mid.map(|m| Json::Str(m.to_string())));
758 opt_field(
759 &mut fields,
760 "mlineidx",
761 mlineidx.map(|n| Json::Num(i64::from(n))),
762 );
763 fields
764}
765
766fn presence_fields(
767 member: &str,
768 state: PresenceState,
769 kind: Option<ToggleKind>,
770 role: Option<Role>,
771) -> Vec<(String, Json)> {
772 let mut fields = alloc::vec![
773 ("member".to_string(), Json::Str(member.to_string())),
774 ("state".to_string(), Json::Str(state.wire().to_string())),
775 ];
776 opt_field(
777 &mut fields,
778 "kind",
779 kind.map(|k| Json::Str(k.wire().to_string())),
780 );
781 opt_field(
782 &mut fields,
783 "role",
784 role.map(|r| Json::Str(r.wire().to_string())),
785 );
786 fields
787}
788
789fn offer_fields(
790 sdp: &str,
791 tracks: Option<&[TrackHint]>,
792 chunk: Option<&ChunkMeta>,
793) -> Vec<(String, Json)> {
794 let mut fields = alloc::vec![("sdp".to_string(), Json::Str(sdp.to_string()))];
795 opt_field(
796 &mut fields,
797 "tracks",
798 tracks.map(|t| Json::Array(t.iter().map(TrackHint::to_json).collect())),
799 );
800 if let Some(chunk) = chunk {
801 fields.extend(chunk_meta_fields(chunk));
802 }
803 fields
804}
805
806fn answer_fields(sdp: &str, chunk: Option<&ChunkMeta>) -> Vec<(String, Json)> {
807 let mut fields = alloc::vec![("sdp".to_string(), Json::Str(sdp.to_string()))];
808 if let Some(chunk) = chunk {
809 fields.extend(chunk_meta_fields(chunk));
810 }
811 fields
812}
813
814fn joined_fields(
815 members: &[String],
816 mode: Option<RoomKind>,
817 role: Option<Role>,
818 streamers: Option<&[String]>,
819 turn: Option<&TurnCredentials>,
820 tracks: Option<&[TrackHint]>,
821) -> Vec<(String, Json)> {
822 let mut fields = alloc::vec![("members".to_string(), strings_json(members))];
823 opt_field(
824 &mut fields,
825 "mode",
826 mode.map(|m| Json::Str(m.wire().to_string())),
827 );
828 opt_field(
829 &mut fields,
830 "role",
831 role.map(|r| Json::Str(r.wire().to_string())),
832 );
833 opt_field(&mut fields, "streamers", streamers.map(strings_json));
834 opt_field(&mut fields, "turn", turn.map(TurnCredentials::to_json));
835 opt_field(
836 &mut fields,
837 "tracks",
838 tracks.map(|t| Json::Array(t.iter().map(TrackHint::to_json).collect())),
839 );
840 fields
841}
842
843fn error_fields(error: Option<&str>) -> Vec<(String, Json)> {
844 let mut fields = Vec::new();
845 opt_field(
846 &mut fields,
847 "error",
848 error.map(|e| Json::Str(e.to_string())),
849 );
850 fields
851}
852
853fn decode_on_off(value: &Json) -> Option<OnOff> {
854 OnOff::parse(&field_string(value, "state")?)
855}
856
857fn decode_joined(value: &Json) -> Option<Signal> {
858 let members = strings(value.field("members")?)?;
859 let mode = field_string_opt(value, "mode").and_then(|s| RoomKind::parse(&s));
860 let role = field_string_opt(value, "role").and_then(|s| Role::parse(&s));
861 let streamers = value.field("streamers").and_then(strings);
862 let turn = value.field("turn").and_then(TurnCredentials::from_json);
863 let tracks = value.field("tracks").and_then(decode_tracks);
864 Some(Signal::Joined {
865 members,
866 mode,
867 role,
868 streamers,
869 turn,
870 tracks,
871 })
872}
873
874fn decode_tracks(value: &Json) -> Option<Vec<TrackHint>> {
875 value.as_array()?.iter().map(TrackHint::from_json).collect()
876}
877
878fn decode_offer(value: &Json) -> Option<Signal> {
879 Some(Signal::Offer {
880 sdp: field_string(value, "sdp")?,
881 tracks: value.field("tracks").and_then(decode_tracks),
882 chunk: decode_chunk(value),
883 })
884}
885
886fn decode_answer(value: &Json) -> Option<Signal> {
887 Some(Signal::Answer {
888 sdp: field_string(value, "sdp")?,
889 chunk: decode_chunk(value),
890 })
891}
892
893fn decode_ice(value: &Json) -> Option<Signal> {
894 Some(Signal::Ice {
895 cand: field_string(value, "cand")?,
896 mid: field_string_opt(value, "mid"),
897 mlineidx: value.field("mlineidx").and_then(Json::as_u32),
898 })
899}
900
901fn decode_presence(value: &Json) -> Option<Signal> {
902 Some(Signal::Presence {
903 member: field_string(value, "member")?,
904 state: PresenceState::parse(&field_string(value, "state")?)?,
905 kind: field_string_opt(value, "kind").and_then(|s| ToggleKind::parse(&s)),
906 role: field_string_opt(value, "role").and_then(|s| Role::parse(&s)),
907 })
908}
909
910fn decode_react(value: &Json) -> Option<Signal> {
911 Some(Signal::React {
912 member: field_string_opt(value, "member"),
913 emoji: field_string(value, "emoji")?,
914 })
915}
916
917fn decode_role(value: &Json) -> Option<Signal> {
918 Some(Signal::Role {
919 member: field_string(value, "member")?,
920 role: Role::parse(&field_string(value, "role")?)?,
921 })
922}
923
924pub const DEFAULT_CHUNK_BUDGET: usize = 8191;
936
937#[derive(Debug, Clone, PartialEq, Eq)]
939#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
940pub struct SdpChunk {
941 pub chunk: ChunkMeta,
943 pub sdp: String,
945}
946
947pub fn split_sdp(sdp: &str, id: &str, budget: usize) -> Option<Vec<SdpChunk>> {
952 if budget == 0 || sdp.len() <= budget {
953 return None;
954 }
955 let slices = char_chunks(sdp, budget)?;
956 let total = u32::try_from(slices.len()).unwrap_or(u32::MAX);
957 Some(
958 slices
959 .into_iter()
960 .enumerate()
961 .map(|(seq, slice)| SdpChunk {
962 chunk: ChunkMeta {
963 id: id.to_string(),
964 seq: u32::try_from(seq).unwrap_or(u32::MAX),
965 total,
966 },
967 sdp: slice.to_string(),
968 })
969 .collect(),
970 )
971}
972
973fn char_chunks(s: &str, budget: usize) -> Option<Vec<&str>> {
979 let mut chunks = Vec::new();
980 let mut start = 0;
981 while start < s.len() {
982 let mut end = (start + budget).min(s.len());
983 while end > start && !s.is_char_boundary(end) {
984 end -= 1;
985 }
986 if end == start {
987 end = start + 1;
988 while end < s.len() && !s.is_char_boundary(end) {
989 end += 1;
990 }
991 }
992 chunks.push(s.get(start..end)?);
993 start = end;
994 }
995 Some(chunks)
996}
997
998pub const DEFAULT_MAX_CHUNKS_PER_REASSEMBLY: usize = 64;
1003
1004pub const DEFAULT_MAX_CONCURRENT_REASSEMBLIES: usize = 16;
1009
1010#[derive(Debug, Clone)]
1011#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1012struct PartialSdp {
1013 total: u32,
1014 parts: BTreeMap<u32, String>,
1015}
1016
1017#[derive(Debug, Clone)]
1019#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1020pub struct SdpReassembler {
1021 partials: BTreeMap<String, PartialSdp>,
1022 max_chunks: usize,
1023 max_concurrent: usize,
1024}
1025
1026impl SdpReassembler {
1027 pub fn new() -> Self {
1029 Self::with_bounds(
1030 DEFAULT_MAX_CHUNKS_PER_REASSEMBLY,
1031 DEFAULT_MAX_CONCURRENT_REASSEMBLIES,
1032 )
1033 }
1034
1035 pub fn with_bounds(max_chunks: usize, max_concurrent: usize) -> Self {
1037 Self {
1038 partials: BTreeMap::new(),
1039 max_chunks,
1040 max_concurrent,
1041 }
1042 }
1043
1044 pub fn push(&mut self, chunk: &ChunkMeta, sdp: &str) -> Option<String> {
1047 let total = usize::try_from(chunk.total).unwrap_or(usize::MAX);
1048 if chunk.total == 0 || total > self.max_chunks || chunk.seq >= chunk.total {
1049 return None;
1050 }
1051 if !self.partials.contains_key(&chunk.id) && self.partials.len() >= self.max_concurrent {
1052 return None;
1053 }
1054 let partial = self
1055 .partials
1056 .entry(chunk.id.clone())
1057 .or_insert_with(|| PartialSdp {
1058 total: chunk.total,
1059 parts: BTreeMap::new(),
1060 });
1061 if partial.total != chunk.total {
1062 return None;
1063 }
1064 partial.parts.insert(chunk.seq, sdp.to_string());
1065 if partial.parts.len() != total {
1066 return None;
1067 }
1068 let complete = self.partials.remove(&chunk.id)?;
1069 let mut joined = String::new();
1070 for seq in 0..complete.total {
1071 joined.push_str(complete.parts.get(&seq)?);
1072 }
1073 Some(joined)
1074 }
1075
1076 pub fn pending_len(&self) -> usize {
1078 self.partials.len()
1079 }
1080
1081 pub fn drop_all(&mut self) {
1083 self.partials.clear();
1084 }
1085}
1086
1087impl Default for SdpReassembler {
1088 fn default() -> Self {
1089 Self::new()
1090 }
1091}
1092
1093#[derive(Debug, Clone, PartialEq, Eq)]
1104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1105#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
1106#[cfg_attr(feature = "ts", ts(rename = "VoiceParticipant"))]
1107pub struct Participant {
1108 pub nick: String,
1111 pub role: Role,
1114 pub mic: OnOff,
1116 pub video: OnOff,
1118 pub speaking: OnOff,
1120 pub deaf: OnOff,
1122 pub screen: OnOff,
1124 pub hand: OnOff,
1126}
1127
1128impl Participant {
1129 pub fn new(nick: impl Into<String>) -> Self {
1131 Self {
1132 nick: nick.into(),
1133 ..Self::default()
1134 }
1135 }
1136}
1137
1138impl Default for Participant {
1139 fn default() -> Self {
1140 Self {
1141 nick: String::new(),
1142 role: Role::Viewer,
1143 mic: OnOff::Off,
1144 video: OnOff::Off,
1145 speaking: OnOff::Off,
1146 deaf: OnOff::Off,
1147 screen: OnOff::Off,
1148 hand: OnOff::Off,
1149 }
1150 }
1151}
1152
1153#[derive(Debug, Clone, PartialEq, Eq)]
1156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1157#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
1158#[cfg_attr(feature = "ts", ts(rename = "VoiceRoom"))]
1159pub struct Room {
1160 pub channel: String,
1162 pub kind: RoomKind,
1164 pub participants: BTreeMap<CaseFolded, Participant>,
1166 pub turn: Option<TurnCredentials>,
1168}
1169
1170impl Room {
1171 pub fn new(channel: impl Into<String>) -> Self {
1173 let channel = channel.into();
1174 let kind = RoomKind::for_channel(&channel);
1175 Self {
1176 channel,
1177 kind,
1178 participants: BTreeMap::new(),
1179 turn: None,
1180 }
1181 }
1182
1183 pub fn apply(&mut self, me: &str, signal: &Signal, casemap: Casemapping) {
1191 match signal {
1192 Signal::Joined {
1193 members,
1194 role,
1195 streamers,
1196 turn,
1197 ..
1198 } => self.apply_joined(
1199 me,
1200 members,
1201 *role,
1202 streamers.as_deref(),
1203 turn.as_ref(),
1204 casemap,
1205 ),
1206 Signal::Presence {
1207 member,
1208 state,
1209 kind,
1210 role,
1211 } => self.apply_presence(member, *state, *kind, *role, casemap),
1212 Signal::Role { member, role } => {
1213 self.participant_mut(member, casemap).role = *role;
1214 }
1215 _ => {}
1216 }
1217 }
1218
1219 fn apply_joined(
1220 &mut self,
1221 me: &str,
1222 members: &[String],
1223 role: Option<Role>,
1224 streamers: Option<&[String]>,
1225 turn: Option<&TurnCredentials>,
1226 casemap: Casemapping,
1227 ) {
1228 self.turn = turn.cloned();
1229 for member in members {
1230 let assigned = match (casemap.eq(member, me), role) {
1231 (true, Some(role)) => role,
1232 _ => self.role_for(member, streamers, casemap),
1233 };
1234 self.participant_mut(member, casemap).role = assigned;
1235 }
1236 }
1237
1238 fn participant_mut(&mut self, member: &str, casemap: Casemapping) -> &mut Participant {
1240 self.participants
1241 .entry(casemap.fold(member))
1242 .or_insert_with(|| Participant::new(member))
1243 }
1244
1245 fn apply_presence(
1246 &mut self,
1247 member: &str,
1248 state: PresenceState,
1249 kind: Option<ToggleKind>,
1250 role: Option<Role>,
1251 casemap: Casemapping,
1252 ) {
1253 match state {
1254 PresenceState::Joined => {
1255 let assigned = role.unwrap_or_else(|| self.role_for(member, None, casemap));
1256 self.participant_mut(member, casemap).role = assigned;
1257 }
1258 PresenceState::Left => {
1259 self.participants.remove(&casemap.fold(member));
1260 }
1261 PresenceState::On | PresenceState::Off => {
1262 if let (Some(kind), Some(participant)) =
1263 (kind, self.participants.get_mut(&casemap.fold(member)))
1264 {
1265 let toggled = if state == PresenceState::On {
1266 OnOff::On
1267 } else {
1268 OnOff::Off
1269 };
1270 kind.apply(participant, toggled);
1271 }
1272 }
1273 PresenceState::Speaking => {
1274 self.set_flag(member, casemap, |p| p.speaking = OnOff::On);
1275 }
1276 PresenceState::Silent => self.set_flag(member, casemap, |p| p.speaking = OnOff::Off),
1277 PresenceState::DeafOn => self.set_flag(member, casemap, |p| p.deaf = OnOff::On),
1278 PresenceState::DeafOff => self.set_flag(member, casemap, |p| p.deaf = OnOff::Off),
1279 }
1280 }
1281
1282 fn set_flag(&mut self, member: &str, casemap: Casemapping, f: impl FnOnce(&mut Participant)) {
1283 if let Some(participant) = self.participants.get_mut(&casemap.fold(member)) {
1284 f(participant);
1285 }
1286 }
1287
1288 fn role_for(&self, member: &str, streamers: Option<&[String]>, casemap: Casemapping) -> Role {
1289 match self.kind {
1290 RoomKind::Publish => Role::Publisher,
1291 RoomKind::Stream => {
1292 let is_streamer =
1293 streamers.is_some_and(|list| list.iter().any(|name| casemap.eq(name, member)));
1294 if is_streamer {
1295 Role::Publisher
1296 } else {
1297 Role::Viewer
1298 }
1299 }
1300 }
1301 }
1302}
1303
1304#[cfg(test)]
1305mod tests {
1306 use super::*;
1307
1308 fn turn() -> TurnCredentials {
1309 TurnCredentials {
1310 urls: alloc::vec!["turn:relay.obby.chat:3478".to_string()],
1311 username: "user1".to_string(),
1312 password: "pass1".to_string(),
1313 }
1314 }
1315
1316 #[test]
1317 fn join_and_leave_round_trip_with_only_a_channel() {
1318 let join = Signal::Join {
1319 channel: "#general".to_string(),
1320 };
1321 assert_eq!(join.to_json(), r##"{"type":"join","channel":"#general"}"##);
1322 assert_eq!(Signal::from_json(&join.to_json()), Some(join));
1323
1324 let leave = Signal::Leave {
1325 channel: "^voice".to_string(),
1326 };
1327 assert_eq!(leave.to_json(), r#"{"type":"leave","channel":"^voice"}"#);
1328 assert_eq!(Signal::from_json(&leave.to_json()), Some(leave));
1329 }
1330
1331 #[test]
1332 fn joined_round_trips_with_every_optional_field_present() {
1333 let joined = Signal::Joined {
1334 members: alloc::vec!["alice".to_string(), "bob".to_string()],
1335 mode: Some(RoomKind::Stream),
1336 role: Some(Role::Publisher),
1337 streamers: Some(alloc::vec!["alice".to_string()]),
1338 turn: Some(turn()),
1339 tracks: Some(alloc::vec![TrackHint {
1340 mid: "0".to_string(),
1341 member: "alice".to_string(),
1342 }]),
1343 };
1344 let json = joined.to_json();
1345 assert!(json.starts_with(r#"{"type":"joined","members":["alice","bob"]"#));
1346 assert!(json.contains(r#""mode":"stream""#));
1347 assert!(json.contains(r#""role":"streamer""#));
1348 assert!(json.contains(r#""streamers":["alice"]"#));
1349 assert!(json.contains(
1350 r#""turn":{"urls":["turn:relay.obby.chat:3478"],"username":"user1","password":"pass1"}"#
1351 ));
1352 assert!(json.contains(r#""tracks":[{"mid":"0","member":"alice"}]"#));
1353 assert_eq!(Signal::from_json(&json), Some(joined));
1354 }
1355
1356 #[test]
1357 fn joined_round_trips_with_every_optional_field_absent() {
1358 let joined = Signal::Joined {
1359 members: alloc::vec!["alice".to_string()],
1360 mode: None,
1361 role: None,
1362 streamers: None,
1363 turn: None,
1364 tracks: None,
1365 };
1366 assert_eq!(joined.to_json(), r#"{"type":"joined","members":["alice"]}"#);
1367 assert_eq!(Signal::from_json(&joined.to_json()), Some(joined));
1368 }
1369
1370 #[test]
1371 fn offer_and_answer_round_trip_unchunked() {
1372 let offer = Signal::Offer {
1373 sdp: "v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\n".to_string(),
1374 tracks: None,
1375 chunk: None,
1376 };
1377 assert_eq!(Signal::from_json(&offer.to_json()), Some(offer.clone()));
1378 assert!(!offer.to_json().contains("\"id\""));
1379
1380 let answer = Signal::Answer {
1381 sdp: "v=0\r\n".to_string(),
1382 chunk: None,
1383 };
1384 assert_eq!(Signal::from_json(&answer.to_json()), Some(answer));
1385 }
1386
1387 #[test]
1388 fn offer_and_answer_round_trip_with_chunk_metadata() {
1389 let offer = Signal::Offer {
1390 sdp: "partial-sdp".to_string(),
1391 tracks: None,
1392 chunk: Some(ChunkMeta {
1393 id: "abc123".to_string(),
1394 seq: 1,
1395 total: 3,
1396 }),
1397 };
1398 let json = offer.to_json();
1399 assert_eq!(
1400 json,
1401 r#"{"type":"offer","sdp":"partial-sdp","id":"abc123","seq":1,"total":3}"#
1402 );
1403 assert_eq!(Signal::from_json(&json), Some(offer));
1404 }
1405
1406 #[test]
1407 fn ice_round_trips_with_and_without_mid() {
1408 let with_mid = Signal::Ice {
1409 cand: "candidate:1 1 UDP 1 1.2.3.4 5 typ host".to_string(),
1410 mid: Some("0".to_string()),
1411 mlineidx: Some(0),
1412 };
1413 assert_eq!(Signal::from_json(&with_mid.to_json()), Some(with_mid));
1414
1415 let without_mid = Signal::Ice {
1416 cand: "candidate:1 1 UDP 1 1.2.3.4 5 typ host".to_string(),
1417 mid: None,
1418 mlineidx: None,
1419 };
1420 assert_eq!(
1421 without_mid.to_json(),
1422 r#"{"type":"ice","cand":"candidate:1 1 UDP 1 1.2.3.4 5 typ host"}"#
1423 );
1424 assert_eq!(Signal::from_json(&without_mid.to_json()), Some(without_mid));
1425 }
1426
1427 #[test]
1428 fn presence_round_trips_all_three_shapes() {
1429 let membership = Signal::Presence {
1430 member: "bob".to_string(),
1431 state: PresenceState::Joined,
1432 kind: None,
1433 role: Some(Role::Viewer),
1434 };
1435 assert_eq!(
1436 membership.to_json(),
1437 r#"{"type":"presence","member":"bob","state":"joined","role":"viewer"}"#
1438 );
1439 assert_eq!(Signal::from_json(&membership.to_json()), Some(membership));
1440
1441 let toggle = Signal::Presence {
1442 member: "bob".to_string(),
1443 state: PresenceState::On,
1444 kind: Some(ToggleKind::Mic),
1445 role: None,
1446 };
1447 assert_eq!(
1448 toggle.to_json(),
1449 r#"{"type":"presence","member":"bob","state":"on","kind":"mic"}"#
1450 );
1451 assert_eq!(Signal::from_json(&toggle.to_json()), Some(toggle));
1452
1453 let activity = Signal::Presence {
1454 member: "bob".to_string(),
1455 state: PresenceState::DeafOn,
1456 kind: None,
1457 role: None,
1458 };
1459 assert_eq!(
1460 activity.to_json(),
1461 r#"{"type":"presence","member":"bob","state":"deaf-on"}"#
1462 );
1463 assert_eq!(Signal::from_json(&activity.to_json()), Some(activity));
1464 }
1465
1466 #[test]
1467 fn the_six_toggle_intents_round_trip() {
1468 let frames = [
1469 Signal::Mic { state: OnOff::On },
1470 Signal::Video { state: OnOff::Off },
1471 Signal::Screen { state: OnOff::On },
1472 Signal::Hand { state: OnOff::On },
1473 Signal::Speaking,
1474 Signal::Silent,
1475 ];
1476 for frame in frames {
1477 assert_eq!(Signal::from_json(&frame.to_json()), Some(frame));
1478 }
1479 }
1480
1481 #[test]
1482 fn deaf_is_a_real_outbound_frame_not_only_a_local_flag() {
1483 let deafen = Signal::Deaf { state: OnOff::On };
1484 assert_eq!(deafen.to_json(), r#"{"type":"deaf","state":"on"}"#);
1485 assert_eq!(Signal::from_json(&deafen.to_json()), Some(deafen));
1486 }
1487
1488 #[test]
1489 fn react_round_trips_outbound_and_inbound_shapes() {
1490 let outbound = Signal::React {
1491 member: None,
1492 emoji: "👍".to_string(),
1493 };
1494 assert_eq!(outbound.to_json(), r#"{"type":"react","emoji":"👍"}"#);
1495 assert_eq!(Signal::from_json(&outbound.to_json()), Some(outbound));
1496
1497 let inbound = Signal::React {
1498 member: Some("carol".to_string()),
1499 emoji: "👍".to_string(),
1500 };
1501 assert_eq!(
1502 inbound.to_json(),
1503 r#"{"type":"react","member":"carol","emoji":"👍"}"#
1504 );
1505 assert_eq!(Signal::from_json(&inbound.to_json()), Some(inbound));
1506 }
1507
1508 #[test]
1509 fn promote_demote_and_role_round_trip() {
1510 let promote = Signal::Promote {
1511 target: "bob".to_string(),
1512 };
1513 assert_eq!(Signal::from_json(&promote.to_json()), Some(promote));
1514
1515 let demote = Signal::Demote {
1516 target: "bob".to_string(),
1517 };
1518 assert_eq!(Signal::from_json(&demote.to_json()), Some(demote));
1519
1520 let role = Signal::Role {
1521 member: "bob".to_string(),
1522 role: Role::Publisher,
1523 };
1524 assert_eq!(
1525 role.to_json(),
1526 r#"{"type":"role","member":"bob","role":"streamer"}"#
1527 );
1528 assert_eq!(Signal::from_json(&role.to_json()), Some(role));
1529 }
1530
1531 #[test]
1532 fn error_round_trips_with_and_without_a_reason() {
1533 let with_reason = Signal::Error {
1534 error: Some("room is full".to_string()),
1535 };
1536 assert_eq!(
1537 with_reason.to_json(),
1538 r#"{"type":"error","error":"room is full"}"#
1539 );
1540 assert_eq!(Signal::from_json(&with_reason.to_json()), Some(with_reason));
1541
1542 let without_reason = Signal::Error { error: None };
1543 assert_eq!(without_reason.to_json(), r#"{"type":"error"}"#);
1544 assert_eq!(
1545 Signal::from_json(&without_reason.to_json()),
1546 Some(without_reason)
1547 );
1548 }
1549
1550 #[test]
1551 fn unknown_frame_types_decode_to_none() {
1552 assert_eq!(Signal::from_json(r#"{"type":"nonsense"}"#), None);
1553 }
1554
1555 #[test]
1556 fn malformed_json_decodes_to_none() {
1557 assert_eq!(Signal::from_json("not json"), None);
1558 assert_eq!(Signal::from_json(r#"{"type":"join""#), None);
1559 assert_eq!(Signal::from_json(r##"{"channel":"#general"}"##), None);
1560 }
1561
1562 #[test]
1563 fn an_sdp_within_budget_is_not_split() {
1564 assert_eq!(split_sdp("short sdp", "id1", 8191), None);
1565 }
1566
1567 #[test]
1568 fn an_oversized_sdp_is_split_into_numbered_chunks_sharing_one_id() {
1569 let sdp = "abcdefghij";
1570 let chunks = split_sdp(sdp, "call-1", 3).expect("must split");
1571 assert_eq!(chunks.len(), 4);
1572 for (seq, chunk) in chunks.iter().enumerate() {
1573 assert_eq!(chunk.chunk.id, "call-1");
1574 assert_eq!(chunk.chunk.seq, u32::try_from(seq).unwrap_or(u32::MAX));
1575 assert_eq!(chunk.chunk.total, 4);
1576 }
1577 let joined: String = chunks.iter().map(|c| c.sdp.as_str()).collect();
1578 assert_eq!(joined, sdp);
1579 }
1580
1581 #[test]
1582 fn a_multibyte_character_is_never_split_across_a_chunk_boundary() {
1583 let sdp = "a👍b";
1584 let chunks = split_sdp(sdp, "id", 2).expect("must split");
1585 for chunk in &chunks {
1586 assert!(core::str::from_utf8(chunk.sdp.as_bytes()).is_ok());
1587 }
1588 let joined: String = chunks.iter().map(|c| c.sdp.as_str()).collect();
1589 assert_eq!(joined, sdp);
1590 }
1591
1592 #[test]
1593 fn chunks_reassemble_regardless_of_arrival_order() {
1594 let chunks = split_sdp(&"x".repeat(20), "call-1", 6).expect("must split");
1595 let mut reassembler = SdpReassembler::new();
1596 let mut out = None;
1597 for chunk in chunks.iter().rev() {
1598 out = reassembler.push(&chunk.chunk, &chunk.sdp);
1599 }
1600 assert_eq!(out, Some("x".repeat(20)));
1601 assert_eq!(reassembler.pending_len(), 0);
1602 }
1603
1604 #[test]
1605 fn a_reassembly_missing_a_chunk_never_completes() {
1606 let chunks = split_sdp(&"y".repeat(20), "call-2", 6).expect("must split");
1607 let mut reassembler = SdpReassembler::new();
1608 for chunk in chunks.iter().take(chunks.len() - 1) {
1609 assert_eq!(reassembler.push(&chunk.chunk, &chunk.sdp), None);
1610 }
1611 assert_eq!(reassembler.pending_len(), 1);
1612 }
1613
1614 #[test]
1615 fn reassembly_rejects_a_total_over_the_configured_bound() {
1616 let mut reassembler = SdpReassembler::with_bounds(4, 16);
1617 let chunk = ChunkMeta {
1618 id: "huge".to_string(),
1619 seq: 0,
1620 total: 5,
1621 };
1622 assert_eq!(reassembler.push(&chunk, "slice"), None);
1623 assert_eq!(reassembler.pending_len(), 0);
1624 }
1625
1626 #[test]
1627 fn reassembly_bounds_the_number_of_concurrent_ids() {
1628 let mut reassembler = SdpReassembler::with_bounds(64, 2);
1629 for id in ["a", "b"] {
1630 let chunk = ChunkMeta {
1631 id: id.to_string(),
1632 seq: 0,
1633 total: 2,
1634 };
1635 assert_eq!(reassembler.push(&chunk, "part"), None);
1636 }
1637 assert_eq!(reassembler.pending_len(), 2);
1638
1639 let overflow = ChunkMeta {
1640 id: "c".to_string(),
1641 seq: 0,
1642 total: 2,
1643 };
1644 assert_eq!(reassembler.push(&overflow, "part"), None);
1645 assert_eq!(
1646 reassembler.pending_len(),
1647 2,
1648 "a third id must not grow past the configured bound"
1649 );
1650 }
1651
1652 #[test]
1653 fn dropping_all_forgets_every_partial_reassembly() {
1654 let mut reassembler = SdpReassembler::new();
1655 let chunk = ChunkMeta {
1656 id: "call".to_string(),
1657 seq: 0,
1658 total: 2,
1659 };
1660 reassembler.push(&chunk, "part");
1661 reassembler.drop_all();
1662 assert_eq!(reassembler.pending_len(), 0);
1663 }
1664
1665 #[test]
1666 fn every_member_publishes_in_a_publish_room() {
1667 let mut room = Room::new("^voice");
1668 assert_eq!(room.kind, RoomKind::Publish);
1669 room.apply(
1670 "me",
1671 &Signal::Joined {
1672 members: alloc::vec!["me".to_string(), "bob".to_string()],
1673 mode: None,
1674 role: None,
1675 streamers: None,
1676 turn: None,
1677 tracks: None,
1678 },
1679 Casemapping::Rfc1459,
1680 );
1681 for nick in ["me", "bob"] {
1682 assert_eq!(
1683 room.participants[&CaseFolded::from(nick)].role,
1684 Role::Publisher
1685 );
1686 }
1687 }
1688
1689 #[test]
1690 fn only_the_streamers_publish_in_a_stream_room() {
1691 let mut room = Room::new("$live");
1692 assert_eq!(room.kind, RoomKind::Stream);
1693 room.apply(
1694 "me",
1695 &Signal::Joined {
1696 members: alloc::vec!["alice".to_string(), "me".to_string()],
1697 mode: Some(RoomKind::Stream),
1698 role: Some(Role::Viewer),
1699 streamers: Some(alloc::vec!["alice".to_string()]),
1700 turn: None,
1701 tracks: None,
1702 },
1703 Casemapping::Rfc1459,
1704 );
1705 assert_eq!(
1706 room.participants[&CaseFolded::from("alice")].role,
1707 Role::Publisher
1708 );
1709 assert_eq!(
1710 room.participants[&CaseFolded::from("me")].role,
1711 Role::Viewer
1712 );
1713 }
1714
1715 #[test]
1716 fn a_promote_broadcast_updates_the_targets_role() {
1717 let mut room = Room::new("$live");
1718 room.apply(
1719 "me",
1720 &Signal::Role {
1721 member: "bob".to_string(),
1722 role: Role::Publisher,
1723 },
1724 Casemapping::Rfc1459,
1725 );
1726 assert_eq!(
1727 room.participants[&CaseFolded::from("bob")].role,
1728 Role::Publisher
1729 );
1730 }
1731
1732 #[test]
1733 fn presence_toggles_update_the_matching_participant_flag() {
1734 let mut room = Room::new("^voice");
1735 room.participants
1736 .insert(CaseFolded::from("bob"), Participant::new("bob"));
1737 room.apply(
1738 "me",
1739 &Signal::Presence {
1740 member: "bob".to_string(),
1741 state: PresenceState::On,
1742 kind: Some(ToggleKind::Video),
1743 role: None,
1744 },
1745 Casemapping::Rfc1459,
1746 );
1747 assert_eq!(room.participants[&CaseFolded::from("bob")].video, OnOff::On);
1748 assert_eq!(room.participants[&CaseFolded::from("bob")].mic, OnOff::Off);
1749 }
1750
1751 #[test]
1752 fn one_participant_under_two_spellings_is_one_person() {
1753 let mut room = Room::new("^voice");
1754 room.apply(
1755 "me",
1756 &Signal::Presence {
1757 member: "Bob[dev]".to_string(),
1758 state: PresenceState::Joined,
1759 kind: None,
1760 role: None,
1761 },
1762 Casemapping::Rfc1459,
1763 );
1764 room.apply(
1765 "me",
1766 &Signal::Presence {
1767 member: "bob{dev}".to_string(),
1768 state: PresenceState::On,
1769 kind: Some(ToggleKind::Mic),
1770 role: None,
1771 },
1772 Casemapping::Rfc1459,
1773 );
1774 assert_eq!(room.participants.len(), 1);
1775 let participant = room
1776 .participants
1777 .values()
1778 .next()
1779 .expect("the room has one participant");
1780 assert_eq!(participant.nick, "Bob[dev]");
1781 assert_eq!(participant.mic, OnOff::On);
1782 }
1783
1784 #[test]
1785 fn presence_joined_and_left_add_and_remove_participants() {
1786 let mut room = Room::new("^voice");
1787 room.apply(
1788 "me",
1789 &Signal::Presence {
1790 member: "bob".to_string(),
1791 state: PresenceState::Joined,
1792 kind: None,
1793 role: None,
1794 },
1795 Casemapping::Rfc1459,
1796 );
1797 assert!(room.participants.contains_key(&CaseFolded::from("bob")));
1798
1799 room.apply(
1800 "me",
1801 &Signal::Presence {
1802 member: "bob".to_string(),
1803 state: PresenceState::Left,
1804 kind: None,
1805 role: None,
1806 },
1807 Casemapping::Rfc1459,
1808 );
1809 assert!(!room.participants.contains_key(&CaseFolded::from("bob")));
1810 }
1811
1812 #[test]
1813 fn deafen_broadcasts_to_the_room_instead_of_staying_local() {
1814 let mut room = Room::new("^voice");
1815 room.participants
1816 .insert(CaseFolded::from("bob"), Participant::new("bob"));
1817 room.apply(
1818 "me",
1819 &Signal::Presence {
1820 member: "bob".to_string(),
1821 state: PresenceState::DeafOn,
1822 kind: None,
1823 role: None,
1824 },
1825 Casemapping::Rfc1459,
1826 );
1827 assert_eq!(
1828 room.participants[&CaseFolded::from("bob")].deaf,
1829 OnOff::On,
1830 "the room's model must learn a peer's deafen from presence, the way it learns mic or video"
1831 );
1832
1833 let intent = Signal::Deaf { state: OnOff::On };
1834 assert!(
1835 intent.to_json().contains("\"type\":\"deaf\""),
1836 "deafening must produce a real outbound frame, not only flip a local field"
1837 );
1838 }
1839}