1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum SdpDirection {
6 Local,
7 LocalRing,
9 Remote,
10 Unknown,
12}
13
14impl fmt::Display for SdpDirection {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 match self {
17 SdpDirection::Local => f.pad("local"),
18 SdpDirection::LocalRing => f.pad("local-ring"),
19 SdpDirection::Remote => f.pad("remote"),
20 SdpDirection::Unknown => f.pad("unknown"),
21 }
22 }
23}
24
25#[non_exhaustive]
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum MessageKind {
32 Execute {
34 depth: u32,
35 channel: String,
36 application: String,
37 arguments: String,
38 },
39 Dialplan { channel: String, detail: String },
41 ChannelData,
43 ChannelField { name: String, value: String },
45 Variable { name: String, value: String },
47 SdpMarker { direction: SdpDirection },
49 StateChange { detail: String },
51 CodecNegotiation,
53 Media { detail: String },
55 ChannelLifecycle { detail: String },
57 EventSocket { detail: String },
59 General,
61 FileChange,
63 DateChange,
65}
66
67impl MessageKind {
68 pub const ALL_LABELS: &[&str] = &[
70 "execute",
71 "dialplan",
72 "channel-data",
73 "channel-field",
74 "variable",
75 "sdp-marker",
76 "state-change",
77 "codec-negotiation",
78 "media",
79 "channel-lifecycle",
80 "event-socket",
81 "general",
82 "file-change",
83 "date-change",
84 ];
85
86 pub fn label(&self) -> &'static str {
88 match self {
89 MessageKind::Execute { .. } => "execute",
90 MessageKind::Dialplan { .. } => "dialplan",
91 MessageKind::ChannelData => "channel-data",
92 MessageKind::ChannelField { .. } => "channel-field",
93 MessageKind::Variable { .. } => "variable",
94 MessageKind::SdpMarker { .. } => "sdp-marker",
95 MessageKind::StateChange { .. } => "state-change",
96 MessageKind::CodecNegotiation => "codec-negotiation",
97 MessageKind::Media { .. } => "media",
98 MessageKind::ChannelLifecycle { .. } => "channel-lifecycle",
99 MessageKind::EventSocket { .. } => "event-socket",
100 MessageKind::General => "general",
101 MessageKind::FileChange => "file-change",
102 MessageKind::DateChange => "date-change",
103 }
104 }
105}
106
107impl fmt::Display for MessageKind {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 match self {
110 MessageKind::Execute { application, .. } => write!(f, "execute({})", application),
111 MessageKind::Dialplan { .. } => f.pad("dialplan"),
112 MessageKind::ChannelData => f.pad("channel-data"),
113 MessageKind::ChannelField { name, .. } => write!(f, "field({})", name),
114 MessageKind::Variable { name, .. } => write!(f, "var({})", name),
115 MessageKind::SdpMarker { direction } => write!(f, "sdp({})", direction),
116 MessageKind::StateChange { .. } => f.pad("state-change"),
117 MessageKind::CodecNegotiation => f.pad("codec-negotiation"),
118 MessageKind::Media { .. } => f.pad("media"),
119 MessageKind::ChannelLifecycle { .. } => f.pad("channel-lifecycle"),
120 MessageKind::EventSocket { .. } => f.pad("event-socket"),
121 MessageKind::General => f.pad("general"),
122 MessageKind::FileChange => f.pad("file-change"),
123 MessageKind::DateChange => f.pad("date-change"),
124 }
125 }
126}
127
128fn parse_execute(msg: &str) -> MessageKind {
129 let rest = &msg["EXECUTE ".len()..];
130
131 let depth = if rest.starts_with("[depth=") {
132 let end = rest.find(']').unwrap_or(0);
133 if end > 7 {
134 rest[7..end].parse::<u32>().unwrap_or(0)
135 } else {
136 0
137 }
138 } else {
139 return MessageKind::Execute {
140 depth: 0,
141 channel: String::new(),
142 application: String::new(),
143 arguments: rest.to_string(),
144 };
145 };
146
147 let after_bracket = rest.find("] ").map(|p| &rest[p + 2..]).unwrap_or("");
148
149 let (channel, app_part) = match after_bracket.find(' ') {
153 Some(p) => {
154 let first_token = &after_bracket[..p];
155 if first_token.contains('/') {
156 (first_token, &after_bracket[p + 1..])
157 } else {
158 ("", after_bracket)
159 }
160 }
161 None => ("", after_bracket),
162 };
163
164 let (application, arguments) = match app_part.find('(') {
165 Some(p) => {
166 let app = &app_part[..p];
167 let args = if app_part.ends_with(')') {
168 &app_part[p + 1..app_part.len() - 1]
169 } else {
170 &app_part[p + 1..]
171 };
172 (app, args)
173 }
174 None => (app_part, ""),
175 };
176
177 MessageKind::Execute {
178 depth,
179 channel: channel.to_string(),
180 application: application.to_string(),
181 arguments: arguments.to_string(),
182 }
183}
184
185fn parse_dialplan(msg: &str) -> MessageKind {
186 let prefix_len = if msg.starts_with("Chatplan: ") {
187 "Chatplan: ".len()
188 } else {
189 "Dialplan: ".len()
190 };
191 let rest = &msg[prefix_len..];
192 let (channel, detail) = match rest.find(' ') {
193 Some(p) => (&rest[..p], &rest[p + 1..]),
194 None => (rest, ""),
195 };
196 MessageKind::Dialplan {
197 channel: channel.to_string(),
198 detail: detail.to_string(),
199 }
200}
201
202fn parse_bracketed_value(s: &str, prefix_len: usize) -> Option<(&str, &str)> {
203 let after_prefix = &s[prefix_len..];
204 let colon = after_prefix.find(": ")?;
205 let name = &after_prefix[..colon];
206 let value_part = &after_prefix[colon + 2..];
207 if let Some(inner) = value_part.strip_prefix('[') {
208 if let Some(stripped) = inner.strip_suffix(']') {
209 Some((name, stripped))
210 } else {
211 Some((name, inner))
212 }
213 } else {
214 Some((name, value_part))
215 }
216}
217
218fn detect_sdp_direction(msg: &str) -> Option<SdpDirection> {
219 if msg.contains("Ring SDP") {
220 Some(SdpDirection::LocalRing)
221 } else if msg.contains("Local SDP") || msg.contains("local-sdp") {
222 Some(SdpDirection::Local)
223 } else if msg.contains("Remote SDP") || msg.contains("remote-sdp") {
224 Some(SdpDirection::Remote)
225 } else if msg.ends_with(" SDP:") || msg.ends_with(" SDP") {
226 Some(SdpDirection::Unknown)
227 } else {
228 None
229 }
230}
231
232pub fn classify_message(msg: &str) -> MessageKind {
237 if msg.starts_with("EXECUTE ") || msg.starts_with("Execute ") {
238 return parse_execute(msg);
239 }
240
241 if msg.starts_with("Dialplan: ") || msg.starts_with("Chatplan: ") {
242 return parse_dialplan(msg);
243 }
244
245 if msg.starts_with("Processing ")
246 && (msg.contains(" in context ") || msg.contains("recursive conditions"))
247 {
248 return parse_dialplan_processing(msg);
249 }
250
251 if msg.contains("CHANNEL_DATA") {
252 return MessageKind::ChannelData;
253 }
254
255 if msg.starts_with("variable_") {
256 if let Some((name, value)) = parse_bracketed_value(msg, 0) {
257 return MessageKind::Variable {
258 name: name.to_string(),
259 value: value.to_string(),
260 };
261 }
262 }
263
264 if let Some(direction) = detect_sdp_direction(msg) {
265 return MessageKind::SdpMarker { direction };
266 }
267
268 if msg.contains("State Change") || msg.contains("Callstate Change") {
269 return MessageKind::StateChange {
270 detail: msg.to_string(),
271 };
272 }
273
274 if msg.starts_with("SET ") || msg.starts_with("EXPORT ") {
275 if let Some(sv) = parse_set_or_export(msg) {
276 return sv;
277 }
278 }
279
280 if msg.starts_with("Audio Codec Compare ") {
281 return MessageKind::CodecNegotiation;
282 }
283
284 if msg.starts_with("CoreSession::setVariable(") {
285 return parse_core_session_set_variable(msg);
286 }
287
288 if msg.starts_with("UNSET ") {
289 return parse_unset(msg);
290 }
291
292 if let Some(rest) = msg.strip_prefix("set variable ") {
294 if let Some((name, value)) = rest.split_once('=') {
295 return MessageKind::Variable {
296 name: format!("variable_{name}"),
297 value: value.to_string(),
298 };
299 }
300 }
301
302 if msg.starts_with("Transfer ") {
303 return MessageKind::Dialplan {
304 channel: String::new(),
305 detail: msg.to_string(),
306 };
307 }
308
309 if msg.starts_with('(') {
311 if msg.contains(") State ") {
312 return MessageKind::StateChange {
313 detail: msg.to_string(),
314 };
315 }
316 return MessageKind::ChannelLifecycle {
317 detail: msg.to_string(),
318 };
319 }
320
321 if msg.starts_with("SOFIA ") {
323 return MessageKind::StateChange {
324 detail: msg.to_string(),
325 };
326 }
327
328 if msg.starts_with("checking condition") || msg.starts_with("action(") {
330 return MessageKind::ChannelLifecycle {
331 detail: msg.to_string(),
332 };
333 }
334
335 if msg.starts_with("Event Socket Command") {
336 return MessageKind::EventSocket {
337 detail: msg.to_string(),
338 };
339 }
340
341 if let Some(kind) = detect_media(msg) {
343 return kind;
344 }
345
346 if let Some(kind) = detect_channel_lifecycle(msg) {
348 return kind;
349 }
350
351 if let Some((_, rest)) = strip_channel_prefix(msg) {
353 return classify_channel_prefixed(rest);
354 }
355
356 if let Some((name, value)) = parse_bracketed_value(msg, 0) {
359 let name_bytes = name.as_bytes();
360 if !name_bytes.is_empty()
361 && !name.contains(' ')
362 && name_bytes[0].is_ascii_alphabetic()
363 && (name.contains('-') || name.starts_with("Channel-"))
364 {
365 return MessageKind::ChannelField {
366 name: name.to_string(),
367 value: value.to_string(),
368 };
369 }
370 }
371
372 MessageKind::General
373}
374
375fn strip_channel_prefix(msg: &str) -> Option<(&str, &str)> {
376 if !msg.starts_with("sofia/") && !msg.starts_with("loopback/") {
377 return None;
378 }
379 let bytes = msg.as_bytes();
380 let mut i = 0;
381 let mut bracket_depth: u32 = 0;
382 while i < bytes.len() {
383 match bytes[i] {
384 b'[' => bracket_depth += 1,
385 b']' => {
386 bracket_depth = bracket_depth.saturating_sub(1);
387 }
388 b' ' if bracket_depth == 0 => {
389 return Some((&msg[..i], &msg[i + 1..]));
390 }
391 _ => {}
392 }
393 i += 1;
394 }
395 None
396}
397
398fn classify_channel_prefixed(rest: &str) -> MessageKind {
399 if rest.starts_with("SOFIA ") || rest.starts_with("Standard ") || rest.starts_with("RTC ") {
401 return MessageKind::StateChange {
402 detail: rest.to_string(),
403 };
404 }
405
406 if let Some(kind) = detect_media(rest) {
407 return kind;
408 }
409
410 MessageKind::ChannelLifecycle {
412 detail: rest.to_string(),
413 }
414}
415
416fn detect_media(msg: &str) -> Option<MessageKind> {
417 let media_prefixes = [
418 "AUDIO RTP ",
419 "VIDEO RTP ",
420 "Activating ",
421 "RTCP ",
422 "Starting timer",
423 "Record session",
424 "Correct audio",
425 "No silence detection",
426 "Audio params",
427 "Codec ",
428 "Attaching BUG",
429 "Removing BUG",
430 "rtcp_stats_init",
431 "Send middle packet",
432 "Send end packet",
433 "Send first packet",
434 "START_RECORDING",
435 "Stop recording",
436 "Engaging Write Buffer",
437 "rtcp_stats:",
438 ];
439 for prefix in &media_prefixes {
440 if msg.starts_with(prefix) {
441 return Some(MessageKind::Media {
442 detail: msg.to_string(),
443 });
444 }
445 }
446
447 if msg.starts_with("Setting RTCP") || msg.starts_with("Setting BUG Codec") {
448 return Some(MessageKind::Media {
449 detail: msg.to_string(),
450 });
451 }
452
453 if msg.starts_with("Set ") {
454 return Some(MessageKind::Media {
455 detail: msg.to_string(),
456 });
457 }
458
459 if msg.starts_with("Original read codec set to")
460 || msg.starts_with("Forcing crypto_mode")
461 || msg.starts_with("Parsing global variables")
462 || msg.starts_with("Parsing session specific variables")
463 {
464 return Some(MessageKind::Media {
465 detail: msg.to_string(),
466 });
467 }
468
469 None
470}
471
472fn detect_channel_lifecycle(msg: &str) -> Option<MessageKind> {
473 let lifecycle_prefixes = [
474 "New Channel ",
475 "Close Channel ",
476 "Hangup ",
477 "Ring-Ready ",
478 "Ring Ready ",
479 "Pre-Answer ",
480 "Sending early media",
481 "Sending BYE",
482 "Sending CANCEL",
483 "Channel is hung up",
484 "Call appears",
485 "Found channel",
486 "3PCC ",
487 "Subscribed to 3PCC",
488 "New log started",
489 "Received a ",
490 "Session ",
491 "BRIDGE ",
492 "Originate ",
493 "USAGE:",
494 "Split into",
495 "Part ",
496 "Responding to INVITE",
497 "Redirecting to",
498 "subscribing to",
499 "Queue digit delay",
500 ];
501 for prefix in &lifecycle_prefixes {
502 if msg.starts_with(prefix) {
503 return Some(MessageKind::ChannelLifecycle {
504 detail: msg.to_string(),
505 });
506 }
507 }
508
509 if msg.starts_with("Channel ") {
510 return Some(MessageKind::ChannelLifecycle {
511 detail: msg.to_string(),
512 });
513 }
514
515 if msg.starts_with("Application ") && msg.contains("Requires media") {
516 return Some(MessageKind::ChannelLifecycle {
517 detail: msg.to_string(),
518 });
519 }
520
521 None
522}
523
524fn parse_core_session_set_variable(msg: &str) -> MessageKind {
525 let rest = &msg["CoreSession::setVariable(".len()..];
526 if let Some(end) = rest.strip_suffix(')') {
527 if let Some(comma) = end.find(", ") {
528 return MessageKind::Variable {
529 name: format!("variable_{}", &end[..comma]),
530 value: end[comma + 2..].to_string(),
531 };
532 }
533 }
534 MessageKind::Variable {
535 name: String::new(),
536 value: msg.to_string(),
537 }
538}
539
540fn parse_unset(msg: &str) -> MessageKind {
541 let rest = &msg["UNSET ".len()..];
542 let name = if let Some(inner) = rest.strip_prefix('[') {
543 inner.strip_suffix(']').unwrap_or(inner)
544 } else {
545 rest
546 };
547 MessageKind::Variable {
548 name: format!("variable_{name}"),
549 value: String::new(),
550 }
551}
552
553fn parse_dialplan_processing(msg: &str) -> MessageKind {
554 let rest = &msg["Processing ".len()..];
555 MessageKind::Dialplan {
556 channel: String::new(),
557 detail: rest.to_string(),
558 }
559}
560
561fn parse_set_or_export(msg: &str) -> Option<MessageKind> {
562 let sep = msg.find("]=[");
567 if let Some(sep_pos) = sep {
568 let name_start = msg[..sep_pos].rfind('[')?;
569 let name = &msg[name_start + 1..sep_pos];
570 let val_start = sep_pos + 3; let val_end = msg[val_start..]
572 .find(']')
573 .map(|p| val_start + p)
574 .unwrap_or(msg.len());
575 let value = &msg[val_start..val_end];
576 return Some(MessageKind::Variable {
577 name: format!("variable_{name}"),
578 value: value.to_string(),
579 });
580 }
581
582 None
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn execute_full() {
594 let msg = "EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 db(insert/ng_a1b2c3d4/city/ST GEORGES)";
595 let kind = classify_message(msg);
596 assert_eq!(
597 kind,
598 MessageKind::Execute {
599 depth: 0,
600 channel: "sofia/internal/+15550001234@192.0.2.1".to_string(),
601 application: "db".to_string(),
602 arguments: "insert/ng_a1b2c3d4/city/ST GEORGES".to_string(),
603 }
604 );
605 }
606
607 #[test]
608 fn execute_nested_depth() {
609 let msg = "EXECUTE [depth=2] sofia/internal/+15550001234@192.0.2.1 set(x=y)";
610 match classify_message(msg) {
611 MessageKind::Execute {
612 depth,
613 application,
614 arguments,
615 ..
616 } => {
617 assert_eq!(depth, 2);
618 assert_eq!(application, "set");
619 assert_eq!(arguments, "x=y");
620 }
621 other => panic!("expected Execute, got {other:?}"),
622 }
623 }
624
625 #[test]
626 fn execute_no_arguments() {
627 let msg = "EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 answer";
628 match classify_message(msg) {
629 MessageKind::Execute {
630 application,
631 arguments,
632 ..
633 } => {
634 assert_eq!(application, "answer");
635 assert_eq!(arguments, "");
636 }
637 other => panic!("expected Execute, got {other:?}"),
638 }
639 }
640
641 #[test]
642 fn execute_export_with_vars() {
643 let msg = "EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 export(originate_timeout=3600)";
644 match classify_message(msg) {
645 MessageKind::Execute {
646 application,
647 arguments,
648 ..
649 } => {
650 assert_eq!(application, "export");
651 assert_eq!(arguments, "originate_timeout=3600");
652 }
653 other => panic!("expected Execute, got {other:?}"),
654 }
655 }
656
657 #[test]
658 fn dialplan_parsing() {
659 let msg = "Dialplan: sofia/internal/+15550001234@192.0.2.1 parsing [public->global] continue=true";
660 match classify_message(msg) {
661 MessageKind::Dialplan { channel, detail } => {
662 assert_eq!(channel, "sofia/internal/+15550001234@192.0.2.1");
663 assert_eq!(detail, "parsing [public->global] continue=true");
664 }
665 other => panic!("expected Dialplan, got {other:?}"),
666 }
667 }
668
669 #[test]
670 fn dialplan_regex() {
671 let msg = "Dialplan: sofia/internal/+15550001234@192.0.2.1 Regex (PASS) [global_routing] destination_number(18001234567) =~ /^1?(\\d{10})$/ break=on-false";
672 match classify_message(msg) {
673 MessageKind::Dialplan { channel, detail } => {
674 assert_eq!(channel, "sofia/internal/+15550001234@192.0.2.1");
675 assert!(detail.starts_with("Regex (PASS)"));
676 }
677 other => panic!("expected Dialplan, got {other:?}"),
678 }
679 }
680
681 #[test]
682 fn dialplan_action() {
683 let msg =
684 "Dialplan: sofia/internal/+15550001234@192.0.2.1 Action set(call_direction=inbound)";
685 match classify_message(msg) {
686 MessageKind::Dialplan { detail, .. } => {
687 assert!(detail.starts_with("Action "));
688 }
689 other => panic!("expected Dialplan, got {other:?}"),
690 }
691 }
692
693 #[test]
694 fn channel_data_marker() {
695 assert_eq!(classify_message("CHANNEL_DATA:"), MessageKind::ChannelData);
696 }
697
698 #[test]
699 fn channel_data_in_message() {
700 assert_eq!(
701 classify_message("New CHANNEL_DATA arrived"),
702 MessageKind::ChannelData,
703 );
704 }
705
706 #[test]
707 fn channel_field_with_brackets() {
708 let msg = "Channel-State: [CS_EXECUTE]";
709 match classify_message(msg) {
710 MessageKind::ChannelField { name, value } => {
711 assert_eq!(name, "Channel-State");
712 assert_eq!(value, "CS_EXECUTE");
713 }
714 other => panic!("expected ChannelField, got {other:?}"),
715 }
716 }
717
718 #[test]
719 fn channel_field_name() {
720 let msg = "Channel-Name: [sofia/internal/+15550001234@192.0.2.1]";
721 match classify_message(msg) {
722 MessageKind::ChannelField { name, value } => {
723 assert_eq!(name, "Channel-Name");
724 assert_eq!(value, "sofia/internal/+15550001234@192.0.2.1");
725 }
726 other => panic!("expected ChannelField, got {other:?}"),
727 }
728 }
729
730 #[test]
731 fn variable_single_line() {
732 let msg = "variable_sip_call_id: [test123@192.0.2.1]";
733 match classify_message(msg) {
734 MessageKind::Variable { name, value } => {
735 assert_eq!(name, "variable_sip_call_id");
736 assert_eq!(value, "test123@192.0.2.1");
737 }
738 other => panic!("expected Variable, got {other:?}"),
739 }
740 }
741
742 #[test]
743 fn variable_multi_line_start() {
744 let msg = "variable_switch_r_sdp: [v=0";
745 match classify_message(msg) {
746 MessageKind::Variable { name, value } => {
747 assert_eq!(name, "variable_switch_r_sdp");
748 assert_eq!(value, "v=0");
749 }
750 other => panic!("expected Variable, got {other:?}"),
751 }
752 }
753
754 #[test]
755 fn sdp_local() {
756 assert_eq!(
757 classify_message("Local SDP:"),
758 MessageKind::SdpMarker {
759 direction: SdpDirection::Local
760 },
761 );
762 }
763
764 #[test]
765 fn sdp_remote() {
766 assert_eq!(
767 classify_message("Remote SDP:"),
768 MessageKind::SdpMarker {
769 direction: SdpDirection::Remote
770 },
771 );
772 }
773
774 #[test]
775 fn sdp_in_longer_message() {
776 match classify_message("Setting Local SDP for call") {
777 MessageKind::SdpMarker { direction } => {
778 assert_eq!(direction, SdpDirection::Local);
779 }
780 other => panic!("expected SdpMarker, got {other:?}"),
781 }
782 }
783
784 #[test]
785 fn sdp_unknown_direction() {
786 assert_eq!(
787 classify_message("Patched SDP:"),
788 MessageKind::SdpMarker {
789 direction: SdpDirection::Unknown
790 },
791 );
792 }
793
794 #[test]
795 fn ring_sdp_is_local_ring() {
796 assert_eq!(
797 classify_message("Ring SDP:"),
798 MessageKind::SdpMarker {
799 direction: SdpDirection::LocalRing
800 },
801 );
802 }
803
804 #[test]
805 fn state_change() {
806 let msg = "State Change CS_INIT -> CS_ROUTING";
807 match classify_message(msg) {
808 MessageKind::StateChange { detail } => {
809 assert_eq!(detail, msg);
810 }
811 other => panic!("expected StateChange, got {other:?}"),
812 }
813 }
814
815 #[test]
816 fn core_session_set_variable() {
817 match classify_message("CoreSession::setVariable(X-City, ST GEORGES)") {
818 MessageKind::Variable { name, value } => {
819 assert_eq!(name, "variable_X-City");
820 assert_eq!(value, "ST GEORGES");
821 }
822 other => panic!("expected Variable, got {other:?}"),
823 }
824 }
825
826 #[test]
827 fn general_empty() {
828 assert_eq!(classify_message(""), MessageKind::General);
829 }
830
831 #[test]
832 fn hangup_is_channel_lifecycle() {
833 match classify_message(
834 "Hangup sofia/internal/+15550001234@192.0.2.1 [CS_CONSUME_MEDIA] [NORMAL_CLEARING]",
835 ) {
836 MessageKind::ChannelLifecycle { .. } => {}
837 other => panic!("expected ChannelLifecycle, got {other:?}"),
838 }
839 }
840
841 #[test]
842 fn channel_field_no_brackets() {
843 let msg = "Channel-Presence-ID: 1234@192.0.2.1";
844 match classify_message(msg) {
845 MessageKind::ChannelField { name, value } => {
846 assert_eq!(name, "Channel-Presence-ID");
847 assert_eq!(value, "1234@192.0.2.1");
848 }
849 other => panic!("expected ChannelField, got {other:?}"),
850 }
851 }
852
853 #[test]
854 fn variable_no_brackets() {
855 let msg = "variable_direction: inbound";
856 match classify_message(msg) {
857 MessageKind::Variable { name, value } => {
858 assert_eq!(name, "variable_direction");
859 assert_eq!(value, "inbound");
860 }
861 other => panic!("expected Variable, got {other:?}"),
862 }
863 }
864
865 #[test]
868 fn execute_lowercase() {
869 let msg = "Execute [depth=2] set(RECORD_STEREO=true)";
870 match classify_message(msg) {
871 MessageKind::Execute {
872 depth,
873 application,
874 arguments,
875 ..
876 } => {
877 assert_eq!(depth, 2);
878 assert_eq!(application, "set");
879 assert_eq!(arguments, "RECORD_STEREO=true");
880 }
881 other => panic!("expected Execute, got {other:?}"),
882 }
883 }
884
885 #[test]
886 fn execute_lowercase_db() {
887 let msg = "Execute [depth=1] db(insert/ng_${originating_leg_uuid}/record_leg/${uuid})";
888 match classify_message(msg) {
889 MessageKind::Execute { application, .. } => {
890 assert_eq!(application, "db");
891 }
892 other => panic!("expected Execute, got {other:?}"),
893 }
894 }
895
896 #[test]
897 fn set_variable_message() {
898 let msg = "SET sofia/internal-v6/1263@[fd51:2050:2220:198::10] [ngcs_bridge_sip_req_uri]=[conf-factory-app.qc.core.ng.911bell.ca]";
899 match classify_message(msg) {
900 MessageKind::Variable { name, value } => {
901 assert_eq!(name, "variable_ngcs_bridge_sip_req_uri");
902 assert_eq!(value, "conf-factory-app.qc.core.ng.911bell.ca");
903 }
904 other => panic!("expected Variable, got {other:?}"),
905 }
906 }
907
908 #[test]
909 fn export_variable_message() {
910 let msg =
911 "EXPORT (export_vars) (REMOTE ONLY) [sip_from_uri]=[sip:cauca1.qc.psap.ng.911bell.ca]";
912 match classify_message(msg) {
913 MessageKind::Variable { name, value } => {
914 assert_eq!(name, "variable_sip_from_uri");
915 assert_eq!(value, "sip:cauca1.qc.psap.ng.911bell.ca");
916 }
917 other => panic!("expected Variable, got {other:?}"),
918 }
919 }
920
921 #[test]
922 fn export_simple_variable() {
923 let msg = "EXPORT (export_vars) [originate_timeout]=[3600]";
924 match classify_message(msg) {
925 MessageKind::Variable { name, value } => {
926 assert_eq!(name, "variable_originate_timeout");
927 assert_eq!(value, "3600");
928 }
929 other => panic!("expected Variable, got {other:?}"),
930 }
931 }
932
933 #[test]
934 fn processing_in_context() {
935 let msg = "Processing Extension 1263 <1263>->start_recording in context recordings";
936 match classify_message(msg) {
937 MessageKind::Dialplan { detail, .. } => {
938 assert!(detail.contains("start_recording"));
939 assert!(detail.contains("recordings"));
940 }
941 other => panic!("expected Dialplan, got {other:?}"),
942 }
943 }
944
945 #[test]
946 fn caller_field_as_channel_field() {
947 let msg = "Caller-Username: [+15550001234]";
948 match classify_message(msg) {
949 MessageKind::ChannelField { name, value } => {
950 assert_eq!(name, "Caller-Username");
951 assert_eq!(value, "+15550001234");
952 }
953 other => panic!("expected ChannelField, got {other:?}"),
954 }
955 }
956
957 #[test]
958 fn answer_state_as_channel_field() {
959 let msg = "Answer-State: [ringing]";
960 match classify_message(msg) {
961 MessageKind::ChannelField { name, value } => {
962 assert_eq!(name, "Answer-State");
963 assert_eq!(value, "ringing");
964 }
965 other => panic!("expected ChannelField, got {other:?}"),
966 }
967 }
968
969 #[test]
970 fn unique_id_as_channel_field() {
971 let msg = "Unique-ID: [a1b2c3d4-e5f6-7890-abcd-ef1234567890]";
972 match classify_message(msg) {
973 MessageKind::ChannelField { name, value } => {
974 assert_eq!(name, "Unique-ID");
975 assert_eq!(value, "a1b2c3d4-e5f6-7890-abcd-ef1234567890");
976 }
977 other => panic!("expected ChannelField, got {other:?}"),
978 }
979 }
980
981 #[test]
982 fn call_direction_as_channel_field() {
983 let msg = "Call-Direction: [inbound]";
984 match classify_message(msg) {
985 MessageKind::ChannelField { name, value } => {
986 assert_eq!(name, "Call-Direction");
987 assert_eq!(value, "inbound");
988 }
989 other => panic!("expected ChannelField, got {other:?}"),
990 }
991 }
992
993 #[test]
994 fn callstate_change() {
995 let msg = "(sofia/internal-v4/sos) Callstate Change RINGING -> ACTIVE";
996 match classify_message(msg) {
997 MessageKind::StateChange { detail } => {
998 assert!(detail.contains("RINGING -> ACTIVE"));
999 }
1000 other => panic!("expected StateChange, got {other:?}"),
1001 }
1002 }
1003
1004 #[test]
1005 fn action_is_pre_dialplan_lifecycle() {
1006 match classify_message("action(1:3pcc_force_dialplan:1:set_tflag) success") {
1007 MessageKind::ChannelLifecycle { .. } => {}
1008 other => panic!("expected ChannelLifecycle, got {other:?}"),
1009 }
1010 }
1011
1012 #[test]
1013 fn channel_answered_is_lifecycle() {
1014 match classify_message("Channel [sofia/internal] has been answered") {
1015 MessageKind::ChannelLifecycle { .. } => {}
1016 other => panic!("expected ChannelLifecycle, got {other:?}"),
1017 }
1018 }
1019
1020 #[test]
1021 fn chatplan_regex() {
1022 let msg = "Chatplan: sofia/internal/+15550001234@192.0.2.1 Regex (PASS) [global_routing] destination_number(18001234567) =~ /^1?(\\d{10})$/ break=on-false";
1023 match classify_message(msg) {
1024 MessageKind::Dialplan { channel, detail } => {
1025 assert_eq!(channel, "sofia/internal/+15550001234@192.0.2.1");
1026 assert!(detail.starts_with("Regex (PASS)"));
1027 }
1028 other => panic!("expected Dialplan, got {other:?}"),
1029 }
1030 }
1031
1032 #[test]
1033 fn chatplan_action() {
1034 let msg =
1035 "Chatplan: sofia/internal/+15550001234@192.0.2.1 Action set(call_direction=inbound)";
1036 match classify_message(msg) {
1037 MessageKind::Dialplan { detail, .. } => {
1038 assert!(detail.starts_with("Action "));
1039 }
1040 other => panic!("expected Dialplan, got {other:?}"),
1041 }
1042 }
1043
1044 #[test]
1045 fn chatplan_anti_action() {
1046 let msg =
1047 "Chatplan: sofia/internal/+15550001234@192.0.2.1 ANTI-Action log(WARNING no match)";
1048 match classify_message(msg) {
1049 MessageKind::Dialplan { detail, .. } => {
1050 assert!(detail.starts_with("ANTI-Action "));
1051 }
1052 other => panic!("expected Dialplan, got {other:?}"),
1053 }
1054 }
1055
1056 #[test]
1057 fn standard_execute_is_state_change() {
1058 let msg = "sofia/internal/+15550001234@192.0.2.1 Standard EXECUTE";
1059 match classify_message(msg) {
1060 MessageKind::StateChange { detail } => {
1061 assert_eq!(detail, "Standard EXECUTE");
1062 }
1063 other => panic!("expected StateChange, got {other:?}"),
1064 }
1065 }
1066
1067 #[test]
1068 fn sofia_execute_is_state_change() {
1069 let msg = "sofia/internal/+15550001234@192.0.2.1 SOFIA EXECUTE";
1070 match classify_message(msg) {
1071 MessageKind::StateChange { detail } => {
1072 assert_eq!(detail, "SOFIA EXECUTE");
1073 }
1074 other => panic!("expected StateChange, got {other:?}"),
1075 }
1076 }
1077
1078 #[test]
1079 fn rtc_execute_is_state_change() {
1080 let msg = "sofia/internal/+15550001234@192.0.2.1 RTC EXECUTE";
1081 match classify_message(msg) {
1082 MessageKind::StateChange { detail } => {
1083 assert_eq!(detail, "RTC EXECUTE");
1084 }
1085 other => panic!("expected StateChange, got {other:?}"),
1086 }
1087 }
1088
1089 #[test]
1090 fn standard_soft_execute_is_state_change() {
1091 let msg = "sofia/internal/+15550001234@192.0.2.1 Standard SOFT_EXECUTE";
1092 match classify_message(msg) {
1093 MessageKind::StateChange { detail } => {
1094 assert_eq!(detail, "Standard SOFT_EXECUTE");
1095 }
1096 other => panic!("expected StateChange, got {other:?}"),
1097 }
1098 }
1099
1100 #[test]
1101 fn dialplan_recursive_conditions() {
1102 let msg = "Processing recursive conditions level:1 [default] require-nested=true";
1103 match classify_message(msg) {
1104 MessageKind::Dialplan { detail, .. } => {
1105 assert!(detail.contains("recursive conditions"));
1106 }
1107 other => panic!("expected Dialplan, got {other:?}"),
1108 }
1109 }
1110
1111 #[test]
1112 fn sdp_duplicate_marker() {
1113 let msg = "Duplicate SDP";
1114 match classify_message(msg) {
1115 MessageKind::SdpMarker { direction } => {
1116 assert_eq!(direction, SdpDirection::Unknown);
1117 }
1118 other => panic!("expected SdpMarker, got {other:?}"),
1119 }
1120 }
1121
1122 #[test]
1123 fn sdp_verto_update_media() {
1124 match classify_message("updateMedia: Local SDP") {
1125 MessageKind::SdpMarker { direction } => {
1126 assert_eq!(direction, SdpDirection::Local);
1127 }
1128 other => panic!("expected SdpMarker, got {other:?}"),
1129 }
1130 }
1131}