Skip to main content

ringo_core/
event.rs

1use std::collections::HashMap;
2
3/// Headers of each INVITE seen in a trace, keyed by SIP `Call-ID`. First
4/// INVITE per Call-ID wins (the call-establishing one).
5pub type InviteHeaders = HashMap<String, Vec<(String, String)>>;
6
7/// RTP media quality for a call (backend-neutral). `mos` is an estimated Mean
8/// Opinion Score (1.0 = worst … 4.5 = best).
9#[derive(Debug, Clone, Copy)]
10pub struct MediaStats {
11    /// Round-trip time in milliseconds.
12    pub rtt_ms: f64,
13    /// Receive-side inter-arrival jitter in milliseconds.
14    pub jitter_ms: f64,
15    /// Cumulative received RTP packets lost.
16    pub rx_lost: i32,
17    /// Receive-side packet loss as a percentage.
18    pub packet_loss_pct: f64,
19    /// Estimated MOS (1.0–4.5).
20    pub mos: f64,
21}
22
23#[derive(Debug, Clone)]
24pub enum AppEvent {
25    Registering {
26        account: String,
27    },
28    RegisterOk {
29        account: String,
30    },
31    RegisterFailed {
32        reason: String,
33    },
34    Unregistered {
35        account: String,
36    },
37    CallIncoming {
38        call_id: String,
39        number: String,
40        display_name: Option<String>,
41    },
42    CallOutgoing {
43        call_id: String,
44        number: String,
45    },
46    CallRinging {
47        call_id: String,
48    },
49    CallEstablished {
50        call_id: String,
51    },
52    CallClosed {
53        call_id: String,
54        reason: String,
55        error: bool,
56    },
57    VoicemailStatus {
58        waiting: bool,
59        new_count: u32,
60    },
61    Response {
62        ok: bool,
63        data: String,
64    },
65    Unknown {
66        class: String,
67        type_: String,
68    },
69    BackendConnectFailed {
70        reason: String,
71    },
72}
73
74/// Whether a call-closed reason indicates an error (not a normal close).
75/// Backend-neutral: shared by all backends that produce SIP reason strings.
76pub fn is_error_reason(reason: &str) -> bool {
77    if reason.is_empty() {
78        return false;
79    }
80    const NORMAL: &[&str] = &[
81        "Connection reset by peer",
82        "Connection closed",
83        "Rejected by user",
84        "Call transfered",
85    ];
86    !NORMAL
87        .iter()
88        .any(|n| reason.to_lowercase().starts_with(&n.to_lowercase()))
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    // ── is_error_reason ────────────────────────────────────────────────────────
96
97    #[test]
98    fn empty_reason_is_not_error() {
99        assert!(!is_error_reason(""));
100    }
101
102    #[test]
103    fn connection_reset_is_not_error() {
104        assert!(!is_error_reason("Connection reset by peer"));
105    }
106
107    #[test]
108    fn connection_reset_with_errno_is_not_error() {
109        assert!(!is_error_reason("Connection reset by peer [104]"));
110    }
111
112    #[test]
113    fn connection_closed_is_not_error() {
114        assert!(!is_error_reason("Connection closed"));
115    }
116
117    #[test]
118    fn rejected_by_user_is_not_error() {
119        assert!(!is_error_reason("Rejected by user"));
120    }
121
122    #[test]
123    fn sip_busy_is_error() {
124        assert!(is_error_reason("486 Busy Here"));
125    }
126
127    #[test]
128    fn sip_not_found_is_error() {
129        assert!(is_error_reason("404 Not Found"));
130    }
131}