1use super::*;
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum ConferenceMenuFamily {
8 Menu,
9 IconMenu,
10}
11
12#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct ConferenceListEntry {
15 pub participant_id: ParticipantId,
16 pub name: String,
17 pub number: String,
18 pub moderator: bool,
19 pub muted: bool,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum ConferenceListAction {
25 Participant {
27 conference_id: ConferenceId,
28 participant_id: ParticipantId,
29 },
30 Mute {
31 conference_id: ConferenceId,
32 participant_id: ParticipantId,
33 },
34 Unmute {
35 conference_id: ConferenceId,
36 participant_id: ParticipantId,
37 },
38 Remove {
39 conference_id: ConferenceId,
40 participant_id: ParticipantId,
41 },
42 Promote {
43 conference_id: ConferenceId,
44 participant_id: ParticipantId,
45 },
46 Demote {
47 conference_id: ConferenceId,
48 participant_id: ParticipantId,
49 },
50 End {
51 conference_id: ConferenceId,
52 },
53}
54
55impl ConferenceListAction {
56 pub const APPLICATION_ID: u32 = 9091;
58
59 pub fn url(self) -> String {
61 match self {
62 Self::Participant {
63 conference_id,
64 participant_id,
65 } => format!(
66 "UserData:{}:0:conference/{}/participant/{}",
67 Self::APPLICATION_ID,
68 conference_id.get(),
69 participant_id.get()
70 ),
71 Self::Mute {
72 conference_id,
73 participant_id,
74 } => format!(
75 "UserData:{}:0:conference/{}/participant/{}/mute",
76 Self::APPLICATION_ID,
77 conference_id.get(),
78 participant_id.get()
79 ),
80 Self::Unmute {
81 conference_id,
82 participant_id,
83 } => format!(
84 "UserData:{}:0:conference/{}/participant/{}/unmute",
85 Self::APPLICATION_ID,
86 conference_id.get(),
87 participant_id.get()
88 ),
89 Self::Remove {
90 conference_id,
91 participant_id,
92 } => format!(
93 "UserData:{}:0:conference/{}/participant/{}/remove",
94 Self::APPLICATION_ID,
95 conference_id.get(),
96 participant_id.get()
97 ),
98 Self::Promote {
99 conference_id,
100 participant_id,
101 } => format!(
102 "UserData:{}:0:conference/{}/participant/{}/promote",
103 Self::APPLICATION_ID,
104 conference_id.get(),
105 participant_id.get()
106 ),
107 Self::Demote {
108 conference_id,
109 participant_id,
110 } => format!(
111 "UserData:{}:0:conference/{}/participant/{}/demote",
112 Self::APPLICATION_ID,
113 conference_id.get(),
114 participant_id.get()
115 ),
116 Self::End { conference_id } => format!(
117 "UserData:{}:0:conference/{}/end",
118 Self::APPLICATION_ID,
119 conference_id.get()
120 ),
121 }
122 }
123
124 pub fn parse(value: &str) -> Option<Self> {
126 let path = value
127 .trim_matches(['\0', ' ', '\r', '\n'])
128 .strip_prefix(&format!("UserData:{}:0:", Self::APPLICATION_ID))
129 .unwrap_or(value)
130 .strip_prefix("conference/")?;
131 let segments: Vec<_> = path.split('/').collect();
132 let [conference_id, action, rest @ ..] = segments.as_slice() else {
133 return None;
134 };
135 let conference_id = ConferenceId::new(conference_id.parse().ok()?);
136 match (*action, rest) {
137 ("participant", [participant]) => Some(Self::Participant {
138 conference_id,
139 participant_id: ParticipantId::new(participant.parse().ok()?),
140 }),
141 ("participant", [participant, "mute"]) => Some(Self::Mute {
142 conference_id,
143 participant_id: ParticipantId::new(participant.parse().ok()?),
144 }),
145 ("participant", [participant, "unmute"]) => Some(Self::Unmute {
146 conference_id,
147 participant_id: ParticipantId::new(participant.parse().ok()?),
148 }),
149 ("participant", [participant, "remove"]) => Some(Self::Remove {
150 conference_id,
151 participant_id: ParticipantId::new(participant.parse().ok()?),
152 }),
153 ("participant", [participant, "promote"]) => Some(Self::Promote {
154 conference_id,
155 participant_id: ParticipantId::new(participant.parse().ok()?),
156 }),
157 ("participant", [participant, "demote"]) => Some(Self::Demote {
158 conference_id,
159 participant_id: ParticipantId::new(participant.parse().ok()?),
160 }),
161 ("end", []) => Some(Self::End { conference_id }),
162 _ => None,
163 }
164 }
165
166 pub fn from_route(route: &[String]) -> Option<Self> {
168 let [conference, conference_id, action, rest @ ..] = route else {
169 return None;
170 };
171 if conference != "conference" {
172 return None;
173 }
174 let conference_id = ConferenceId::new(conference_id.parse().ok()?);
175 match (action.as_str(), rest) {
176 ("participant", [participant]) => Some(Self::Participant {
177 conference_id,
178 participant_id: ParticipantId::new(participant.parse().ok()?),
179 }),
180 ("participant", [participant, operation]) if operation == "mute" => Some(Self::Mute {
181 conference_id,
182 participant_id: ParticipantId::new(participant.parse().ok()?),
183 }),
184 ("participant", [participant, operation]) if operation == "unmute" => {
185 Some(Self::Unmute {
186 conference_id,
187 participant_id: ParticipantId::new(participant.parse().ok()?),
188 })
189 }
190 ("participant", [participant, operation]) if operation == "remove" => {
191 Some(Self::Remove {
192 conference_id,
193 participant_id: ParticipantId::new(participant.parse().ok()?),
194 })
195 }
196 ("participant", [participant, operation]) if operation == "promote" => {
197 Some(Self::Promote {
198 conference_id,
199 participant_id: ParticipantId::new(participant.parse().ok()?),
200 })
201 }
202 ("participant", [participant, operation]) if operation == "demote" => {
203 Some(Self::Demote {
204 conference_id,
205 participant_id: ParticipantId::new(participant.parse().ok()?),
206 })
207 }
208 ("end", []) => Some(Self::End { conference_id }),
209 _ => None,
210 }
211 }
212}
213
214#[derive(Clone, Debug, Eq, PartialEq)]
216pub enum ConferenceListDocument {
217 Menu(CiscoIpPhoneMenu),
218 IconMenu(CiscoIpPhoneIconMenu),
219}
220
221impl ConferenceListDocument {
222 pub fn new(
224 conference_id: ConferenceId,
225 participants: &[ConferenceListEntry],
226 family: ConferenceMenuFamily,
227 ) -> Result<Self, PhoneXmlError> {
228 if participants.len() > CONFERENCE_LIST_MAX_PARTICIPANTS {
229 return Err(PhoneXmlError::LimitExceeded {
230 kind: "conference participants",
231 actual: participants.len(),
232 maximum: CONFERENCE_LIST_MAX_PARTICIPANTS,
233 });
234 }
235 let title = format!("Conference {}", conference_id.get());
236 let prompt = if participants.is_empty() {
237 "No participants".to_owned()
238 } else {
239 "Select a participant".to_owned()
240 };
241 match family {
242 ConferenceMenuFamily::Menu => CiscoIpPhoneMenu::new(
243 title,
244 prompt,
245 participants
246 .iter()
247 .map(|participant| CiscoIpPhoneMenuItem {
248 name: Some(conference_participant_label(participant)),
249 url: Some(
250 ConferenceListAction::Participant {
251 conference_id,
252 participant_id: participant.participant_id,
253 }
254 .url(),
255 ),
256 })
257 .chain(std::iter::once(CiscoIpPhoneMenuItem {
258 name: Some("End conference".into()),
259 url: Some(ConferenceListAction::End { conference_id }.url()),
260 }))
261 .collect(),
262 )
263 .map(Self::Menu),
264 ConferenceMenuFamily::IconMenu => CiscoIpPhoneIconMenu::new(
265 title,
266 prompt,
267 participants
268 .iter()
269 .map(|participant| CiscoIpPhoneIconMenuItem {
270 name: Some(conference_participant_label(participant)),
271 url: Some(
272 ConferenceListAction::Participant {
273 conference_id,
274 participant_id: participant.participant_id,
275 }
276 .url(),
277 ),
278 icon_index: Some(u16::from(participant.moderator)),
279 })
280 .chain(std::iter::once(CiscoIpPhoneIconMenuItem {
281 name: Some("End conference".into()),
282 url: Some(ConferenceListAction::End { conference_id }.url()),
283 icon_index: Some(0),
284 }))
285 .collect(),
286 conference_icons(),
287 )
288 .map(Self::IconMenu),
289 }
290 }
291
292 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
294 match self {
295 Self::Menu(document) => document.to_xml_with_limit(CONFERENCE_LIST_MAX_BYTES),
296 Self::IconMenu(document) => document.to_xml_with_limit(CONFERENCE_LIST_MAX_BYTES),
297 }
298 }
299
300 pub fn from_xml(document: &[u8], family: ConferenceMenuFamily) -> Result<Self, PhoneXmlError> {
302 match family {
303 ConferenceMenuFamily::Menu => {
304 CiscoIpPhoneMenu::from_xml_with_limit(document, CONFERENCE_LIST_MAX_BYTES)
305 .map(Self::Menu)
306 }
307 ConferenceMenuFamily::IconMenu => {
308 CiscoIpPhoneIconMenu::from_xml_with_limit(document, CONFERENCE_LIST_MAX_BYTES)
309 .map(Self::IconMenu)
310 }
311 }
312 }
313
314 pub fn actions(&self) -> impl Iterator<Item = ConferenceListAction> + '_ {
316 let urls: Box<dyn Iterator<Item = &str>> = match self {
317 Self::Menu(document) => {
318 Box::new(document.items.iter().filter_map(|item| item.url.as_deref()))
319 }
320 Self::IconMenu(document) => {
321 Box::new(document.items.iter().filter_map(|item| item.url.as_deref()))
322 }
323 };
324 urls.filter_map(ConferenceListAction::parse)
325 }
326}
327
328#[derive(Clone, Debug, Eq, PartialEq)]
330pub enum ConferenceParticipantActionsDocument {
331 Menu(CiscoIpPhoneMenu),
332 IconMenu(CiscoIpPhoneIconMenu),
333}
334
335impl ConferenceParticipantActionsDocument {
336 pub fn new(
341 conference_id: ConferenceId,
342 participant: &ConferenceListEntry,
343 removable: bool,
344 demotable: bool,
345 family: ConferenceMenuFamily,
346 ) -> Result<Self, PhoneXmlError> {
347 let mut actions = Vec::new();
348 if participant.moderator {
349 if demotable {
350 actions.push((
351 "Demote",
352 ConferenceListAction::Demote {
353 conference_id,
354 participant_id: participant.participant_id,
355 },
356 ));
357 }
358 } else {
359 let (toggle_name, toggle) = if participant.muted {
360 (
361 "Unmute",
362 ConferenceListAction::Unmute {
363 conference_id,
364 participant_id: participant.participant_id,
365 },
366 )
367 } else {
368 (
369 "Mute",
370 ConferenceListAction::Mute {
371 conference_id,
372 participant_id: participant.participant_id,
373 },
374 )
375 };
376 actions.push((toggle_name, toggle));
377 if removable {
378 actions.push((
379 "Remove",
380 ConferenceListAction::Remove {
381 conference_id,
382 participant_id: participant.participant_id,
383 },
384 ));
385 }
386 actions.push((
387 "Promote",
388 ConferenceListAction::Promote {
389 conference_id,
390 participant_id: participant.participant_id,
391 },
392 ));
393 }
394 let title = format!("Participant {}", participant.participant_id.get());
395 match family {
396 ConferenceMenuFamily::Menu => CiscoIpPhoneMenu::new(
397 title,
398 "Choose an action",
399 actions
400 .into_iter()
401 .map(|(name, action)| CiscoIpPhoneMenuItem {
402 name: Some(name.into()),
403 url: Some(action.url()),
404 })
405 .collect(),
406 )
407 .map(Self::Menu),
408 ConferenceMenuFamily::IconMenu => CiscoIpPhoneIconMenu::new(
409 title,
410 "Choose an action",
411 actions
412 .into_iter()
413 .map(|(name, action)| CiscoIpPhoneIconMenuItem {
414 name: Some(name.into()),
415 url: Some(action.url()),
416 icon_index: None,
417 })
418 .collect(),
419 Vec::new(),
420 )
421 .map(Self::IconMenu),
422 }
423 }
424
425 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
427 match self {
428 Self::Menu(document) => document.to_xml_with_limit(CONFERENCE_LIST_MAX_BYTES),
429 Self::IconMenu(document) => document.to_xml_with_limit(CONFERENCE_LIST_MAX_BYTES),
430 }
431 }
432
433 pub fn from_xml(document: &[u8], family: ConferenceMenuFamily) -> Result<Self, PhoneXmlError> {
435 match family {
436 ConferenceMenuFamily::Menu => {
437 CiscoIpPhoneMenu::from_xml_with_limit(document, CONFERENCE_LIST_MAX_BYTES)
438 .map(Self::Menu)
439 }
440 ConferenceMenuFamily::IconMenu => {
441 CiscoIpPhoneIconMenu::from_xml_with_limit(document, CONFERENCE_LIST_MAX_BYTES)
442 .map(Self::IconMenu)
443 }
444 }
445 }
446
447 pub fn actions(&self) -> impl Iterator<Item = ConferenceListAction> + '_ {
449 let urls: Box<dyn Iterator<Item = &str>> = match self {
450 Self::Menu(document) => {
451 Box::new(document.items.iter().filter_map(|item| item.url.as_deref()))
452 }
453 Self::IconMenu(document) => {
454 Box::new(document.items.iter().filter_map(|item| item.url.as_deref()))
455 }
456 };
457 urls.filter_map(ConferenceListAction::parse)
458 }
459}
460
461pub(super) fn conference_participant_label(participant: &ConferenceListEntry) -> String {
462 let identity = if !participant.name.trim().is_empty() {
463 participant.name.trim()
464 } else if !participant.number.trim().is_empty() {
465 participant.number.trim()
466 } else {
467 "Unknown participant"
468 };
469 let role = if participant.moderator {
470 "Moderator"
471 } else {
472 "Participant"
473 };
474 let mute = if participant.muted { ", muted" } else { "" };
475 format!("{identity} ({role}{mute})")
476}
477
478pub(super) fn conference_icons() -> Vec<CiscoIpPhoneIconItem> {
479 vec![
480 CiscoIpPhoneIconItem {
481 index: 0,
482 width: 10,
483 height: 10,
484 depth: 2,
485 data: Some("00000000000000000000000000".into()),
486 },
487 CiscoIpPhoneIconItem {
488 index: 1,
489 width: 10,
490 height: 10,
491 depth: 2,
492 data: Some("00000155415555554155000000".into()),
493 },
494 ]
495}