Skip to main content

freeswitch_log_parser/session/
state.rs

1//! Per-UUID accumulated state and the point-in-time snapshot attached to each
2//! enriched entry.
3
4use std::collections::HashMap;
5use std::str::FromStr;
6
7use freeswitch_types::variables::VariableName;
8use freeswitch_types::{CallDirection, CallState, ChannelState, HangupCause};
9
10use crate::line::parse_line;
11use crate::message::{classify_message, MessageKind};
12use crate::stream::{Block, LogEntry, ParseWarning, SessionReading};
13
14use super::conference::ConferenceMembership;
15use super::media::SessionMedia;
16use super::parse::{
17    is_answered, parse_bridge_args, parse_dialplan_context, parse_hangup, parse_new_channel,
18    parse_processing_line, parse_state_change, StateChange,
19};
20
21/// Resolve a typed value into `slot`, or record that it could not be read.
22///
23/// The failed reading leaves whatever `slot` already held — a repeat of a field
24/// the vocabulary cannot read must not erase what an earlier one established.
25fn read<T: FromStr>(
26    slot: &mut Option<T>,
27    value: &str,
28    reading: SessionReading,
29    warnings: &mut Vec<ParseWarning>,
30) {
31    match T::from_str(value) {
32        Ok(parsed) => *slot = Some(parsed),
33        Err(_) => warnings.push(ParseWarning::UnreadableValue {
34            reading,
35            value: ParseWarning::excerpt(value),
36        }),
37    }
38}
39
40/// Mutable per-UUID state accumulator, updated as entries are processed.
41///
42/// Fields are `None` until the corresponding data is first seen in the stream.
43/// Variables accumulate from CHANNEL_DATA dumps, `set()`/`export()` executions,
44/// `SET`/`EXPORT` log lines, and inline `variable_*` lines.
45#[derive(Debug, Clone, Default)]
46#[non_exhaustive]
47pub struct SessionState {
48    /// `None` until a `Channel-Name` field is encountered.
49    pub channel_name: Option<String>,
50    /// `None` until a `State Change` line or a `Channel-State` field is seen.
51    pub channel_state: Option<ChannelState>,
52    /// `None` until a `Callstate Change` line is seen. A distinct vocabulary from
53    /// [`channel_state`](Self::channel_state); neither displaces the other.
54    pub call_state: Option<CallState>,
55    /// First dialplan context seen; set once and never overwritten.
56    pub initial_context: Option<String>,
57    /// Destination of the first `Processing` line = the dialed number at ingress;
58    /// set once and never overwritten (unlike last-wins `dialplan_to`).
59    pub initial_destination: Option<String>,
60    /// Current dialplan context; updated on each transfer/continue.
61    pub dialplan_context: Option<String>,
62    /// Caller side of the last `Processing` line — a number, or a display name
63    /// and number. `None` until one is seen.
64    pub dialplan_from: Option<String>,
65    /// Dialed destination of the last `Processing` line. `None` until one is seen.
66    pub dialplan_to: Option<String>,
67    /// Call direction from `Call-Direction` CHANNEL_DATA field; `None` until seen.
68    pub call_direction: Option<CallDirection>,
69    /// Caller ID number from `Caller-Caller-ID-Number` CHANNEL_DATA field; `None` until seen.
70    pub caller_id_number: Option<String>,
71    /// Caller ID name from `Caller-Caller-ID-Name` CHANNEL_DATA field; `None` until seen.
72    pub caller_id_name: Option<String>,
73    /// Destination number from `Caller-Destination-Number` CHANNEL_DATA field; `None` until seen.
74    pub destination_number: Option<String>,
75    /// Hangup cause extracted from ChannelLifecycle Hangup detail; `None` until hangup seen.
76    pub hangup_cause: Option<HangupCause>,
77    /// Timestamp when "has been answered" lifecycle event was seen; `None` until answered.
78    pub answered_at: Option<String>,
79    /// Other leg's UUID; `None` until bridged. Set from `Originate Resulted in Success` on A-leg,
80    /// and from `New Channel` on B-leg (back-pointing to A-leg via originate context).
81    pub other_leg_uuid: Option<String>,
82    /// Conference this session is currently a member of; `None` once it leaves.
83    pub conference: Option<ConferenceMembership>,
84    /// Codecs negotiated on this leg, by media type and direction.
85    pub media: SessionMedia,
86    /// Pending bridge target channel from `EXECUTE bridge()`, consumed when B-leg `New Channel` matches.
87    pub(crate) pending_bridge_target: Option<String>,
88    /// All variables learned so far, with the `variable_` prefix stripped from names.
89    pub variables: HashMap<String, String>,
90}
91/// Immutable point-in-time copy of a session's state, attached to each [`EnrichedEntry`].
92///
93/// Does not include `variables` to keep snapshots lightweight — access the full
94/// variable map via [`SessionTracker::sessions()`].
95#[derive(Debug, Clone)]
96#[non_exhaustive]
97pub struct SessionSnapshot {
98    pub channel_name: Option<String>,
99    pub channel_state: Option<ChannelState>,
100    pub call_state: Option<CallState>,
101    pub initial_context: Option<String>,
102    pub initial_destination: Option<String>,
103    pub dialplan_context: Option<String>,
104    pub dialplan_from: Option<String>,
105    pub dialplan_to: Option<String>,
106    pub call_direction: Option<CallDirection>,
107    pub caller_id_number: Option<String>,
108    pub caller_id_name: Option<String>,
109    pub destination_number: Option<String>,
110    pub hangup_cause: Option<HangupCause>,
111    pub answered_at: Option<String>,
112    pub other_leg_uuid: Option<String>,
113    pub conference: Option<ConferenceMembership>,
114    pub media: SessionMedia,
115}
116
117impl SessionState {
118    /// Value of a typed channel variable, or `None` if this session never saw it.
119    ///
120    /// Accepts any of `freeswitch-types`' variable-name enums, and spares the
121    /// caller from knowing that [`variables`](Self::variables) keys are stored
122    /// with the `variable_` prefix stripped.
123    pub fn variable<V: VariableName>(&self, var: V) -> Option<&str> {
124        self.variables.get(var.as_str()).map(String::as_str)
125    }
126
127    /// Destructured rather than field-by-field: the snapshot mirrors this struct
128    /// by hand, and without an exhaustive binding a field added to one and
129    /// forgotten in the other compiles silently. The two `_` bindings are the
130    /// deliberate omissions.
131    pub(super) fn snapshot(&self) -> SessionSnapshot {
132        let SessionState {
133            channel_name,
134            channel_state,
135            call_state,
136            initial_context,
137            initial_destination,
138            dialplan_context,
139            dialplan_from,
140            dialplan_to,
141            call_direction,
142            caller_id_number,
143            caller_id_name,
144            destination_number,
145            hangup_cause,
146            answered_at,
147            other_leg_uuid,
148            conference,
149            media,
150            pending_bridge_target: _,
151            variables: _,
152        } = self;
153
154        SessionSnapshot {
155            channel_name: channel_name.clone(),
156            channel_state: *channel_state,
157            call_state: *call_state,
158            initial_context: initial_context.clone(),
159            initial_destination: initial_destination.clone(),
160            dialplan_context: dialplan_context.clone(),
161            dialplan_from: dialplan_from.clone(),
162            dialplan_to: dialplan_to.clone(),
163            call_direction: *call_direction,
164            caller_id_number: caller_id_number.clone(),
165            caller_id_name: caller_id_name.clone(),
166            destination_number: destination_number.clone(),
167            hangup_cause: *hangup_cause,
168            answered_at: answered_at.clone(),
169            other_leg_uuid: other_leg_uuid.clone(),
170            conference: conference.clone(),
171            media: media.clone(),
172        }
173    }
174
175    /// Absorb one CHANNEL_DATA field. The collision splitter can hand a dump
176    /// field over as its own entry rather than inside a block, so both arrival
177    /// shapes decode here — a lighter reading on one of them would drop whatever
178    /// it left out, `Other-Leg-Unique-ID` included, only for split dumps.
179    fn apply_channel_field(&mut self, name: &str, value: &str, warnings: &mut Vec<ParseWarning>) {
180        match name {
181            "Channel-Name" => self.channel_name = Some(value.to_string()),
182            "Channel-State" => read(
183                &mut self.channel_state,
184                value,
185                SessionReading::ChannelState,
186                warnings,
187            ),
188            "Call-Direction" => read(
189                &mut self.call_direction,
190                value,
191                SessionReading::CallDirection,
192                warnings,
193            ),
194            "Caller-Caller-ID-Number" => self.caller_id_number = Some(value.to_string()),
195            "Caller-Caller-ID-Name" => self.caller_id_name = Some(value.to_string()),
196            "Caller-Destination-Number" => self.destination_number = Some(value.to_string()),
197            "Other-Leg-Unique-ID" => self.other_leg_uuid = Some(value.to_string()),
198            _ => {}
199        }
200    }
201
202    /// Whether this session has reached a state it cannot leave. Stragglers in a
203    /// terminal state are never candidates for leg linking.
204    ///
205    /// `DOWN` is not terminal: it doubles as the initial call state before any
206    /// change is observed.
207    pub(super) fn is_terminal(&self) -> bool {
208        matches!(
209            self.channel_state,
210            Some(
211                ChannelState::CsHangup
212                    | ChannelState::CsReporting
213                    | ChannelState::CsDestroy
214                    | ChannelState::CsNone
215            )
216        ) || matches!(self.call_state, Some(CallState::Hangup))
217    }
218
219    /// Absorb an entry, returning whatever readings its values defeated.
220    pub(super) fn update_from_entry(&mut self, entry: &LogEntry) -> Vec<ParseWarning> {
221        let mut warnings = Vec::new();
222        let block_has_channel_data = matches!(entry.block, Some(Block::ChannelData { .. }));
223        if let Some(Block::ChannelData { fields, variables }) = &entry.block {
224            for (name, value) in fields {
225                self.apply_channel_field(name, value, &mut warnings);
226            }
227            for (name, value) in variables {
228                let var_name = name.strip_prefix("variable_").unwrap_or(name);
229                self.variables.insert(var_name.to_string(), value.clone());
230            }
231        }
232
233        match &entry.message_kind {
234            MessageKind::Execute {
235                application,
236                arguments,
237                ..
238            } => match application.as_str() {
239                "set" | "export" => {
240                    if let Some((name, value)) = arguments.split_once('=') {
241                        self.variables.insert(name.to_string(), value.to_string());
242                    }
243                }
244                "bridge" => {
245                    if let Some(info) = parse_bridge_args(arguments) {
246                        if let Some(uuid) = &info.origination_uuid {
247                            self.other_leg_uuid = Some(uuid.clone());
248                        }
249                        self.pending_bridge_target = Some(info.target_channel);
250                    }
251                }
252                _ => {}
253            },
254            MessageKind::ChannelLifecycle { detail } => {
255                if let Some(name) = parse_new_channel(detail) {
256                    if self.channel_name.is_none() {
257                        self.channel_name = Some(name);
258                    }
259                }
260                if let Some(cause) = parse_hangup(detail) {
261                    read(
262                        &mut self.hangup_cause,
263                        &cause,
264                        SessionReading::HangupCause,
265                        &mut warnings,
266                    );
267                }
268                if is_answered(detail) && self.answered_at.is_none() {
269                    self.answered_at = Some(entry.timestamp.clone());
270                }
271            }
272            kind => self.apply_kind(kind, &mut warnings),
273        }
274
275        self.apply_processing(&entry.message);
276        self.media.update_from_entry(entry);
277
278        for attached in &entry.attached {
279            let parsed = parse_line(attached);
280            self.update_from_message(parsed.message, block_has_channel_data, &mut warnings);
281        }
282        warnings
283    }
284
285    /// Canonical extraction for message kinds that appear on both primary and
286    /// attached lines. Entry-only kinds (Execute, ChannelLifecycle — the
287    /// latter needs the entry timestamp) stay in `update_from_entry`.
288    fn apply_kind(&mut self, kind: &MessageKind, warnings: &mut Vec<ParseWarning>) {
289        match kind {
290            MessageKind::Dialplan { detail, .. } => {
291                if let Some(context) = parse_dialplan_context(detail) {
292                    self.initial_context.get_or_insert(context.to_string());
293                    self.dialplan_context = Some(context.to_string());
294                }
295            }
296            MessageKind::Variable { name, value } => {
297                let var_name = name.strip_prefix("variable_").unwrap_or(name);
298                self.variables.insert(var_name.to_string(), value.clone());
299            }
300            MessageKind::ChannelField { name, value } => {
301                self.apply_channel_field(name, value, warnings)
302            }
303            MessageKind::StateChange { detail } => match parse_state_change(detail) {
304                Some(StateChange::Channel(to)) => read(
305                    &mut self.channel_state,
306                    to,
307                    SessionReading::ChannelState,
308                    warnings,
309                ),
310                Some(StateChange::Call(to)) => read(
311                    &mut self.call_state,
312                    to,
313                    SessionReading::CallState,
314                    warnings,
315                ),
316                None => {}
317            },
318            _ => {}
319        }
320    }
321
322    /// `Processing <caller>-><dest> in context <ctx>` — emitted by the dialplan
323    /// hunt on both primary and attached lines; anchored on raw text because
324    /// `classify_message` folds it into `Dialplan` with the prefix stripped.
325    fn apply_processing(&mut self, msg: &str) {
326        if msg.contains("Processing ") && msg.contains(" in context ") {
327            if let Some(dp) = parse_processing_line(msg) {
328                self.initial_context.get_or_insert(dp.context.clone());
329                self.initial_destination.get_or_insert(dp.to.clone());
330                self.dialplan_context = Some(dp.context);
331                self.dialplan_from = Some(dp.from);
332                self.dialplan_to = Some(dp.to);
333            }
334        }
335    }
336
337    fn update_from_message(
338        &mut self,
339        msg: &str,
340        block_provides_channel_data: bool,
341        warnings: &mut Vec<ParseWarning>,
342    ) {
343        let kind = classify_message(msg);
344        match &kind {
345            // A ChannelData block already carries these — re-applying the raw
346            // attached lines would clobber reassembled multi-line values with
347            // their opening fragment.
348            MessageKind::Variable { .. } | MessageKind::ChannelField { .. }
349                if block_provides_channel_data => {}
350            kind => self.apply_kind(kind, warnings),
351        }
352        self.apply_processing(msg);
353    }
354}