use std::collections::HashMap;
use std::str::FromStr;
use freeswitch_types::variables::VariableName;
use freeswitch_types::{CallDirection, CallState, ChannelState, HangupCause};
use crate::line::parse_line;
use crate::message::{classify_message, MessageKind};
use crate::stream::{Block, LogEntry, ParseWarning, SessionReading};
use super::conference::ConferenceMembership;
use super::media::SessionMedia;
use super::parse::{
is_answered, parse_bridge_args, parse_dialplan_context, parse_hangup, parse_new_channel,
parse_processing_line, parse_state_change, StateChange,
};
fn read<T: FromStr>(
slot: &mut Option<T>,
value: &str,
reading: SessionReading,
warnings: &mut Vec<ParseWarning>,
) {
match T::from_str(value) {
Ok(parsed) => *slot = Some(parsed),
Err(_) => warnings.push(ParseWarning::UnreadableValue {
reading,
value: ParseWarning::excerpt(value),
}),
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct SessionState {
pub channel_name: Option<String>,
pub channel_state: Option<ChannelState>,
pub call_state: Option<CallState>,
pub initial_context: Option<String>,
pub initial_destination: Option<String>,
pub dialplan_context: Option<String>,
pub dialplan_from: Option<String>,
pub dialplan_to: Option<String>,
pub call_direction: Option<CallDirection>,
pub caller_id_number: Option<String>,
pub caller_id_name: Option<String>,
pub destination_number: Option<String>,
pub hangup_cause: Option<HangupCause>,
pub answered_at: Option<String>,
pub other_leg_uuid: Option<String>,
pub conference: Option<ConferenceMembership>,
pub media: SessionMedia,
pub(crate) pending_bridge_target: Option<String>,
pub variables: HashMap<String, String>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SessionSnapshot {
pub channel_name: Option<String>,
pub channel_state: Option<ChannelState>,
pub call_state: Option<CallState>,
pub initial_context: Option<String>,
pub initial_destination: Option<String>,
pub dialplan_context: Option<String>,
pub dialplan_from: Option<String>,
pub dialplan_to: Option<String>,
pub call_direction: Option<CallDirection>,
pub caller_id_number: Option<String>,
pub caller_id_name: Option<String>,
pub destination_number: Option<String>,
pub hangup_cause: Option<HangupCause>,
pub answered_at: Option<String>,
pub other_leg_uuid: Option<String>,
pub conference: Option<ConferenceMembership>,
pub media: SessionMedia,
}
impl SessionState {
pub fn variable<V: VariableName>(&self, var: V) -> Option<&str> {
self.variables.get(var.as_str()).map(String::as_str)
}
pub(super) fn snapshot(&self) -> SessionSnapshot {
let SessionState {
channel_name,
channel_state,
call_state,
initial_context,
initial_destination,
dialplan_context,
dialplan_from,
dialplan_to,
call_direction,
caller_id_number,
caller_id_name,
destination_number,
hangup_cause,
answered_at,
other_leg_uuid,
conference,
media,
pending_bridge_target: _,
variables: _,
} = self;
SessionSnapshot {
channel_name: channel_name.clone(),
channel_state: *channel_state,
call_state: *call_state,
initial_context: initial_context.clone(),
initial_destination: initial_destination.clone(),
dialplan_context: dialplan_context.clone(),
dialplan_from: dialplan_from.clone(),
dialplan_to: dialplan_to.clone(),
call_direction: *call_direction,
caller_id_number: caller_id_number.clone(),
caller_id_name: caller_id_name.clone(),
destination_number: destination_number.clone(),
hangup_cause: *hangup_cause,
answered_at: answered_at.clone(),
other_leg_uuid: other_leg_uuid.clone(),
conference: conference.clone(),
media: media.clone(),
}
}
fn apply_channel_field(&mut self, name: &str, value: &str, warnings: &mut Vec<ParseWarning>) {
match name {
"Channel-Name" => self.channel_name = Some(value.to_string()),
"Channel-State" => read(
&mut self.channel_state,
value,
SessionReading::ChannelState,
warnings,
),
"Call-Direction" => read(
&mut self.call_direction,
value,
SessionReading::CallDirection,
warnings,
),
"Caller-Caller-ID-Number" => self.caller_id_number = Some(value.to_string()),
"Caller-Caller-ID-Name" => self.caller_id_name = Some(value.to_string()),
"Caller-Destination-Number" => self.destination_number = Some(value.to_string()),
"Other-Leg-Unique-ID" => self.other_leg_uuid = Some(value.to_string()),
_ => {}
}
}
pub(super) fn is_terminal(&self) -> bool {
matches!(
self.channel_state,
Some(
ChannelState::CsHangup
| ChannelState::CsReporting
| ChannelState::CsDestroy
| ChannelState::CsNone
)
) || matches!(self.call_state, Some(CallState::Hangup))
}
pub(super) fn update_from_entry(&mut self, entry: &LogEntry) -> Vec<ParseWarning> {
let mut warnings = Vec::new();
let block_has_channel_data = matches!(entry.block, Some(Block::ChannelData { .. }));
if let Some(Block::ChannelData { fields, variables }) = &entry.block {
for (name, value) in fields {
self.apply_channel_field(name, value, &mut warnings);
}
for (name, value) in variables {
let var_name = name.strip_prefix("variable_").unwrap_or(name);
self.variables.insert(var_name.to_string(), value.clone());
}
}
match &entry.message_kind {
MessageKind::Execute {
application,
arguments,
..
} => match application.as_str() {
"set" | "export" => {
if let Some((name, value)) = arguments.split_once('=') {
self.variables.insert(name.to_string(), value.to_string());
}
}
"bridge" => {
if let Some(info) = parse_bridge_args(arguments) {
if let Some(uuid) = &info.origination_uuid {
self.other_leg_uuid = Some(uuid.clone());
}
self.pending_bridge_target = Some(info.target_channel);
}
}
_ => {}
},
MessageKind::ChannelLifecycle { detail } => {
if let Some(name) = parse_new_channel(detail) {
if self.channel_name.is_none() {
self.channel_name = Some(name);
}
}
if let Some(cause) = parse_hangup(detail) {
read(
&mut self.hangup_cause,
&cause,
SessionReading::HangupCause,
&mut warnings,
);
}
if is_answered(detail) && self.answered_at.is_none() {
self.answered_at = Some(entry.timestamp.clone());
}
}
kind => self.apply_kind(kind, &mut warnings),
}
self.apply_processing(&entry.message);
self.media.update_from_entry(entry);
for attached in &entry.attached {
let parsed = parse_line(attached);
self.update_from_message(parsed.message, block_has_channel_data, &mut warnings);
}
warnings
}
fn apply_kind(&mut self, kind: &MessageKind, warnings: &mut Vec<ParseWarning>) {
match kind {
MessageKind::Dialplan { detail, .. } => {
if let Some(context) = parse_dialplan_context(detail) {
self.initial_context.get_or_insert(context.to_string());
self.dialplan_context = Some(context.to_string());
}
}
MessageKind::Variable { name, value } => {
let var_name = name.strip_prefix("variable_").unwrap_or(name);
self.variables.insert(var_name.to_string(), value.clone());
}
MessageKind::ChannelField { name, value } => {
self.apply_channel_field(name, value, warnings)
}
MessageKind::StateChange { detail } => match parse_state_change(detail) {
Some(StateChange::Channel(to)) => read(
&mut self.channel_state,
to,
SessionReading::ChannelState,
warnings,
),
Some(StateChange::Call(to)) => read(
&mut self.call_state,
to,
SessionReading::CallState,
warnings,
),
None => {}
},
_ => {}
}
}
fn apply_processing(&mut self, msg: &str) {
if msg.contains("Processing ") && msg.contains(" in context ") {
if let Some(dp) = parse_processing_line(msg) {
self.initial_context.get_or_insert(dp.context.clone());
self.initial_destination.get_or_insert(dp.to.clone());
self.dialplan_context = Some(dp.context);
self.dialplan_from = Some(dp.from);
self.dialplan_to = Some(dp.to);
}
}
}
fn update_from_message(
&mut self,
msg: &str,
block_provides_channel_data: bool,
warnings: &mut Vec<ParseWarning>,
) {
let kind = classify_message(msg);
match &kind {
MessageKind::Variable { .. } | MessageKind::ChannelField { .. }
if block_provides_channel_data => {}
kind => self.apply_kind(kind, warnings),
}
self.apply_processing(msg);
}
}