freeswitch_log_parser/session/
conference.rs1use std::collections::{HashMap, HashSet};
9
10use freeswitch_types::variables::ConferenceVariable;
11
12use crate::message::MessageKind;
13use crate::stream::LogEntry;
14
15#[non_exhaustive]
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ConferenceMembership {
22 pub name: String,
24 pub profile: Option<String>,
27 pub instance: String,
29 pub member_id: Option<u32>,
32 pub conference_uuid: Option<String>,
35}
36
37pub(crate) struct ConferenceTarget {
39 pub name: String,
40 pub profile: Option<String>,
41}
42
43pub(crate) enum ConferenceEvent {
44 Join(ConferenceTarget),
45 Leave,
46}
47
48#[derive(Debug, Default)]
51pub(crate) struct ConferenceRegistry {
52 live: HashMap<String, Instance>,
53}
54
55#[derive(Debug)]
56struct Instance {
57 id: String,
58 members: HashSet<String>,
59}
60
61impl ConferenceRegistry {
62 pub(crate) fn instance_for(&self, name: &str) -> Option<&str> {
63 self.live.get(name).map(|i| i.id.as_str())
64 }
65
66 pub(crate) fn join(&mut self, name: &str, instance: &str, uuid: &str) {
67 self.live
68 .entry(name.to_string())
69 .or_insert_with(|| Instance {
70 id: instance.to_string(),
71 members: HashSet::new(),
72 })
73 .members
74 .insert(uuid.to_string());
75 }
76
77 pub(crate) fn leave(&mut self, name: &str, uuid: &str) {
78 if let Some(instance) = self.live.get_mut(name) {
79 instance.members.remove(uuid);
80 if instance.members.is_empty() {
81 self.live.remove(name);
82 }
83 }
84 }
85
86 pub(crate) fn members<'a>(&'a self, instance: &'a str) -> impl Iterator<Item = &'a str> {
87 self.live
88 .values()
89 .filter(move |i| i.id == instance)
90 .flat_map(|i| i.members.iter().map(String::as_str))
91 }
92}
93
94pub(crate) fn detect(entry: &LogEntry) -> Option<ConferenceEvent> {
96 if entry.message.starts_with("Channel leaving conference") {
97 return Some(ConferenceEvent::Leave);
98 }
99 if let MessageKind::Execute {
100 application,
101 arguments,
102 ..
103 } = &entry.message_kind
104 {
105 if application == "conference" {
106 return parse_target(arguments).map(ConferenceEvent::Join);
107 }
108 }
109 parse_transfer(&entry.message).map(ConferenceEvent::Join)
110}
111
112fn parse_transfer(msg: &str) -> Option<ConferenceTarget> {
116 if !msg.starts_with("Transfer ") {
117 return None;
118 }
119 let start = msg.find("[conference:")? + "[conference:".len();
120 let extension = msg[start..].strip_suffix(']')?;
121 let extension = match extension.rfind('@') {
122 Some(at) => &extension[..at],
123 None => extension,
124 };
125 parse_target(extension)
126}
127
128fn parse_target(arguments: &str) -> Option<ConferenceTarget> {
132 let mut spec = match arguments.find("+flags{") {
133 Some(at) => &arguments[..at],
134 None => arguments,
135 };
136 if let Some(rest) = spec.strip_prefix("bridge:") {
137 spec = rest.split_once(':').map(|(name, _)| name)?;
138 }
139 let spec = spec.trim_start_matches(' ');
140 let spec = match spec.split_once('+') {
141 Some((name, _pin)) => name,
142 None => spec,
143 };
144 let (name, profile) = match spec.rsplit_once('@') {
145 Some((name, profile)) => (name, Some(profile.to_string())),
146 None => (spec, None),
147 };
148 if name.is_empty() {
149 return None;
150 }
151 Some(ConferenceTarget {
152 name: name.to_string(),
153 profile,
154 })
155}
156
157pub(crate) fn target_from_variables(vars: &HashMap<String, String>) -> Option<ConferenceTarget> {
160 Some(ConferenceTarget {
161 name: vars
162 .get(ConferenceVariable::ConferenceName.as_str())?
163 .clone(),
164 profile: None,
165 })
166}
167
168pub(crate) fn refresh(membership: &mut ConferenceMembership, vars: &HashMap<String, String>) {
171 if let Some(member_id) = vars
172 .get(ConferenceVariable::ConferenceMemberId.as_str())
173 .and_then(|v| v.parse().ok())
174 {
175 membership.member_id = Some(member_id);
176 }
177 if let Some(uuid) = vars.get(ConferenceVariable::ConferenceUuid.as_str()) {
178 membership.conference_uuid = Some(uuid.clone());
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 fn target(arguments: &str) -> (String, Option<String>) {
187 let t = parse_target(arguments).expect("parsed");
188 (t.name, t.profile)
189 }
190
191 #[test]
192 fn bare_name() {
193 assert_eq!(target("835"), ("835".to_string(), None));
194 }
195
196 #[test]
197 fn name_with_profile() {
198 assert_eq!(
199 target("835@wideband"),
200 ("835".to_string(), Some("wideband".to_string()))
201 );
202 }
203
204 #[test]
205 fn pin_is_not_part_of_the_name() {
206 assert_eq!(target("835+1234"), ("835".to_string(), None));
207 assert_eq!(
208 target("835@wideband+1234"),
209 ("835".to_string(), Some("wideband".to_string()))
210 );
211 }
212
213 #[test]
214 fn flags_truncate_the_argument() {
215 assert_eq!(
216 target("835@wideband+flags{mute|deaf}"),
217 ("835".to_string(), Some("wideband".to_string()))
218 );
219 }
220
221 #[test]
222 fn bridge_form_names_the_conference_before_the_dialstring() {
223 assert_eq!(
224 target("bridge:835:sofia/internal/1000"),
225 ("835".to_string(), None)
226 );
227 }
228
229 #[test]
230 fn empty_argument_is_not_a_conference() {
231 assert!(parse_target("").is_none());
232 assert!(parse_target("@wideband").is_none());
233 }
234
235 #[test]
236 fn transfer_context_is_not_the_profile() {
237 let t = parse_transfer("Transfer loopback/tty-a to inline[conference:835@default]")
238 .expect("parsed");
239 assert_eq!(t.name, "835");
240 assert_eq!(t.profile, None);
241 }
242
243 #[test]
244 fn transfer_keeps_an_explicit_profile() {
245 let t = parse_transfer("Transfer loopback/tty-a to inline[conference:835@wideband@public]")
246 .expect("parsed");
247 assert_eq!(t.name, "835");
248 assert_eq!(t.profile.as_deref(), Some("wideband"));
249 }
250
251 #[test]
252 fn transfer_elsewhere_is_not_a_conference() {
253 assert!(parse_transfer("Transfer sofia/internal/1000 to inline[park@default]").is_none());
254 }
255
256 #[test]
257 fn instance_lives_only_while_it_has_members() {
258 let mut reg = ConferenceRegistry::default();
259 reg.join("835", "aaaaaaaa-0000-0000-0000-000000000001", "a");
260 reg.join("835", "aaaaaaaa-0000-0000-0000-000000000001", "b");
261 assert_eq!(
262 reg.instance_for("835"),
263 Some("aaaaaaaa-0000-0000-0000-000000000001")
264 );
265
266 reg.leave("835", "a");
267 assert_eq!(
268 reg.instance_for("835"),
269 Some("aaaaaaaa-0000-0000-0000-000000000001")
270 );
271 reg.leave("835", "b");
272 assert_eq!(reg.instance_for("835"), None);
273
274 reg.join("835", "aaaaaaaa-0000-0000-0000-000000000002", "c");
275 assert_eq!(
276 reg.instance_for("835"),
277 Some("aaaaaaaa-0000-0000-0000-000000000002")
278 );
279 }
280
281 #[test]
282 fn members_are_listed_per_instance() {
283 let mut reg = ConferenceRegistry::default();
284 reg.join("835", "aaaaaaaa-0000-0000-0000-000000000001", "a");
285 reg.join("835", "aaaaaaaa-0000-0000-0000-000000000001", "b");
286 reg.join("844", "aaaaaaaa-0000-0000-0000-000000000003", "c");
287
288 let mut members: Vec<&str> = reg
289 .members("aaaaaaaa-0000-0000-0000-000000000001")
290 .collect();
291 members.sort_unstable();
292 assert_eq!(members, ["a", "b"]);
293 assert_eq!(
294 reg.members("aaaaaaaa-0000-0000-0000-000000000003")
295 .collect::<Vec<_>>(),
296 ["c"]
297 );
298 }
299}