use crate::types::CallState;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct SessionId(pub String);
impl SessionId {
pub fn new() -> Self {
Self(format!("session-{}", uuid::Uuid::new_v4()))
}
pub fn from_string(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for SessionId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for SessionId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl std::fmt::Display for SessionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum MediaFlowDirection {
Send,
Receive,
Both,
None,
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum MediaDirection {
SendRecv,
SendOnly,
RecvOnly,
Inactive,
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct DialogId(pub uuid::Uuid);
impl DialogId {
pub fn new() -> Self {
Self(uuid::Uuid::new_v4())
}
pub fn from_uuid(uuid: uuid::Uuid) -> Self {
Self(uuid)
}
pub fn as_uuid(&self) -> &uuid::Uuid {
&self.0
}
}
impl std::fmt::Display for DialogId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<rvoip_sip_dialog::DialogId> for DialogId {
fn from(dialog_id: rvoip_sip_dialog::DialogId) -> Self {
Self(dialog_id.0)
}
}
impl From<DialogId> for rvoip_sip_dialog::DialogId {
fn from(dialog_id: DialogId) -> Self {
rvoip_sip_dialog::DialogId(dialog_id.0)
}
}
impl From<&DialogId> for rvoip_sip_dialog::DialogId {
fn from(dialog_id: &DialogId) -> Self {
rvoip_sip_dialog::DialogId(dialog_id.0)
}
}
pub type MediaSessionId = rvoip_media_core::DialogId;
pub type CallId = String;
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct StateKey {
pub role: Role,
pub state: CallState,
pub event: EventType,
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum Role {
UAC, UAS, Both, }
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum EventType {
MakeCall {
target: String,
},
IncomingCall {
from: String,
sdp: Option<String>,
},
IncomingCallAutoAccept {
from: String,
sdp: Option<String>,
},
AcceptCall,
RejectCall {
status: u16,
reason: String,
},
RedirectCall {
status: u16,
contacts: Vec<String>,
},
SendEarlyMedia {
sdp: Option<String>,
},
AuthRequired {
status_code: u16,
challenge: String,
method: String,
},
SessionIntervalTooSmall {
min_se_secs: u32,
},
HangupCall,
CancelCall,
HoldCall,
ResumeCall,
MuteCall,
UnmuteCall,
PlayAudio {
file: String,
},
StartRecording,
StopRecording,
DialogCreated {
dialog_id: String,
call_id: String,
},
CallEstablished {
session_id: String,
sdp_answer: Option<String>,
},
DialogInvite,
Dialog180Ringing,
Dialog183SessionProgress,
Dialog200OK,
DialogACK,
DialogBYE,
DialogCANCEL,
DialogREFER,
DialogReINVITE,
Dialog3xxRedirect {
status: u16,
targets: Vec<String>,
},
ReinviteGlare,
Dialog4xxFailure(u16),
Dialog5xxFailure(u16),
Dialog6xxFailure(u16),
Dialog487RequestTerminated,
DialogTimeout,
DialogTerminated,
DialogError(String),
DialogStateChanged {
old_state: String,
new_state: String,
},
ReinviteReceived {
sdp: Option<String>,
},
UpdateReceived {
sdp: Option<String>,
},
TransferRequested {
refer_to: String,
transfer_type: String,
transaction_id: String,
},
MediaSessionCreated,
MediaSessionReady,
MediaNegotiated,
MediaFlowEstablished,
MediaError(String),
MediaEvent(String), MediaQualityDegraded {
packet_loss_percent: u32,
jitter_ms: u32,
severity: String,
},
DtmfDetected {
digit: char,
duration_ms: u32,
},
RtpTimeout {
last_packet_time: String,
},
PacketLossThresholdExceeded {
loss_percentage: u32,
},
InternalCheckReady,
InternalACKSent,
InternalUASMedia,
InternalCleanupComplete,
CheckConditions,
PublishCallEstablished,
CreateConference {
name: String,
},
AddParticipant {
session_id: String,
},
JoinConference {
conference_id: String,
},
LeaveConference,
MuteInConference,
UnmuteInConference,
BridgeSessions {
other_session: SessionId,
},
UnbridgeSessions,
ModifySession,
StartRegistration,
Registration200OK,
Registration401,
RegistrationFailed(u16),
RetryRegistration,
RefreshRegistration,
StartUnregistration,
Unregistration200OK,
UnregistrationFailed,
UnregisterRequest,
RegistrationExpired,
StartSubscription,
ReceiveNOTIFY,
SendNOTIFY,
SubscriptionAccepted,
SubscriptionFailed(u16),
SubscriptionExpired,
UnsubscribeRequest,
SendMessage,
ReceiveMESSAGE,
MessageDelivered,
MessageFailed(u16),
CleanupComplete,
Reset,
InternalProceedWithTransfer,
InternalMakeTransferCall,
InternalTransferCallEstablished,
SendOutboundInvite,
SendOutboundReInvite,
SendOutboundBye,
SendOutboundCancel,
SendOutboundRefer,
SendOutboundNotify,
SendOutboundInfo,
SendOutboundUpdate,
SendOutboundMessage,
SendOutboundOptions,
SendOutboundSubscribe,
SendOutboundRegister,
}
impl EventType {
pub fn normalize(&self) -> Self {
match self {
EventType::MakeCall { .. } => EventType::MakeCall {
target: String::new(),
},
EventType::IncomingCall { .. } => EventType::IncomingCall {
from: String::new(),
sdp: None,
},
EventType::IncomingCallAutoAccept { .. } => EventType::IncomingCallAutoAccept {
from: String::new(),
sdp: None,
},
EventType::RejectCall { .. } => EventType::RejectCall {
status: 0,
reason: String::new(),
},
EventType::RedirectCall { .. } => EventType::RedirectCall {
status: 0,
contacts: Vec::new(),
},
EventType::SendEarlyMedia { .. } => EventType::SendEarlyMedia { sdp: None },
EventType::AuthRequired { .. } => EventType::AuthRequired {
status_code: 0,
challenge: String::new(),
method: String::new(),
},
EventType::SessionIntervalTooSmall { .. } => {
EventType::SessionIntervalTooSmall { min_se_secs: 0 }
}
EventType::PlayAudio { .. } => EventType::PlayAudio {
file: String::new(),
},
EventType::CreateConference { .. } => EventType::CreateConference {
name: String::new(),
},
EventType::AddParticipant { .. } => EventType::AddParticipant {
session_id: String::new(),
},
EventType::JoinConference { .. } => EventType::JoinConference {
conference_id: String::new(),
},
EventType::BridgeSessions { .. } => EventType::BridgeSessions {
other_session: SessionId::new(),
},
EventType::TransferRequested { .. } => EventType::TransferRequested {
refer_to: String::new(),
transfer_type: String::new(),
transaction_id: String::new(),
},
EventType::Dialog3xxRedirect { .. } => EventType::Dialog3xxRedirect {
status: 0,
targets: Vec::new(),
},
EventType::Dialog4xxFailure(_) => EventType::Dialog4xxFailure(400),
EventType::Dialog5xxFailure(_) => EventType::Dialog5xxFailure(500),
EventType::Dialog6xxFailure(_) => EventType::Dialog6xxFailure(600),
EventType::ReinviteReceived { .. } => EventType::ReinviteReceived { sdp: None },
EventType::UpdateReceived { .. } => EventType::UpdateReceived { sdp: None },
EventType::RegistrationFailed(_) => EventType::RegistrationFailed(0),
EventType::UnregistrationFailed => EventType::UnregistrationFailed,
EventType::Registration401 => EventType::Registration401,
EventType::RetryRegistration => EventType::RetryRegistration,
EventType::RefreshRegistration => EventType::RefreshRegistration,
EventType::StartUnregistration => EventType::StartUnregistration,
EventType::Unregistration200OK => EventType::Unregistration200OK,
EventType::SubscriptionFailed(_) => EventType::SubscriptionFailed(0),
EventType::MessageFailed(_) => EventType::MessageFailed(0),
_ => self.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transition {
pub guards: Vec<Guard>,
pub actions: Vec<Action>,
pub next_state: Option<CallState>,
pub condition_updates: ConditionUpdates,
pub publish_events: Vec<EventTemplate>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Guard {
HasLocalSDP,
HasRemoteSDP,
HasNegotiatedConfig,
AllConditionsMet,
DialogEstablished,
MediaReady,
SDPNegotiated,
IsIdle,
InActiveCall,
IsRegistered,
IsSubscribed,
HasActiveSubscription,
HasPendingReinvite,
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, strum::VariantNames)]
pub enum Action {
CreateDialog,
CreateMediaSession,
GenerateLocalSDP,
SendSIPResponse(u16, String),
SendRejectResponse,
SendRedirectResponse,
SendINVITE,
SendACK,
SendBYE,
SendReINVITE,
RetryWithContact,
ScheduleReinviteRetry,
ClearPendingReinvite,
HoldCall,
ResumeCall,
TransferCall(String),
StartRecording,
StopRecording,
StartMediaSession,
SwitchToPassThroughOnActive,
NegotiateSDPAsUAC,
NegotiateSDPAsUAS,
PrepareEarlyMediaSDP,
SendINVITEWithAuth,
SendRequestWithAuth,
SendINVITEWithBumpedSessionExpires,
PlayAudioFile(String),
StartRecordingMedia,
StopRecordingMedia,
CreateAudioMixer,
RedirectToMixer,
ConnectToMixer,
DisconnectFromMixer,
MuteToMixer,
UnmuteToMixer,
DestroyMixer,
BridgeToMixer,
RestoreDirectMedia,
StartRecordingMixer,
StopRecordingMixer,
UpdateMediaDirection {
direction: MediaDirection,
},
HoldCurrentCall,
SetCondition(Condition, bool),
StoreLocalSDP,
StoreRemoteSDP,
StoreNegotiatedConfig,
CreateBridge(SessionId),
DestroyBridge,
RestoreMediaFlow,
ReleaseAllResources,
StartEmergencyCleanup,
AttemptMediaRecovery,
CleanupResources,
TriggerCallEstablished,
TriggerCallTerminated,
StartDialogCleanup,
StartMediaCleanup,
SendREGISTER,
SendREGISTERWithAuth,
SendUnREGISTER,
StoreAuthChallenge,
ProcessRegistrationResponse,
SendSUBSCRIBE,
ProcessNOTIFY,
SendMESSAGE,
ProcessMESSAGE,
SendINVITEWithOptions,
SendReINVITEWithOptions,
SendREGISTERWithOptions,
SendSUBSCRIBEWithOptions,
SendMESSAGEWithOptions,
SendNOTIFYWithOptions,
SendBYEWithOptions,
SendCANCELWithOptions,
SendREFERWithOptions,
SendINFOWithOptions,
SendUPDATEWithOptions,
SendOPTIONSWithOptions,
ClearPendingINVITEOptions,
ClearPendingReINVITEOptions,
ClearPendingREGISTEROptions,
ClearPendingSUBSCRIBEOptions,
ClearPendingMESSAGEOptions,
ClearPendingNOTIFYOptions,
ClearPendingBYEOptions,
ClearPendingCANCELOptions,
ClearPendingREFEROptions,
ClearPendingINFOOptions,
ClearPendingUPDATEOptions,
ClearPendingOPTIONSOptions,
CleanupDialog,
CleanupMedia,
SendReferAccepted,
SendRefer100Trying,
SendTransferNotifyRinging,
SendTransferNotifySuccess,
SendTransferNotifyFailure,
Custom(String),
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum Condition {
DialogEstablished,
MediaSessionReady,
SDPNegotiated,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConditionUpdates {
pub dialog_established: Option<bool>,
pub media_session_ready: Option<bool>,
pub sdp_negotiated: Option<bool>,
}
impl ConditionUpdates {
pub fn none() -> Self {
Self::default()
}
pub fn set_dialog_established(established: bool) -> Self {
Self {
dialog_established: Some(established),
..Default::default()
}
}
pub fn set_media_ready(ready: bool) -> Self {
Self {
media_session_ready: Some(ready),
..Default::default()
}
}
pub fn set_sdp_negotiated(negotiated: bool) -> Self {
Self {
sdp_negotiated: Some(negotiated),
..Default::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EventTemplate {
StateChanged,
SessionCreated,
IncomingCall,
CallEstablished,
CallTerminated,
CallFailed,
CallCancelled,
CallOnHold,
CallResumed,
MediaFlowEstablished,
MediaNegotiated,
MediaSessionReady,
Custom(String),
}
const CORE_STATES_REQUIRING_EXITS: &[CallState] = &[
CallState::Idle,
CallState::Initiating,
CallState::CancelPending,
CallState::Cancelling,
CallState::Ringing,
CallState::Answering,
CallState::AnsweringHangupPending,
CallState::Active,
];
pub struct MasterStateTable {
transitions: HashMap<StateKey, Transition>,
wildcard_transitions: HashMap<(Role, EventType), Transition>,
}
pub type StateTable = MasterStateTable;
impl MasterStateTable {
pub fn new() -> Self {
Self {
transitions: HashMap::new(),
wildcard_transitions: HashMap::new(),
}
}
pub fn insert(&mut self, key: StateKey, transition: Transition) {
let normalized_key = StateKey {
role: key.role,
state: key.state,
event: key.event.normalize(),
};
self.transitions.insert(normalized_key, transition);
}
pub fn insert_wildcard(&mut self, role: Role, event: EventType, transition: Transition) {
let normalized_event = event.normalize();
self.wildcard_transitions
.insert((role, normalized_event), transition);
}
pub fn get(&self, key: &StateKey) -> Option<&Transition> {
let normalized_key = StateKey {
role: key.role,
state: key.state,
event: key.event.normalize(),
};
if let Some(transition) = self.transitions.get(&normalized_key) {
return Some(transition);
}
if key.role == Role::UAC || key.role == Role::UAS {
let both_key = StateKey {
role: Role::Both,
state: key.state,
event: key.event.normalize(),
};
if let Some(transition) = self.transitions.get(&both_key) {
return Some(transition);
}
}
let normalized_event = key.event.normalize();
self.wildcard_transitions.get(&(key.role, normalized_event))
}
pub fn get_transition(&self, key: &StateKey) -> Option<&Transition> {
self.get(key)
}
pub fn has_transition(&self, key: &StateKey) -> bool {
let normalized_key = StateKey {
role: key.role,
state: key.state,
event: key.event.normalize(),
};
if self.transitions.contains_key(&normalized_key) {
return true;
}
if key.role == Role::UAC || key.role == Role::UAS {
let both_key = StateKey {
role: Role::Both,
state: key.state,
event: key.event.normalize(),
};
if self.transitions.contains_key(&both_key) {
return true;
}
}
let normalized_event = key.event.normalize();
self.wildcard_transitions
.contains_key(&(key.role, normalized_event))
}
pub fn transition_count(&self) -> usize {
self.transitions.len() + self.wildcard_transitions.len()
}
pub fn collect_used_states(&self) -> HashSet<CallState> {
let mut states = HashSet::new();
for (key, transition) in &self.transitions {
states.insert(key.state);
if let Some(next_state) = &transition.next_state {
states.insert(*next_state);
}
}
for (_, transition) in &self.wildcard_transitions {
if let Some(next_state) = &transition.next_state {
states.insert(*next_state);
}
}
states
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
let used_states = self.collect_used_states();
for state in used_states.iter() {
if matches!(
state,
CallState::Terminated | CallState::Cancelled | CallState::Failed(_)
) {
continue;
}
let has_exact_exit = self.transitions.iter().any(|(k, _)| k.state == *state);
let has_wildcard_exit = !self.wildcard_transitions.is_empty();
if !has_exact_exit && !has_wildcard_exit {
if CORE_STATES_REQUIRING_EXITS.contains(state) {
errors.push(format!("Core state {:?} has no exit transitions", state));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}