use serde::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;
pub use crate::api::types::{
AudioFrame, AudioStreamConfig, CallDecision, CallDirection, CallSession, IncomingCall,
MediaInfo, PreparedCall, Session, SessionRole, SessionStats, StatusCode, TerminationReason,
};
pub use crate::state_table::types::{DialogId, MediaSessionId, SessionId};
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum FailureReason {
Timeout,
Rejected,
NetworkError,
MediaError,
ProtocolError,
Other,
}
impl fmt::Display for FailureReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FailureReason::Timeout => write!(f, "Timeout"),
FailureReason::Rejected => write!(f, "Rejected"),
FailureReason::NetworkError => write!(f, "Network error"),
FailureReason::MediaError => write!(f, "Media error"),
FailureReason::ProtocolError => write!(f, "Protocol error"),
FailureReason::Other => write!(f, "Other error"),
}
}
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum CallState {
Idle,
Initiating,
CancelPending,
Cancelling,
Ringing,
Answering, AnsweringHangupPending,
EarlyMedia,
Active,
HoldPending,
OnHold,
Resuming,
Muted,
Bridged, Transferring,
TransferringCall, ConsultationCall,
Terminating,
Terminated,
Cancelled,
Failed(FailureReason),
Registering,
Registered,
Unregistering,
Subscribing,
Subscribed,
Publishing,
Authenticating,
Messaging, }
impl CallState {
pub fn is_final(&self) -> bool {
matches!(
self,
CallState::Terminated | CallState::Cancelled | CallState::Failed(_)
)
}
pub fn is_in_progress(&self) -> bool {
matches!(
self,
CallState::Initiating
| CallState::CancelPending
| CallState::Cancelling
| CallState::Ringing
| CallState::Answering
| CallState::AnsweringHangupPending
| CallState::Active
| CallState::HoldPending
| CallState::OnHold
| CallState::EarlyMedia
| CallState::Resuming
| CallState::Muted
| CallState::Bridged
| CallState::Transferring
| CallState::TransferringCall
| CallState::ConsultationCall
)
}
}
impl fmt::Display for CallState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CallState::Idle => write!(f, "Idle"),
CallState::Initiating => write!(f, "Initiating"),
CallState::CancelPending => write!(f, "CancelPending"),
CallState::Cancelling => write!(f, "Cancelling"),
CallState::Ringing => write!(f, "Ringing"),
CallState::Answering => write!(f, "Answering"),
CallState::AnsweringHangupPending => write!(f, "AnsweringHangupPending"),
CallState::EarlyMedia => write!(f, "EarlyMedia"),
CallState::Active => write!(f, "Active"),
CallState::HoldPending => write!(f, "HoldPending"),
CallState::OnHold => write!(f, "OnHold"),
CallState::Resuming => write!(f, "Resuming"),
CallState::Muted => write!(f, "Muted"),
CallState::Bridged => write!(f, "Bridged"),
CallState::Transferring => write!(f, "Transferring"),
CallState::TransferringCall => write!(f, "TransferringCall"),
CallState::ConsultationCall => write!(f, "ConsultationCall"),
CallState::Terminating => write!(f, "Terminating"),
CallState::Terminated => write!(f, "Terminated"),
CallState::Cancelled => write!(f, "Cancelled"),
CallState::Failed(reason) => write!(f, "Failed({})", reason),
CallState::Registering => write!(f, "Registering"),
CallState::Registered => write!(f, "Registered"),
CallState::Unregistering => write!(f, "Unregistering"),
CallState::Subscribing => write!(f, "Subscribing"),
CallState::Subscribed => write!(f, "Subscribed"),
CallState::Publishing => write!(f, "Publishing"),
CallState::Authenticating => write!(f, "Authenticating"),
CallState::Messaging => write!(f, "Messaging"),
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct CallInfo {
pub session_id: SessionId,
pub from: String,
pub to: String,
pub state: CallState,
pub start_time: std::time::SystemTime,
pub media_active: bool,
}
impl fmt::Debug for CallInfo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CallInfo")
.field("session_id", &self.session_id)
.field("from_bytes", &self.from.len())
.field("to_bytes", &self.to.len())
.field("state", &self.state)
.field("media_active", &self.media_active)
.finish()
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct SessionInfo {
pub session_id: SessionId,
pub from: String,
pub to: String,
pub state: CallState,
pub start_time: std::time::SystemTime,
pub media_active: bool,
}
impl fmt::Debug for SessionInfo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SessionInfo")
.field("session_id", &self.session_id)
.field("from_bytes", &self.from.len())
.field("to_bytes", &self.to.len())
.field("state", &self.state)
.field("media_active", &self.media_active)
.finish()
}
}
pub struct AudioFrameSubscriber {
pub session_id: SessionId,
pub receiver: tokio::sync::mpsc::Receiver<rvoip_media_core::types::AudioFrame>,
}
impl AudioFrameSubscriber {
pub fn new(
session_id: SessionId,
receiver: tokio::sync::mpsc::Receiver<rvoip_media_core::types::AudioFrame>,
) -> Self {
Self {
session_id,
receiver,
}
}
pub async fn recv(&mut self) -> Option<rvoip_media_core::types::AudioFrame> {
self.receiver.recv().await
}
pub fn try_recv(
&mut self,
) -> Result<rvoip_media_core::types::AudioFrame, tokio::sync::mpsc::error::TryRecvError> {
self.receiver.try_recv()
}
}
#[derive(Clone, Serialize, Deserialize)]
pub enum SessionEvent {
IncomingCall {
from: String,
to: String,
call_id: String,
dialog_id: DialogId,
sdp: Option<String>,
},
CallProgress {
dialog_id: DialogId,
status_code: u16,
reason: Option<String>,
},
CallAnswered {
dialog_id: DialogId,
sdp: Option<String>,
},
CallTerminated {
dialog_id: DialogId,
reason: Option<String>,
},
CallFailed { dialog_id: DialogId, reason: String },
MediaStateChanged {
media_id: MediaSessionId,
state: MediaState,
},
DtmfReceived { dialog_id: DialogId, digit: char },
HoldRequest { dialog_id: DialogId },
ResumeRequest { dialog_id: DialogId },
TransferRequest {
dialog_id: DialogId,
target: String,
attended: bool,
},
RegistrationStarted {
dialog_id: DialogId,
uri: String,
expires: u32,
},
RegistrationSuccess {
dialog_id: DialogId,
uri: String,
expires: u32,
},
RegistrationFailed {
dialog_id: DialogId,
reason: String,
status_code: u16,
},
UnregistrationComplete { dialog_id: DialogId },
SubscriptionStarted {
dialog_id: DialogId,
uri: String,
event_package: String,
expires: u32,
},
SubscriptionAccepted { dialog_id: DialogId, expires: u32 },
SubscriptionFailed {
dialog_id: DialogId,
reason: String,
status_code: u16,
},
NotifyReceived {
dialog_id: DialogId,
event_package: String,
body: Option<String>,
},
MessageSent {
dialog_id: DialogId,
to: String,
body: String,
},
MessageReceived {
dialog_id: DialogId,
from: String,
body: String,
},
MessageDelivered { dialog_id: DialogId },
MessageDeliveryFailed {
dialog_id: DialogId,
reason: String,
status_code: u16,
},
}
impl fmt::Debug for SessionEvent {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IncomingCall { from, to, sdp, .. } => formatter
.debug_struct("IncomingCall")
.field("from_bytes", &from.len())
.field("to_bytes", &to.len())
.field("sdp_present", &sdp.is_some())
.field("sdp_bytes", &sdp.as_ref().map_or(0, String::len))
.finish(),
Self::CallProgress {
status_code,
reason,
..
} => formatter
.debug_struct("CallProgress")
.field("status_code", status_code)
.field("reason_present", &reason.is_some())
.field("reason_bytes", &reason.as_ref().map_or(0, String::len))
.finish(),
Self::CallAnswered { sdp, .. } => formatter
.debug_struct("CallAnswered")
.field("sdp_present", &sdp.is_some())
.field("sdp_bytes", &sdp.as_ref().map_or(0, String::len))
.finish(),
Self::CallTerminated { reason, .. } => formatter
.debug_struct("CallTerminated")
.field("reason_present", &reason.is_some())
.field("reason_bytes", &reason.as_ref().map_or(0, String::len))
.finish(),
Self::CallFailed { reason, .. } => formatter
.debug_struct("CallFailed")
.field("reason_bytes", &reason.len())
.finish(),
Self::MediaStateChanged { state, .. } => formatter
.debug_struct("MediaStateChanged")
.field("state", state)
.finish(),
Self::DtmfReceived { .. } => formatter.write_str("DtmfReceived"),
Self::HoldRequest { .. } => formatter.write_str("HoldRequest"),
Self::ResumeRequest { .. } => formatter.write_str("ResumeRequest"),
Self::TransferRequest {
attended, target, ..
} => formatter
.debug_struct("TransferRequest")
.field("target_bytes", &target.len())
.field("attended", attended)
.finish(),
Self::RegistrationStarted { uri, expires, .. } => formatter
.debug_struct("RegistrationStarted")
.field("uri_bytes", &uri.len())
.field("expires", expires)
.finish(),
Self::RegistrationSuccess { uri, expires, .. } => formatter
.debug_struct("RegistrationSuccess")
.field("uri_bytes", &uri.len())
.field("expires", expires)
.finish(),
Self::RegistrationFailed {
reason,
status_code,
..
} => formatter
.debug_struct("RegistrationFailed")
.field("reason_bytes", &reason.len())
.field("status_code", status_code)
.finish(),
Self::UnregistrationComplete { .. } => formatter.write_str("UnregistrationComplete"),
Self::SubscriptionStarted {
uri,
event_package,
expires,
..
} => formatter
.debug_struct("SubscriptionStarted")
.field("uri_bytes", &uri.len())
.field("event_package_bytes", &event_package.len())
.field("expires", expires)
.finish(),
Self::SubscriptionAccepted { expires, .. } => formatter
.debug_struct("SubscriptionAccepted")
.field("expires", expires)
.finish(),
Self::SubscriptionFailed {
reason,
status_code,
..
} => formatter
.debug_struct("SubscriptionFailed")
.field("reason_bytes", &reason.len())
.field("status_code", status_code)
.finish(),
Self::NotifyReceived {
event_package,
body,
..
} => formatter
.debug_struct("NotifyReceived")
.field("event_package_bytes", &event_package.len())
.field("body_present", &body.is_some())
.field("body_bytes", &body.as_ref().map_or(0, String::len))
.finish(),
Self::MessageSent { to, body, .. } => formatter
.debug_struct("MessageSent")
.field("to_bytes", &to.len())
.field("body_bytes", &body.len())
.finish(),
Self::MessageReceived { from, body, .. } => formatter
.debug_struct("MessageReceived")
.field("from_bytes", &from.len())
.field("body_bytes", &body.len())
.finish(),
Self::MessageDelivered { .. } => formatter.write_str("MessageDelivered"),
Self::MessageDeliveryFailed {
reason,
status_code,
..
} => formatter
.debug_struct("MessageDeliveryFailed")
.field("reason_bytes", &reason.len())
.field("status_code", status_code)
.finish(),
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MediaState {
Idle,
Negotiating,
Active,
OnHold,
Failed(String),
Terminated,
}
impl fmt::Debug for MediaState {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Idle => formatter.write_str("Idle"),
Self::Negotiating => formatter.write_str("Negotiating"),
Self::Active => formatter.write_str("Active"),
Self::OnHold => formatter.write_str("OnHold"),
Self::Failed(reason) => formatter
.debug_struct("Failed")
.field("reason_bytes", &reason.len())
.finish(),
Self::Terminated => formatter.write_str("Terminated"),
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransferStatus {
Initiated,
InProgress,
Completed,
Failed(String),
}
impl fmt::Debug for TransferStatus {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Initiated => formatter.write_str("Initiated"),
Self::InProgress => formatter.write_str("InProgress"),
Self::Completed => formatter.write_str("Completed"),
Self::Failed(reason) => formatter
.debug_struct("Failed")
.field("reason_bytes", &reason.len())
.finish(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MediaDirection {
SendRecv,
SendOnly,
RecvOnly,
Inactive,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RegistrationState {
Unregistered,
Registering,
Registered,
Failed(String),
Unregistering,
}
impl fmt::Debug for RegistrationState {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unregistered => formatter.write_str("Unregistered"),
Self::Registering => formatter.write_str("Registering"),
Self::Registered => formatter.write_str("Registered"),
Self::Failed(reason) => formatter
.debug_struct("Failed")
.field("reason_bytes", &reason.len())
.finish(),
Self::Unregistering => formatter.write_str("Unregistering"),
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PresenceStatus {
Available,
Away,
Busy,
DoNotDisturb,
Offline,
Custom(String),
}
impl fmt::Debug for PresenceStatus {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Available => formatter.write_str("Available"),
Self::Away => formatter.write_str("Away"),
Self::Busy => formatter.write_str("Busy"),
Self::DoNotDisturb => formatter.write_str("DoNotDisturb"),
Self::Offline => formatter.write_str("Offline"),
Self::Custom(value) => formatter
.debug_struct("Custom")
.field("bytes", &value.len())
.finish(),
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Credentials {
pub username: String,
pub password: String,
pub realm: Option<String>,
}
impl fmt::Debug for Credentials {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Credentials")
.field("username", &"[redacted]")
.field("password", &"[redacted]")
.field("has_realm", &self.realm.is_some())
.finish()
}
}
impl Credentials {
pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
Self {
username: username.into(),
password: password.into(),
realm: None,
}
}
pub fn with_realm(mut self, realm: impl Into<String>) -> Self {
self.realm = Some(realm.into());
self
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct AudioDevice {
pub id: String,
pub name: String,
pub is_input: bool,
pub is_output: bool,
pub sample_rates: Vec<u32>,
pub channels: u8,
}
impl fmt::Debug for AudioDevice {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("AudioDevice")
.field("id_bytes", &self.id.len())
.field("name_bytes", &self.name.len())
.field("is_input", &self.is_input)
.field("is_output", &self.is_output)
.field("sample_rates", &self.sample_rates)
.field("channels", &self.channels)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ConferenceId(pub Uuid);
impl ConferenceId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
impl Default for ConferenceId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for ConferenceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct CallDetailRecord {
pub session_id: SessionId,
pub dialog_id: DialogId,
pub from: String,
pub to: String,
pub start_time: std::time::SystemTime,
pub end_time: Option<std::time::SystemTime>,
pub duration: Option<u64>,
pub termination_reason: Option<String>,
pub call_id: String,
}
impl fmt::Debug for CallDetailRecord {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CallDetailRecord")
.field("session_id", &self.session_id)
.field("dialog_id", &"[opaque]")
.field("from_bytes", &self.from.len())
.field("to_bytes", &self.to.len())
.field("end_time_present", &self.end_time.is_some())
.field("duration", &self.duration)
.field(
"termination_reason_present",
&self.termination_reason.is_some(),
)
.field(
"termination_reason_bytes",
&self.termination_reason.as_ref().map_or(0, String::len),
)
.field("call_id_bytes", &self.call_id.len())
.finish()
}
}
#[derive(Clone)]
pub struct IncomingCallInfo {
pub session_id: SessionId,
pub dialog_id: DialogId,
pub from: String,
pub to: String,
pub call_id: String,
pub p_asserted_identity: Option<String>,
}
impl fmt::Debug for IncomingCallInfo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IncomingCallInfo")
.field("session_id", &self.session_id)
.field("dialog_id", &"[opaque]")
.field("from_bytes", &self.from.len())
.field("to_bytes", &self.to.len())
.field("call_id_bytes", &self.call_id.len())
.field(
"p_asserted_identity_present",
&self.p_asserted_identity.is_some(),
)
.field(
"p_asserted_identity_bytes",
&self.p_asserted_identity.as_ref().map_or(0, String::len),
)
.finish()
}
}
#[cfg(test)]
mod diagnostic_safety_tests {
use super::*;
#[test]
fn public_session_and_cdr_debug_is_payload_free() {
const SECRET: &str = "legacy-session-type-secret-canary";
let session_id = SessionId::from_string(SECRET);
let dialog_id = DialogId::new();
let event = SessionEvent::IncomingCall {
from: SECRET.to_string(),
to: SECRET.to_string(),
call_id: SECRET.to_string(),
dialog_id,
sdp: Some(SECRET.to_string()),
};
let cdr = CallDetailRecord {
session_id: session_id.clone(),
dialog_id,
from: SECRET.to_string(),
to: SECRET.to_string(),
start_time: std::time::SystemTime::now(),
end_time: None,
duration: None,
termination_reason: Some(SECRET.to_string()),
call_id: SECRET.to_string(),
};
let incoming = IncomingCallInfo {
session_id,
dialog_id,
from: SECRET.to_string(),
to: SECRET.to_string(),
call_id: SECRET.to_string(),
p_asserted_identity: Some(SECRET.to_string()),
};
for rendered in [
format!("{event:?}"),
format!("{cdr:?}"),
format!("{incoming:?}"),
] {
assert!(!rendered.contains(SECRET), "debug leaked: {rendered}");
}
}
}