1use mumble_server_runtime_protocol::ControlMessage;
21use mumble_server_runtime_protocol::messages::tcp;
22
23use crate::ids::{ActionKey, ChannelId, SessionId};
24use crate::plan::{ChannelPatch, PlanOp, UserPatch};
25use crate::reply::Word;
26use crate::view::{Action, Actions, Channel, User};
27
28pub mod perm {
32 pub const TRAVERSE: u32 = 0x2;
33 pub const ENTER: u32 = 0x4;
34 pub const SPEAK: u32 = 0x8;
35 pub const WHISPER: u32 = 0x100;
36 pub const TEXT_MESSAGE: u32 = 0x200;
37
38 pub const DEFAULT: u32 = TRAVERSE | ENTER | SPEAK | WHISPER | TEXT_MESSAGE;
45}
46
47#[must_use]
59pub fn permissions_of(channel: &Channel) -> u32 {
60 let mut permissions = perm::DEFAULT;
61 if !channel.can_enter {
62 permissions &= !perm::ENTER;
63 }
64 if !channel.can_text {
65 permissions &= !perm::TEXT_MESSAGE;
66 }
67 permissions
68}
69
70#[must_use]
72pub fn permission_query(channel: &Channel) -> ControlMessage {
73 ControlMessage::PermissionQuery(tcp::PermissionQuery {
74 channel_id: Some(channel.id.0),
75 permissions: Some(permissions_of(channel)),
76 flush: Some(false),
82 })
83}
84
85#[must_use]
101pub fn user_stats(session: SessionId) -> ControlMessage {
102 ControlMessage::UserStats(tcp::UserStats {
103 session: Some(session.0),
104 ..Default::default()
105 })
106}
107
108#[must_use]
122pub fn spoken(to: SessionId, word: &Word) -> ControlMessage {
123 match word {
124 Word::Say(text) => ControlMessage::TextMessage(tcp::TextMessage {
125 session: vec![to.0],
126 message: text.clone(),
127 ..Default::default()
128 }),
129 Word::Refuse(reason) => ControlMessage::PermissionDenied(tcp::PermissionDenied {
130 session: Some(to.0),
131 reason: Some(reason.clone()),
132 r#type: Some(i32::from(tcp::permission_denied::DenyType::Text)),
133 ..Default::default()
134 }),
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum TextTarget {
150 Session(SessionId),
151 Channel(ChannelId),
152 Tree(ChannelId),
153}
154
155#[must_use]
171pub fn relayed(actor: Option<SessionId>, to: TextTarget, text: &str) -> ControlMessage {
172 let mut message = tcp::TextMessage {
173 actor: actor.map(|session| session.0),
174 message: text.to_owned(),
175 ..Default::default()
176 };
177 match to {
178 TextTarget::Session(session) => message.session = vec![session.0],
179 TextTarget::Channel(channel) => message.channel_id = vec![channel.0],
180 TextTarget::Tree(channel) => message.tree_id = vec![channel.0],
181 }
182 ControlMessage::TextMessage(message)
183}
184
185#[must_use]
196pub fn denied_permission(
197 session: SessionId,
198 channel: ChannelId,
199 permission: u32,
200) -> ControlMessage {
201 ControlMessage::PermissionDenied(tcp::PermissionDenied {
202 permission: Some(permission),
203 channel_id: Some(channel.0),
204 session: Some(session.0),
205 r#type: Some(i32::from(tcp::permission_denied::DenyType::Permission)),
206 ..Default::default()
207 })
208}
209
210#[must_use]
221pub fn action_name(key: ActionKey) -> String {
222 key.0.to_string()
223}
224
225#[must_use]
227pub fn action_key(name: &str) -> Option<ActionKey> {
228 name.parse::<u64>().ok().map(ActionKey)
229}
230
231#[must_use]
245pub fn actions(sent: &Actions, fresh: &Actions) -> Vec<ControlMessage> {
246 let mut withdrawn: Vec<ControlMessage> = Vec::new();
247 let mut offered: Vec<ControlMessage> = Vec::new();
248
249 for (key, action) in sent {
250 if fresh.get(key) != Some(action) {
251 withdrawn.push(withdraw_action(*key));
252 }
253 }
254 for (key, action) in fresh {
255 if sent.get(key) != Some(action) {
256 offered.push(offer_action(action));
257 }
258 }
259
260 withdrawn.extend(offered);
261 withdrawn
262}
263
264fn offer_action(action: &Action) -> ControlMessage {
265 ControlMessage::ContextActionModify(tcp::ContextActionModify {
266 action: action_name(action.key),
267 text: Some(action.text.clone()),
268 context: Some(action.on.bits()),
269 operation: Some(i32::from(tcp::context_action_modify::Operation::Add)),
270 })
271}
272
273fn withdraw_action(key: ActionKey) -> ControlMessage {
274 ControlMessage::ContextActionModify(tcp::ContextActionModify {
275 action: action_name(key),
276 text: None,
279 context: None,
280 operation: Some(i32::from(tcp::context_action_modify::Operation::Remove)),
281 })
282}
283
284#[must_use]
289pub fn emit(ops: &[PlanOp], self_session: SessionId) -> Vec<ControlMessage> {
290 self_first_within_user_additions(ops, self_session)
291 .into_iter()
292 .map(emit_op)
293 .collect()
294}
295
296fn self_first_within_user_additions(ops: &[PlanOp], self_session: SessionId) -> Vec<&PlanOp> {
299 let mut ordered: Vec<&PlanOp> = Vec::with_capacity(ops.len());
300 let mut index = 0;
301
302 while index < ops.len() {
303 let Some(op) = ops.get(index) else { break };
304 if !matches!(op, PlanOp::AddUser(_)) {
305 ordered.push(op);
306 index += 1;
307 continue;
308 }
309
310 let start = index;
311 while ops
312 .get(index)
313 .is_some_and(|op| matches!(op, PlanOp::AddUser(_)))
314 {
315 index += 1;
316 }
317 let Some(run) = ops.get(start..index) else {
318 break;
319 };
320
321 ordered.extend(run.iter().filter(|op| is_self_addition(op, self_session)));
324 ordered.extend(run.iter().filter(|op| !is_self_addition(op, self_session)));
325 }
326
327 ordered
328}
329
330fn is_self_addition(op: &PlanOp, self_session: SessionId) -> bool {
331 matches!(op, PlanOp::AddUser(user) if user.session == self_session)
332}
333
334fn emit_op(op: &PlanOp) -> ControlMessage {
337 match op {
338 PlanOp::CreateChannel(channel) => ControlMessage::ChannelState(created_channel(channel)),
339 PlanOp::UpdateChannel(patch) => ControlMessage::ChannelState(patched_channel(patch)),
340 PlanOp::RemoveChannel(channel) => ControlMessage::ChannelRemove(tcp::ChannelRemove {
341 channel_id: channel.0,
342 }),
343 PlanOp::AddUser(user) => ControlMessage::UserState(added_user(user)),
344 PlanOp::UpdateUser(patch) => ControlMessage::UserState(patched_user(patch)),
345 PlanOp::MoveUser { session, channel } => ControlMessage::UserState(tcp::UserState {
346 session: Some(session.0),
347 channel_id: Some(channel.0),
348 ..Default::default()
349 }),
350 PlanOp::RemoveUser(session) => ControlMessage::UserRemove(tcp::UserRemove {
351 session: session.0,
352 ..Default::default()
353 }),
354 }
355}
356
357fn created_channel(channel: &Channel) -> tcp::ChannelState {
366 let parent = (channel.id != ChannelId::ROOT).then_some(channel.parent.0);
367
368 tcp::ChannelState {
369 channel_id: Some(channel.id.0),
370 parent,
371 name: Some(channel.name.clone()),
372 position: Some(channel.position),
373 temporary: Some(false),
374 can_enter: Some(channel.can_enter),
375 links: channel.links.iter().map(|id| id.0).collect(),
376 ..Default::default()
377 }
378}
379
380fn patched_channel(patch: &ChannelPatch) -> tcp::ChannelState {
391 tcp::ChannelState {
392 channel_id: Some(patch.id.0),
393 parent: patch.parent.map(|id| id.0),
394 name: patch.name.clone(),
395 position: patch.position,
396 can_enter: patch.can_enter,
397 links_add: patch.links_added.iter().map(|id| id.0).collect(),
398 links_remove: patch.links_removed.iter().map(|id| id.0).collect(),
399 ..Default::default()
400 }
401}
402
403fn added_user(user: &User) -> tcp::UserState {
409 let flags = user.flags;
410 tcp::UserState {
411 session: Some(user.session.0),
412 name: Some(user.name.clone()),
413 channel_id: Some(user.channel.0),
414 mute: Some(flags.mute),
415 deaf: Some(flags.deaf),
416 suppress: Some(flags.suppress),
417 self_mute: Some(flags.self_mute),
418 self_deaf: Some(flags.self_deaf),
419 priority_speaker: Some(flags.priority_speaker),
420 recording: Some(flags.recording),
421 ..Default::default()
422 }
423}
424
425fn patched_user(patch: &UserPatch) -> tcp::UserState {
427 let flags = patch.flags.unwrap_or_default();
428 let set = patch.flags.is_some();
429
430 tcp::UserState {
431 session: Some(patch.session.0),
432 name: patch.name.clone(),
433 mute: set.then_some(flags.mute),
434 deaf: set.then_some(flags.deaf),
435 suppress: set.then_some(flags.suppress),
436 self_mute: set.then_some(flags.self_mute),
437 self_deaf: set.then_some(flags.self_deaf),
438 priority_speaker: set.then_some(flags.priority_speaker),
439 recording: set.then_some(flags.recording),
440 ..Default::default()
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 #![allow(clippy::expect_used)]
447
448 use std::collections::BTreeSet;
449
450 use super::*;
451 use crate::ids::{ChannelKey, ConnectionId, Occupant};
452 use crate::scope::Scope;
453 use crate::view::On;
454 use crate::view::UserFlags;
455
456 fn channel(id: u32, parent: u32) -> Channel {
457 Channel {
458 key: ChannelKey(u64::from(id)),
459 id: ChannelId(id),
460 parent: ChannelId(parent),
461 scope: Scope::ROOT,
462 name: format!("channel-{id}"),
463 position: 0,
464 can_enter: true,
465 can_text: true,
466 links: BTreeSet::new(),
467 }
468 }
469
470 fn user(session: u32, channel: u32) -> User {
471 User {
472 occupant: Occupant::Connection(ConnectionId(u64::from(session))),
473 session: SessionId(session),
474 channel: ChannelId(channel),
475 scope: Scope::ROOT,
476 name: format!("user-{session}"),
477 flags: UserFlags::default(),
478 }
479 }
480
481 fn user_sessions(messages: &[ControlMessage]) -> Vec<u32> {
482 messages
483 .iter()
484 .filter_map(|message| match message {
485 ControlMessage::UserState(state) => state.session,
486 _ => None,
487 })
488 .collect()
489 }
490
491 #[test]
492 fn a_channel_nobody_may_enter_is_advertised_without_the_enter_bit() {
493 let mut open = channel(1, 0);
494 assert_eq!(permissions_of(&open), perm::DEFAULT);
495
496 open.can_enter = false;
497 let closed = permissions_of(&open);
498 assert_eq!(
499 closed & perm::ENTER,
500 0,
501 "the client must not offer to enter"
502 );
503 assert_eq!(
504 closed & perm::TRAVERSE,
505 perm::TRAVERSE,
506 "a channel it can see is a channel it has traversed"
507 );
508 }
509
510 #[test]
511 fn a_read_only_channel_is_advertised_without_the_text_bit() {
512 let mut quiet = channel(1, 0);
513 quiet.can_text = false;
514
515 let permissions = permissions_of(&quiet);
516 assert_eq!(
517 permissions & perm::TEXT_MESSAGE,
518 0,
519 "the client must grey its chat box out rather than be refused later"
520 );
521 assert_eq!(
522 permissions & perm::SPEAK,
523 perm::SPEAK,
524 "being unable to write is not being unable to talk"
525 );
526 }
527
528 fn offered(key: u64, text: &str, on: On) -> (ActionKey, Action) {
529 let key = ActionKey(key);
530 (
531 key,
532 Action {
533 key,
534 text: text.to_owned(),
535 on,
536 },
537 )
538 }
539
540 fn modifications(messages: &[ControlMessage]) -> Vec<(String, Option<String>, i32)> {
541 messages
542 .iter()
543 .map(|message| match message {
544 ControlMessage::ContextActionModify(modify) => (
545 modify.action.clone(),
546 modify.text.clone(),
547 modify.operation.unwrap_or_default(),
548 ),
549 other => panic!("expected a context action, got {other:?}"),
550 })
551 .collect()
552 }
553
554 const ADD: i32 = tcp::context_action_modify::Operation::Add as i32;
555 const REMOVE: i32 = tcp::context_action_modify::Operation::Remove as i32;
556
557 #[test]
558 fn an_unchanged_offer_says_nothing() {
559 let fresh: Actions = [offered(1, "Kick", On::USER)].into_iter().collect();
560 assert!(
561 actions(&fresh.clone(), &fresh).is_empty(),
562 "a menu that did not move must cost nothing"
563 );
564 }
565
566 #[test]
567 fn a_new_action_is_offered_and_a_dropped_one_withdrawn() {
568 let sent: Actions = [offered(1, "Kick", On::USER)].into_iter().collect();
569 let fresh: Actions = [offered(2, "Start", On::SERVER)].into_iter().collect();
570
571 assert_eq!(
572 modifications(&actions(&sent, &fresh)),
573 vec![
574 ("1".to_owned(), None, REMOVE),
575 ("2".to_owned(), Some("Start".to_owned()), ADD),
576 ],
577 "withdrawals come first, so a rename cannot race its own removal"
578 );
579 }
580
581 #[test]
582 fn a_relabelled_action_is_withdrawn_before_being_offered_again() {
583 let sent: Actions = [offered(1, "Join", On::SERVER)].into_iter().collect();
587 let fresh: Actions = [offered(1, "Join the arena", On::SERVER)]
588 .into_iter()
589 .collect();
590
591 assert_eq!(
592 modifications(&actions(&sent, &fresh)),
593 vec![
594 ("1".to_owned(), None, REMOVE),
595 ("1".to_owned(), Some("Join the arena".to_owned()), ADD),
596 ]
597 );
598 }
599
600 #[test]
601 fn a_place_change_travels_like_a_relabelling() {
602 let sent: Actions = [offered(1, "Poke", On::USER)].into_iter().collect();
603 let fresh: Actions = [offered(1, "Poke", On::USER.and(On::CHANNEL))]
604 .into_iter()
605 .collect();
606
607 let messages = actions(&sent, &fresh);
608 assert_eq!(messages.len(), 2, "{messages:?}");
609 match messages.last() {
610 Some(ControlMessage::ContextActionModify(modify)) => assert_eq!(
611 modify.context,
612 Some(On::USER.and(On::CHANNEL).bits()),
613 "the offer must carry every place it is offered in"
614 ),
615 other => panic!("expected an offer, got {other:?}"),
616 }
617 }
618
619 #[test]
620 fn an_action_name_reads_back_and_nothing_else_does() {
621 assert_eq!(action_key(&action_name(ActionKey(7))), Some(ActionKey(7)));
622 assert_eq!(
623 action_key(&action_name(ActionKey(u64::MAX))),
624 Some(ActionKey(u64::MAX))
625 );
626 for hostile in ["", "1;drop", "-1", " 1", "0x1", "99999999999999999999999"] {
627 assert_eq!(
628 action_key(hostile),
629 None,
630 "{hostile:?} is not a name this server writes"
631 );
632 }
633 }
634
635 #[test]
636 fn a_permission_answer_never_flushes_the_client_s_cache() {
637 let ControlMessage::PermissionQuery(answer) = permission_query(&channel(4, 0)) else {
638 panic!("expected a PermissionQuery");
639 };
640 assert_eq!(answer.channel_id, Some(4));
641 assert_eq!(answer.permissions, Some(perm::DEFAULT));
642 assert_eq!(
643 answer.flush,
644 Some(false),
645 "answering one question must not invalidate every other channel"
646 );
647 }
648
649 #[test]
650 fn stats_about_somebody_else_carry_the_session_and_nothing_more() {
651 let ControlMessage::UserStats(answer) = user_stats(SessionId(7)) else {
652 panic!("expected a UserStats");
653 };
654 assert_eq!(answer.session, Some(7));
655 assert!(answer.certificates.is_empty(), "no certificate ever leaves");
656 assert_eq!(answer.address, None, "no address ever leaves");
657 assert_eq!(answer.version, None);
658 assert_eq!(answer.from_client, None, "no counters about a third party");
659 assert_eq!(
660 answer.onlinesecs, None,
661 "no connection time about a third party"
662 );
663 }
664
665 #[test]
666 fn the_self_user_is_introduced_before_the_others() {
667 let messages = emit(
668 &[
669 PlanOp::AddUser(user(7, 0)),
670 PlanOp::AddUser(user(9, 0)),
671 PlanOp::AddUser(user(3, 0)),
672 ],
673 SessionId(3),
674 );
675 assert_eq!(user_sessions(&messages), vec![3, 7, 9]);
676 }
677
678 #[test]
679 fn the_self_user_is_not_hoisted_past_the_channel_it_lives_in() {
680 let messages = emit(
681 &[
682 PlanOp::CreateChannel(channel(0, 0)),
683 PlanOp::CreateChannel(channel(1, 0)),
684 PlanOp::AddUser(user(7, 1)),
685 PlanOp::AddUser(user(3, 1)),
686 ],
687 SessionId(3),
688 );
689
690 let kinds: Vec<&str> = messages
691 .iter()
692 .map(|message| match message {
693 ControlMessage::ChannelState(_) => "channel",
694 ControlMessage::UserState(_) => "user",
695 _ => "other",
696 })
697 .collect();
698 assert_eq!(kinds, vec!["channel", "channel", "user", "user"]);
699 }
700
701 #[test]
702 fn a_plan_without_the_self_user_passes_through_unchanged() {
703 let ops = [PlanOp::AddUser(user(7, 0)), PlanOp::AddUser(user(9, 0))];
704 let messages = emit(&ops, SessionId(42));
705 assert_eq!(user_sessions(&messages), vec![7, 9]);
706 }
707
708 #[test]
709 fn the_root_channel_carries_no_parent() {
710 let messages = emit(&[PlanOp::CreateChannel(channel(0, 0))], SessionId(1));
711 let ControlMessage::ChannelState(state) = &messages[0] else {
712 panic!("expected a ChannelState");
713 };
714 assert_eq!(state.parent, None, "a self-parented root is a cycle");
715 assert_eq!(state.channel_id, Some(0));
716 }
717
718 #[test]
719 fn a_child_channel_carries_its_parent_and_name() {
720 let messages = emit(&[PlanOp::CreateChannel(channel(4, 1))], SessionId(1));
723 let ControlMessage::ChannelState(state) = &messages[0] else {
724 panic!("expected a ChannelState");
725 };
726 assert_eq!(state.parent, Some(1));
727 assert_eq!(state.name.as_deref(), Some("channel-4"));
728 }
729
730 #[test]
731 fn link_changes_ride_as_incremental_lists() {
732 let patch = ChannelPatch {
733 id: ChannelId(1),
734 parent: None,
735 name: None,
736 position: None,
737 can_enter: None,
738 links_added: BTreeSet::from([ChannelId(4)]),
739 links_removed: BTreeSet::from([ChannelId(2), ChannelId(3)]),
740 };
741 let messages = emit(&[PlanOp::UpdateChannel(patch)], SessionId(1));
742 let ControlMessage::ChannelState(state) = &messages[0] else {
743 panic!("expected a ChannelState");
744 };
745
746 assert_eq!(state.links_add, vec![4]);
747 assert_eq!(state.links_remove, vec![2, 3]);
748 assert!(
749 state.links.is_empty(),
750 "an empty `links` is a no-op client-side, so clearing must use links_remove"
751 );
752 }
753
754 #[test]
755 fn a_move_carries_only_the_session_and_the_channel() {
756 let messages = emit(
757 &[PlanOp::MoveUser {
758 session: SessionId(5),
759 channel: ChannelId(9),
760 }],
761 SessionId(1),
762 );
763 let ControlMessage::UserState(state) = &messages[0] else {
764 panic!("expected a UserState");
765 };
766 assert_eq!(state.session, Some(5));
767 assert_eq!(state.channel_id, Some(9));
768 assert_eq!(state.name, None, "a move must not restate the name");
769 }
770
771 #[test]
772 fn a_patch_with_no_flag_change_leaves_the_flags_unset() {
773 let patch = UserPatch {
774 session: SessionId(5),
775 name: Some("renamed".to_owned()),
776 flags: None,
777 };
778 let messages = emit(&[PlanOp::UpdateUser(patch)], SessionId(1));
779 let ControlMessage::UserState(state) = &messages[0] else {
780 panic!("expected a UserState");
781 };
782 assert_eq!(state.name.as_deref(), Some("renamed"));
783 assert_eq!(
784 state.mute, None,
785 "sending a default flag would clear a flag nobody touched"
786 );
787 }
788
789 #[test]
790 fn removals_use_the_dedicated_messages() {
791 let messages = emit(
792 &[
793 PlanOp::RemoveUser(SessionId(4)),
794 PlanOp::RemoveChannel(ChannelId(9)),
795 ],
796 SessionId(1),
797 );
798 match messages.as_slice() {
799 [
800 ControlMessage::UserRemove(removed),
801 ControlMessage::ChannelRemove(gone),
802 ] => {
803 assert_eq!(removed.session, 4);
804 assert_eq!(gone.channel_id, 9);
805 }
806 other => panic!("unexpected emission: {other:?}"),
807 }
808 }
809}