Skip to main content

freeswitch_log_parser/session/
parse.rs

1//! Message shapes the tracker reads out of dialplan, originate and bridge lines.
2
3use std::str::FromStr;
4
5use freeswitch_types::{BridgeDialString, DialString};
6
7use crate::fields::processing_parts;
8use crate::message::new_channel_name;
9
10pub(super) struct DialplanContext {
11    pub(super) from: String,
12    pub(super) to: String,
13    pub(super) context: String,
14}
15
16/// The context of a `parsing [<context>-><extension>]` line.
17///
18/// The extension is deliberately dropped: it names a dialplan entry point, not
19/// the dialed number `dialplan_to` carries, and folding the two into one field
20/// left it meaning whichever shape was logged last.
21pub(super) fn parse_dialplan_context(detail: &str) -> Option<&str> {
22    let rest = detail.strip_prefix("parsing [")?;
23    let bracket_end = rest.find(']')?;
24    let inner = &rest[..bracket_end];
25    let arrow = inner.find("->")?;
26    Some(&inner[..arrow])
27}
28
29pub(super) fn parse_processing_line(msg: &str) -> Option<DialplanContext> {
30    let parts = processing_parts(msg)?;
31    Some(DialplanContext {
32        from: msg[parts.head].to_string(),
33        to: msg[parts.dest].to_string(),
34        context: msg[parts.context].to_string(),
35    })
36}
37
38pub(super) fn parse_new_channel(detail: &str) -> Option<String> {
39    new_channel_name(detail).map(str::to_string)
40}
41
42/// Which of the two state vocabularies a `... Change <old> -> <new>` line speaks.
43pub(super) enum StateChange<'a> {
44    Channel(&'a str),
45    Call(&'a str),
46}
47
48/// The new state a change line moves to. `Callstate Change` and `State Change`
49/// are distinct markers, so the two vocabularies never have to share a slot.
50pub(super) fn parse_state_change(detail: &str) -> Option<StateChange<'_>> {
51    let arrow = detail.find(" -> ")?;
52    let to = detail[arrow + 4..].trim();
53    if detail.contains("Callstate Change") {
54        Some(StateChange::Call(to))
55    } else {
56        Some(StateChange::Channel(to))
57    }
58}
59
60pub(super) fn parse_hangup(detail: &str) -> Option<String> {
61    if !detail.contains("Hangup ") {
62        return None;
63    }
64    let start = detail.rfind('[')?;
65    let end = detail[start..].find(']')?;
66    Some(detail[start + 1..start + end].to_string())
67}
68
69pub(super) fn is_answered(detail: &str) -> bool {
70    detail.contains("has been answered")
71}
72
73/// Extract `origination_uuid` and the bridge target channel from bridge() arguments.
74/// Uses `BridgeDialString` from freeswitch-types for correct parsing of `[]`, `{}`,
75/// `|` failover, and `,` simultaneous ring syntax.
76///
77/// Returns `None` when the arguments do not parse as a dial string or name no
78/// endpoint at all.
79pub fn parse_bridge_args(arguments: &str) -> Option<BridgeInfo> {
80    let dial = BridgeDialString::from_str(arguments).ok()?;
81    let first_ep = dial.groups().first()?.first()?;
82    // `{}` variables apply to every endpoint, so a single-leg bridge can name
83    // the new leg's UUID there instead of in the endpoint's own `[]`.
84    let origination_uuid = first_ep
85        .variables()
86        .and_then(|v| v.get("origination_uuid"))
87        .or_else(|| dial.variables().and_then(|v| v.get("origination_uuid")))
88        .map(|s| s.to_string());
89    let mut bare = first_ep.clone();
90    bare.set_variables(None);
91    let target_channel = bare.to_string();
92    Some(BridgeInfo {
93        origination_uuid,
94        target_channel,
95    })
96}
97
98/// What a `bridge()` argument list says about the leg it is about to create.
99#[derive(Debug, Clone, PartialEq, Eq)]
100#[non_exhaustive]
101pub struct BridgeInfo {
102    /// The UUID the new leg is being forced to take, when the dial string sets one.
103    pub origination_uuid: Option<String>,
104    /// The first endpoint with its `{}`/`[]` variables removed, matching the form
105    /// the new channel will report as its channel name.
106    pub target_channel: String,
107}
108
109/// Parse "Originate Resulted in Success: [channel] Peer UUID: uuid"
110pub(super) fn parse_originate_success(msg: &str) -> Option<String> {
111    let marker = "Peer UUID: ";
112    let idx = msg.find(marker)?;
113    let uuid = msg[idx + marker.len()..].trim();
114    if uuid.is_empty() {
115        None
116    } else {
117        Some(uuid.to_string())
118    }
119}
120
121/// Parse the bracketed channel name from "Originate Resulted in Success: [<chan>] …".
122/// Used as a fallback when the `Peer UUID:` suffix is absent (FS 1.10.5-dev and
123/// similar builds). Returns the channel name borrowed from `msg`.
124pub(super) fn parse_originate_channel(msg: &str) -> Option<&str> {
125    let start = msg.find(" [")? + 2;
126    let end = msg[start..].find(']')?;
127    let chan = &msg[start..start + end];
128    if chan.is_empty() {
129        None
130    } else {
131        Some(chan)
132    }
133}