mumble_server_runtime_shard/
view.rs1use std::collections::{BTreeMap, BTreeSet};
12
13use crate::ids::{ActionKey, ChannelId, ChannelKey, Occupant, SessionId};
14use crate::scope::{Scope, ScopeSet};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Channel {
19 pub key: ChannelKey,
21 pub id: ChannelId,
22 pub parent: ChannelId,
24 pub scope: Scope,
26 pub name: String,
27 pub position: i32,
28 pub can_enter: bool,
31 pub can_text: bool,
36 pub links: BTreeSet<ChannelId>,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct UserFlags {
42 pub mute: bool,
43 pub deaf: bool,
44 pub suppress: bool,
45 pub self_mute: bool,
46 pub self_deaf: bool,
47 pub priority_speaker: bool,
48 pub recording: bool,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct User {
54 pub occupant: Occupant,
56 pub session: SessionId,
57 pub channel: ChannelId,
59 pub scope: Scope,
61 pub name: String,
62 pub flags: UserFlags,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Default)]
67pub struct ShardView {
68 pub channels: BTreeMap<ChannelId, Channel>,
69 pub users: BTreeMap<SessionId, User>,
70}
71
72impl ShardView {
73 #[must_use]
75 pub fn empty() -> ShardView {
76 ShardView::default()
77 }
78
79 #[must_use]
85 pub fn restrict(&self, see: ScopeSet) -> ShardView {
86 ShardView {
87 channels: self
88 .channels
89 .iter()
90 .filter(|(_, channel)| see.sees(channel.scope))
91 .map(|(id, channel)| (*id, channel.clone()))
92 .collect(),
93 users: self
94 .users
95 .iter()
96 .filter(|(_, user)| see.sees(user.scope))
97 .map(|(session, user)| (*session, user.clone()))
98 .collect(),
99 }
100 }
101
102 #[must_use]
108 pub fn compose(&self, overlay: &Overlay) -> ShardView {
109 let mut composed = self.clone();
110 composed
111 .channels
112 .extend(overlay.channels.iter().map(|(id, c)| (*id, c.clone())));
113 composed
114 .users
115 .extend(overlay.users.iter().map(|(s, u)| (*s, u.clone())));
116 composed
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Default)]
129pub struct Overlay {
130 pub channels: BTreeMap<ChannelId, Channel>,
131 pub users: BTreeMap<SessionId, User>,
132}
133
134impl Overlay {
135 #[must_use]
136 pub fn is_empty(&self) -> bool {
137 self.channels.is_empty() && self.users.is_empty()
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
152pub struct On(u32);
153
154impl On {
155 pub const SERVER: On = On(0x01);
157 pub const CHANNEL: On = On(0x02);
159 pub const USER: On = On(0x04);
161
162 #[must_use]
164 pub fn and(self, other: On) -> On {
165 On(self.0 | other.0)
166 }
167
168 #[must_use]
171 pub fn covers(self, place: On) -> bool {
172 self.0 & place.0 == place.0
173 }
174
175 #[must_use]
176 pub fn bits(self) -> u32 {
177 self.0
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct Action {
184 pub key: ActionKey,
185 pub text: String,
187 pub on: On,
188}
189
190pub type Actions = BTreeMap<ActionKey, Action>;
196
197#[cfg(test)]
198mod tests {
199 #![allow(clippy::expect_used)]
200
201 use super::*;
202 use crate::ids::ConnectionId;
203
204 fn scope(segments: &[u32]) -> Scope {
205 let mut scope = Scope::ROOT;
206 for segment in segments {
207 scope = scope.child(*segment).expect("within MAX_DEPTH");
208 }
209 scope
210 }
211
212 fn channel(id: u32, scope: Scope) -> Channel {
213 Channel {
214 key: ChannelKey(u64::from(id)),
215 id: ChannelId(id),
216 parent: ChannelId::ROOT,
217 scope,
218 name: format!("channel-{id}"),
219 position: 0,
220 can_enter: true,
221 can_text: true,
222 links: BTreeSet::new(),
223 }
224 }
225
226 fn user(session: u32, channel: u32, scope: Scope) -> User {
227 User {
228 occupant: Occupant::Connection(ConnectionId(u64::from(session))),
229 session: SessionId(session),
230 channel: ChannelId(channel),
231 scope,
232 name: format!("user-{session}"),
233 flags: UserFlags::default(),
234 }
235 }
236
237 fn sample() -> ShardView {
238 let mut view = ShardView::empty();
239 view.channels.insert(ChannelId(1), channel(1, scope(&[7])));
240 view.channels
241 .insert(ChannelId(2), channel(2, scope(&[7, 2])));
242 view.channels
243 .insert(ChannelId(3), channel(3, scope(&[7, 3])));
244 view.users
245 .insert(SessionId(10), user(10, 2, scope(&[7, 2])));
246 view.users
247 .insert(SessionId(11), user(11, 3, scope(&[7, 3])));
248 view
249 }
250
251 #[test]
252 fn restricting_keeps_exactly_the_comparable_elements() {
253 let view = sample();
254 let team_two = ScopeSet::new(&[scope(&[7, 2])]).expect("one scope");
255 let restricted = view.restrict(team_two);
256
257 assert!(restricted.channels.contains_key(&ChannelId(1)));
260 assert!(restricted.channels.contains_key(&ChannelId(2)));
261 assert!(!restricted.channels.contains_key(&ChannelId(3)));
262 assert!(restricted.users.contains_key(&SessionId(10)));
263 assert!(!restricted.users.contains_key(&SessionId(11)));
264 }
265
266 #[test]
267 fn a_restricted_view_never_strands_a_user_without_its_channel() {
268 let view = sample();
272 let observations = [
273 ScopeSet::new(&[scope(&[])]).expect("one scope"),
274 ScopeSet::new(&[scope(&[7])]).expect("one scope"),
275 ScopeSet::new(&[scope(&[7, 2])]).expect("one scope"),
276 ScopeSet::new(&[scope(&[7, 3])]).expect("one scope"),
277 ScopeSet::new(&[scope(&[8])]).expect("one scope"),
278 ];
279
280 for see in observations {
281 let restricted = view.restrict(see);
282 for user in restricted.users.values() {
283 assert!(
284 restricted.channels.contains_key(&user.channel),
285 "user {:?} lost its channel under {see:?}",
286 user.session
287 );
288 }
289 }
290 }
291
292 #[test]
293 fn composing_layers_the_overlay_over_the_shared_view() {
294 let view = sample();
295 let mut overlay = Overlay::default();
296 overlay
297 .users
298 .insert(SessionId(99), user(99, 2, scope(&[7, 2])));
299
300 let composed = view.compose(&overlay);
301 assert!(composed.users.contains_key(&SessionId(99)));
302 assert!(composed.users.contains_key(&SessionId(10)));
303 assert!(
304 !view.users.contains_key(&SessionId(99)),
305 "composing must not mutate the shared view"
306 );
307 }
308}