1use std::collections::{BTreeMap, BTreeSet};
5
6use crate::proto;
7use crate::version::MumbleVersion;
8
9#[derive(Debug, Clone, Default, PartialEq)]
11pub struct Channel {
12 pub id: u32,
13 pub parent: Option<u32>,
14 pub name: String,
15 pub description: Option<String>,
18 pub description_hash: Option<Vec<u8>>,
19 pub links: BTreeSet<u32>,
20 pub temporary: bool,
21 pub position: i32,
22 pub max_users: Option<u32>,
23 pub is_enter_restricted: bool,
24 pub can_enter: Option<bool>,
26}
27
28#[derive(Debug, Clone, Default, PartialEq)]
30pub struct User {
31 pub session: u32,
32 pub name: String,
33 pub user_id: Option<u32>,
35 pub channel_id: u32,
36 pub mute: bool,
38 pub deaf: bool,
39 pub suppress: bool,
40 pub self_mute: bool,
41 pub self_deaf: bool,
42 pub priority_speaker: bool,
43 pub recording: bool,
44 pub comment: Option<String>,
46 pub comment_hash: Option<Vec<u8>>,
47 pub texture: Option<Vec<u8>>,
49 pub texture_hash: Option<Vec<u8>>,
50 pub cert_hash: Option<String>,
52 pub listening: BTreeSet<u32>,
54 pub listening_volume: BTreeMap<u32, f32>,
57}
58
59#[derive(Clone, Default, PartialEq, Eq)]
61pub struct CryptSetup {
62 pub key: Option<Vec<u8>>,
63 pub client_nonce: Option<Vec<u8>>,
64 pub server_nonce: Option<Vec<u8>>,
65}
66
67impl std::fmt::Debug for CryptSetup {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("CryptSetup")
71 .field("has_key", &self.key.is_some())
72 .field("has_client_nonce", &self.client_nonce.is_some())
73 .field("has_server_nonce", &self.server_nonce.is_some())
74 .finish()
75 }
76}
77
78impl From<proto::CryptSetup> for CryptSetup {
79 fn from(m: proto::CryptSetup) -> Self {
80 Self {
81 key: m.key,
82 client_nonce: m.client_nonce,
83 server_nonce: m.server_nonce,
84 }
85 }
86}
87
88impl From<CryptSetup> for proto::CryptSetup {
89 fn from(c: CryptSetup) -> Self {
90 Self {
91 key: c.key,
92 client_nonce: c.client_nonce,
93 server_nonce: c.server_nonce,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Default, PartialEq)]
100pub struct ServerInfo {
101 pub version: Option<MumbleVersion>,
102 pub release: Option<String>,
103 pub os: Option<String>,
104 pub max_bandwidth: Option<u32>,
105 pub welcome_text: Option<String>,
107 pub allow_html: Option<bool>,
108 pub message_length: Option<u32>,
110 pub image_message_length: Option<u32>,
112 pub max_users: Option<u32>,
113 pub recording_allowed: Option<bool>,
114}
115
116impl ServerInfo {
117 pub fn supports_listening(&self) -> bool {
119 self.version.is_some_and(MumbleVersion::supports_listening)
120 }
121
122 pub fn supports_listener_volume(&self) -> bool {
123 self.version
124 .is_some_and(MumbleVersion::supports_listener_volume)
125 }
126}
127
128#[derive(Debug, Clone, Default, PartialEq)]
130pub struct ServerState {
131 pub channels: BTreeMap<u32, Channel>,
132 pub users: BTreeMap<u32, User>,
133 pub session: Option<u32>,
135 pub server: ServerInfo,
136 pub crypt: Option<CryptSetup>,
138 pub permissions: BTreeMap<u32, u32>,
140}
141
142#[derive(Debug, Clone, PartialEq)]
144pub struct UserChange {
145 pub user: User,
146 pub joined: bool,
148 pub moved_from: Option<u32>,
150 pub actor: Option<u32>,
152 pub echoed_rights: u32,
156}
157
158fn echoed_rights(m: &proto::UserState) -> u32 {
160 use crate::permissions::{MOVE, MUTE_DEAFEN, REGISTER};
161 let mut rights = 0;
162 if m.mute.is_some() || m.deaf.is_some() || m.suppress.is_some() || m.priority_speaker.is_some()
163 {
164 rights |= MUTE_DEAFEN;
165 }
166 if m.channel_id.is_some() {
167 rights |= MOVE;
168 }
169 if m.user_id.is_some() {
170 rights |= REGISTER;
171 }
172 rights
173}
174
175impl ServerState {
176 pub fn own_user(&self) -> Option<&User> {
178 self.session.and_then(|s| self.users.get(&s))
179 }
180
181 pub fn children(&self, parent: u32) -> Vec<&Channel> {
183 let mut out: Vec<&Channel> = self
184 .channels
185 .values()
186 .filter(|c| c.parent == Some(parent))
187 .collect();
188 out.sort_by(|a, b| {
189 a.position
190 .cmp(&b.position)
191 .then_with(|| a.name.cmp(&b.name))
192 });
193 out
194 }
195
196 pub fn users_in(&self, channel: u32) -> impl Iterator<Item = &User> {
198 self.users.values().filter(move |u| u.channel_id == channel)
199 }
200
201 pub fn listeners_of(&self, channel: u32) -> impl Iterator<Item = &User> {
203 self.users
204 .values()
205 .filter(move |u| u.listening.contains(&channel))
206 }
207
208 pub fn child_by_name(&self, parent: u32, name: &str) -> Option<&Channel> {
210 self.channels
211 .values()
212 .find(|c| c.parent == Some(parent) && c.name == name)
213 }
214
215 pub fn ancestry(&self, id: u32) -> impl Iterator<Item = &Channel> {
218 let mut next = Some(id);
219 std::iter::from_fn(move || {
220 let channel = self.channels.get(&next?)?;
221 next = channel.parent;
222 Some(channel)
223 })
224 .take(self.channels.len())
225 }
226
227 fn would_loop(&self, id: u32, parent: u32) -> bool {
229 parent == id || self.ancestry(parent).any(|c| c.parent == Some(id))
230 }
231
232 pub fn apply_channel_state(&mut self, m: proto::ChannelState) -> Option<&Channel> {
233 let id = m.channel_id?;
234 let parent = match m.parent {
235 Some(_) if id == 0 => Some(None),
237 Some(parent) if self.would_loop(id, parent) => {
238 tracing::warn!("ignored a channel parent that would form a loop");
239 None
240 }
241 parent => parent.map(Some),
242 };
243 let ch = self.channels.entry(id).or_insert_with(|| Channel {
244 id,
245 ..Default::default()
246 });
247 if let Some(parent) = parent {
248 ch.parent = parent;
249 }
250 if let Some(name) = m.name {
251 ch.name = name;
252 }
253 if let Some(hash) = m.description_hash {
254 if ch.description_hash.as_ref() != Some(&hash) {
256 ch.description = None;
257 }
258 ch.description_hash = Some(hash);
259 }
260 if let Some(description) = m.description {
261 ch.description = Some(description);
262 }
263 if !m.links.is_empty() {
264 ch.links = m.links.into_iter().collect();
265 }
266 for l in m.links_add {
267 ch.links.insert(l);
268 }
269 for l in &m.links_remove {
270 ch.links.remove(l);
271 }
272 if let Some(t) = m.temporary {
273 ch.temporary = t;
274 }
275 if let Some(p) = m.position {
276 ch.position = p;
277 }
278 if let Some(max) = m.max_users {
279 ch.max_users = Some(max);
280 }
281 if let Some(r) = m.is_enter_restricted {
282 ch.is_enter_restricted = r;
283 }
284 if let Some(c) = m.can_enter {
285 ch.can_enter = Some(c);
286 }
287 self.channels.get(&id)
288 }
289
290 pub fn apply_channel_remove(&mut self, id: u32) -> Option<Channel> {
292 let removed = self.channels.remove(&id)?;
293 for ch in self.channels.values_mut() {
294 ch.links.remove(&id);
295 }
296 for user in self.users.values_mut() {
297 user.listening.remove(&id);
298 user.listening_volume.remove(&id);
299 }
300 self.permissions.remove(&id);
301 Some(removed)
302 }
303
304 pub fn apply_user_state(&mut self, m: proto::UserState) -> Option<UserChange> {
305 let session = m.session?;
306 let joined = !self.users.contains_key(&session);
307 let echoed_rights = echoed_rights(&m);
308 let user = self.users.entry(session).or_insert_with(|| User {
309 session,
310 ..Default::default()
311 });
312 let mut moved_from = None;
313 if let Some(name) = m.name {
314 user.name = name;
315 }
316 if let Some(id) = m.user_id {
317 user.user_id = (id != u32::MAX).then_some(id);
319 }
320 if let Some(c) = m.channel_id {
321 if !joined && user.channel_id != c {
322 moved_from = Some(user.channel_id);
323 }
324 user.channel_id = c;
325 }
326 set(&mut user.mute, m.mute);
327 set(&mut user.deaf, m.deaf);
328 set(&mut user.suppress, m.suppress);
329 set(&mut user.self_mute, m.self_mute);
330 set(&mut user.self_deaf, m.self_deaf);
331 set(&mut user.priority_speaker, m.priority_speaker);
332 set(&mut user.recording, m.recording);
333 if let Some(hash) = m.comment_hash {
335 if user.comment_hash.as_ref() != Some(&hash) {
336 user.comment = None;
337 }
338 user.comment_hash = Some(hash);
339 }
340 if let Some(comment) = m.comment {
341 user.comment = (!comment.is_empty()).then_some(comment);
343 if user.comment.is_none() {
344 user.comment_hash = None;
345 }
346 }
347 if let Some(hash) = m.texture_hash {
348 if user.texture_hash.as_ref() != Some(&hash) {
349 user.texture = None;
350 }
351 user.texture_hash = Some(hash);
352 }
353 if let Some(texture) = m.texture {
354 user.texture = (!texture.is_empty()).then_some(texture);
355 if user.texture.is_none() {
356 user.texture_hash = None;
357 }
358 }
359 if let Some(hash) = m.hash {
360 user.cert_hash = (!hash.is_empty()).then_some(hash);
361 }
362 for c in m.listening_channel_add {
363 user.listening.insert(c);
364 }
365 for c in &m.listening_channel_remove {
366 user.listening.remove(c);
367 user.listening_volume.remove(c);
368 }
369 for adj in m.listening_volume_adjustment {
370 if let (Some(c), Some(v)) = (adj.listening_channel, adj.volume_adjustment) {
371 user.listening_volume.insert(c, v);
372 }
373 }
374 Some(UserChange {
375 user: user.clone(),
376 joined,
377 moved_from,
378 actor: m.actor,
379 echoed_rights,
380 })
381 }
382
383 pub fn apply_user_remove(&mut self, session: u32) -> Option<User> {
384 self.users.remove(&session)
385 }
386
387 pub(crate) fn apply_server_sync(&mut self, m: &proto::ServerSync) {
388 if let Some(s) = m.session {
389 self.session = Some(s);
390 }
391 if m.max_bandwidth.is_some() {
392 self.server.max_bandwidth = m.max_bandwidth;
393 }
394 if m.welcome_text.is_some() {
395 self.server.welcome_text.clone_from(&m.welcome_text);
396 }
397 if let Some(p) = m.permissions {
398 self.permissions.insert(0, p as u32);
400 }
401 }
402
403 pub(crate) fn apply_server_config(&mut self, m: proto::ServerConfig) {
404 let s = &mut self.server;
405 if m.max_bandwidth.is_some() {
406 s.max_bandwidth = m.max_bandwidth;
407 }
408 if m.welcome_text.is_some() {
409 s.welcome_text = m.welcome_text;
410 }
411 if m.allow_html.is_some() {
412 s.allow_html = m.allow_html;
413 }
414 if m.message_length.is_some() {
415 s.message_length = m.message_length;
416 }
417 if m.image_message_length.is_some() {
418 s.image_message_length = m.image_message_length;
419 }
420 if m.max_users.is_some() {
421 s.max_users = m.max_users;
422 }
423 if m.recording_allowed.is_some() {
424 s.recording_allowed = m.recording_allowed;
425 }
426 }
427
428 pub(crate) fn apply_permission_query(&mut self, m: &proto::PermissionQuery) {
429 if m.flush == Some(true) {
430 self.permissions.clear();
431 }
432 if let (Some(c), Some(p)) = (m.channel_id, m.permissions) {
433 self.permissions.insert(c, p);
434 }
435 }
436}
437
438fn set(target: &mut bool, value: Option<bool>) {
439 if let Some(v) = value {
440 *target = v;
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 fn channel(id: u32, parent: Option<u32>, name: &str) -> proto::ChannelState {
449 proto::ChannelState {
450 channel_id: Some(id),
451 parent,
452 name: Some(name.into()),
453 ..Default::default()
454 }
455 }
456
457 fn tree() -> ServerState {
458 let mut s = ServerState::default();
459 s.apply_channel_state(channel(0, None, "Root"));
460 s.apply_channel_state(proto::ChannelState {
461 position: Some(1),
462 ..channel(1, Some(0), "Lobby")
463 });
464 s.apply_channel_state(channel(2, Some(0), "Raid"));
465 s.apply_channel_state(channel(3, Some(0), "AFK"));
466 s
467 }
468
469 #[test]
470 fn builds_tree_in_mumble_order() {
471 let s = tree();
472 let names: Vec<&str> = s.children(0).iter().map(|c| c.name.as_str()).collect();
473 assert_eq!(names, ["AFK", "Raid", "Lobby"]);
474 assert_eq!(s.channels[&0].parent, None);
475 assert_eq!(s.child_by_name(0, "Raid").map(|c| c.id), Some(2));
476 }
477
478 #[test]
479 fn a_channel_cannot_become_its_own_parent() {
480 let mut s = tree();
481 s.apply_channel_state(proto::ChannelState {
482 channel_id: Some(2),
483 parent: Some(2),
484 ..Default::default()
485 });
486 assert_eq!(s.channels[&2].parent, Some(0));
487 }
488
489 #[test]
490 fn a_parent_change_cannot_form_a_loop() {
491 let mut s = tree();
492 s.apply_channel_state(channel(4, Some(2), "Team"));
493 s.apply_channel_state(channel(5, Some(4), "Squad"));
494 s.apply_channel_state(proto::ChannelState {
496 channel_id: Some(2),
497 parent: Some(5),
498 ..Default::default()
499 });
500 assert_eq!(s.channels[&2].parent, Some(0));
501 s.apply_channel_state(proto::ChannelState {
503 channel_id: Some(5),
504 parent: Some(3),
505 ..Default::default()
506 });
507 assert_eq!(s.channels[&5].parent, Some(3));
508 }
509
510 #[test]
511 fn a_loop_through_a_channel_not_yet_known_is_refused() {
512 let mut s = tree();
513 s.apply_channel_state(channel(6, Some(7), "Early"));
515 s.apply_channel_state(channel(7, Some(6), "Late"));
516 assert_eq!(s.channels[&7].parent, None);
517 assert_eq!(s.ancestry(6).count(), 2);
518 }
519
520 #[test]
521 fn ancestry_ends_on_a_cyclic_tree() {
522 let mut s = tree();
523 s.channels.get_mut(&2).expect("raid").parent = Some(2);
524 assert_eq!(s.ancestry(2).count(), s.channels.len());
525 }
526
527 #[test]
528 fn partial_channel_update_keeps_fields() {
529 let mut s = tree();
530 s.apply_channel_state(proto::ChannelState {
531 channel_id: Some(2),
532 description: Some("raid night".into()),
533 ..Default::default()
534 });
535 assert_eq!(s.channels[&2].name, "Raid");
536 assert_eq!(s.channels[&2].description.as_deref(), Some("raid night"));
537 }
538
539 #[test]
540 fn user_join_move_and_remove() {
541 let mut s = tree();
542 let c = s
543 .apply_user_state(proto::UserState {
544 session: Some(5),
545 name: Some("alice".into()),
546 channel_id: Some(1),
547 hash: Some("abc".into()),
548 ..Default::default()
549 })
550 .expect("change");
551 assert!(c.joined);
552 assert_eq!(c.moved_from, None);
553
554 let c = s
555 .apply_user_state(proto::UserState {
556 session: Some(5),
557 channel_id: Some(2),
558 self_mute: Some(true),
559 ..Default::default()
560 })
561 .expect("change");
562 assert!(!c.joined);
563 assert_eq!(c.moved_from, Some(1));
564 assert_eq!(c.user.name, "alice");
565 assert!(c.user.self_mute);
566 assert_eq!(c.user.cert_hash.as_deref(), Some("abc"));
567 assert_eq!(s.users_in(2).count(), 1);
568
569 assert!(s.apply_user_remove(5).is_some());
570 assert!(s.users.is_empty());
571 }
572
573 #[test]
574 fn listening_add_volume_remove() {
575 let mut s = tree();
576 s.apply_user_state(proto::UserState {
577 session: Some(1),
578 channel_id: Some(1),
579 listening_channel_add: vec![2, 3],
580 listening_volume_adjustment: vec![proto::user_state::VolumeAdjustment {
581 listening_channel: Some(2),
582 volume_adjustment: Some(0.0),
583 }],
584 ..Default::default()
585 });
586 assert_eq!(s.listeners_of(2).count(), 1);
587 assert_eq!(s.users[&1].listening_volume.get(&2), Some(&0.0));
588
589 s.apply_user_state(proto::UserState {
590 session: Some(1),
591 listening_channel_remove: vec![2],
592 ..Default::default()
593 });
594 assert_eq!(s.listeners_of(2).count(), 0);
595 assert!(s.users[&1].listening_volume.is_empty());
596
597 s.apply_channel_remove(3);
598 assert!(s.users[&1].listening.is_empty());
599 assert!(!s.channels.contains_key(&3));
600 }
601
602 #[test]
603 fn blobs_and_registration() {
604 let mut s = tree();
605 s.apply_user_state(proto::UserState {
606 session: Some(1),
607 comment: Some("hi".into()),
608 texture: Some(vec![1, 2]),
609 ..Default::default()
610 });
611 s.apply_user_state(proto::UserState {
613 session: Some(1),
614 comment_hash: Some(vec![9]),
615 texture: Some(vec![]),
616 user_id: Some(7),
617 ..Default::default()
618 });
619 let u = &s.users[&1];
620 assert_eq!(u.comment, None);
621 assert_eq!(u.texture, None);
622 assert_eq!(u.user_id, Some(7));
623 }
624
625 #[test]
626 fn sync_config_and_permissions() {
627 let mut s = ServerState::default();
628 s.apply_server_sync(&proto::ServerSync {
629 session: Some(3),
630 max_bandwidth: Some(72000),
631 welcome_text: Some("hello".into()),
632 permissions: Some(0x200),
633 });
634 s.apply_server_config(proto::ServerConfig {
635 message_length: Some(5000),
636 allow_html: Some(true),
637 ..Default::default()
638 });
639 s.apply_permission_query(&proto::PermissionQuery {
640 channel_id: Some(2),
641 permissions: Some(0x4),
642 flush: None,
643 });
644 assert_eq!(s.session, Some(3));
645 assert_eq!(s.server.max_bandwidth, Some(72000));
646 assert_eq!(s.server.message_length, Some(5000));
647 assert_eq!(s.permissions.get(&0), Some(&0x200));
648 assert_eq!(s.permissions.get(&2), Some(&0x4));
649 assert!(!s.server.supports_listening());
650 }
651}