Skip to main content

ringo_core/
event.rs

1use crate::client::BaresipMessage;
2use serde_json::{Map, Value};
3
4#[derive(Debug)]
5pub enum AppEvent {
6    Registering {
7        account: String,
8    },
9    RegisterOk {
10        account: String,
11    },
12    RegisterFailed {
13        reason: String,
14    },
15    CallIncoming {
16        call_id: String,
17        number: String,
18        display_name: Option<String>,
19    },
20    CallOutgoing {
21        call_id: String,
22        number: String,
23    },
24    CallRinging {
25        call_id: String,
26    },
27    CallEstablished {
28        call_id: String,
29    },
30    CallClosed {
31        call_id: String,
32        reason: String,
33        error: bool,
34    },
35    VoicemailStatus {
36        waiting: bool,
37        new_count: u32,
38    },
39    Response {
40        ok: bool,
41        data: String,
42    },
43    Unknown {
44        class: String,
45        type_: String,
46    },
47    BaresipConnectFailed {
48        reason: String,
49    },
50}
51
52impl From<BaresipMessage> for AppEvent {
53    fn from(msg: BaresipMessage) -> Self {
54        match msg {
55            BaresipMessage::Event {
56                class,
57                type_,
58                param,
59                extra,
60            } => map_event(&class, &type_, param, &extra),
61            BaresipMessage::Response { ok, data, .. } => AppEvent::Response { ok, data },
62        }
63    }
64}
65
66fn map_event(class: &str, type_: &str, param: String, extra: &Map<String, Value>) -> AppEvent {
67    let t = type_.trim_start_matches("BEVENT_");
68    let call_id = || {
69        extra
70            .get("id")
71            .and_then(|v| v.as_str())
72            .unwrap_or("")
73            .to_string()
74    };
75    let account = || {
76        extra
77            .get("accountaor")
78            .and_then(|v| v.as_str())
79            .unwrap_or("")
80            .to_string()
81    };
82    let number = || {
83        extra
84            .get("peeruri")
85            .and_then(|v| v.as_str())
86            .map(|s| s.to_string())
87            .unwrap_or_else(|| param.clone())
88    };
89
90    match t {
91        "REGISTERING" => AppEvent::Registering { account: account() },
92        "REGISTER_OK" | "FALLBACK_OK" => AppEvent::RegisterOk { account: account() },
93        "REGISTER_FAIL" | "FALLBACK_FAIL" => AppEvent::RegisterFailed { reason: param },
94        "CALL_INCOMING" => AppEvent::CallIncoming {
95            call_id: call_id(),
96            number: number(),
97            display_name: extra
98                .get("peerdisplayname")
99                .and_then(|v| v.as_str())
100                .filter(|s| !s.is_empty())
101                .map(|s| s.to_string()),
102        },
103        "CALL_OUTGOING" => AppEvent::CallOutgoing {
104            call_id: call_id(),
105            number: number(),
106        },
107        "CALL_RINGING" => AppEvent::CallRinging { call_id: call_id() },
108        "CALL_ESTABLISHED" => AppEvent::CallEstablished { call_id: call_id() },
109        "CALL_CLOSED" => {
110            let error = is_error_reason(&param);
111            AppEvent::CallClosed {
112                call_id: call_id(),
113                reason: param,
114                error,
115            }
116        }
117
118        "MWI_NOTIFY" => parse_mwi(&param),
119        _ => {
120            crate::rlog!(
121                Debug,
122                "unknown baresip event: class={} type={}",
123                class,
124                type_
125            );
126            AppEvent::Unknown {
127                class: class.to_string(),
128                type_: type_.to_string(),
129            }
130        }
131    }
132}
133
134fn is_error_reason(reason: &str) -> bool {
135    if reason.is_empty() {
136        return false;
137    }
138    const NORMAL: &[&str] = &[
139        "Connection reset by peer",
140        "Connection closed",
141        "Rejected by user",
142        "Call transfered",
143    ];
144    !NORMAL
145        .iter()
146        .any(|n| reason.to_lowercase().starts_with(&n.to_lowercase()))
147}
148
149fn parse_mwi(param: &str) -> AppEvent {
150    let mut waiting = false;
151    let mut new_count = 0u32;
152    for line in param.lines() {
153        if let Some(val) = line.strip_prefix("Messages-Waiting:") {
154            waiting = val.trim().eq_ignore_ascii_case("yes");
155        }
156        if let Some(val) = line.strip_prefix("Voice-Message:") {
157            if let Some(new) = val.trim().split('/').next() {
158                new_count = new.trim().parse().unwrap_or(0);
159            }
160        }
161    }
162    AppEvent::VoicemailStatus { waiting, new_count }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::client::BaresipMessage;
169    use serde_json::json;
170
171    fn event_msg(type_: &str, param: &str, extra: serde_json::Value) -> BaresipMessage {
172        let mut map = extra.as_object().cloned().unwrap_or_default();
173        map.insert("class".into(), json!("call"));
174        BaresipMessage::Event {
175            class: "call".into(),
176            type_: type_.into(),
177            param: param.into(),
178            extra: map,
179        }
180    }
181
182    // ── MWI ────────────────────────────────────────────────────────────────────
183
184    #[test]
185    fn mwi_waiting_yes() {
186        let param = "Messages-Waiting: yes\r\nVoice-Message: 3/0";
187        let event = AppEvent::from(event_msg("BEVENT_MWI_NOTIFY", param, json!({})));
188        assert!(matches!(
189            event,
190            AppEvent::VoicemailStatus {
191                waiting: true,
192                new_count: 3
193            }
194        ));
195    }
196
197    #[test]
198    fn mwi_waiting_no() {
199        let param = "Messages-Waiting: no\r\nVoice-Message: 0/0";
200        let event = AppEvent::from(event_msg("BEVENT_MWI_NOTIFY", param, json!({})));
201        assert!(matches!(
202            event,
203            AppEvent::VoicemailStatus {
204                waiting: false,
205                new_count: 0
206            }
207        ));
208    }
209
210    // ── event mapping ──────────────────────────────────────────────────────────
211
212    #[test]
213    fn register_ok_event() {
214        let extra = json!({"accountaor": "sip:bob@example.com"});
215        let event = AppEvent::from(event_msg("BEVENT_REGISTER_OK", "", extra));
216        assert!(
217            matches!(event, AppEvent::RegisterOk { account } if account == "sip:bob@example.com")
218        );
219    }
220
221    #[test]
222    fn call_incoming_event() {
223        let extra = json!({"id": "call-1", "peeruri": "sip:carol@example.com"});
224        let event = AppEvent::from(event_msg("BEVENT_CALL_INCOMING", "", extra));
225        assert!(
226            matches!(event, AppEvent::CallIncoming { call_id, number, .. }
227                if call_id == "call-1" && number == "sip:carol@example.com")
228        );
229    }
230
231    #[test]
232    fn unknown_event() {
233        let event = AppEvent::from(event_msg("BEVENT_SOMETHING_NEW", "", json!({})));
234        assert!(matches!(event, AppEvent::Unknown { .. }));
235    }
236
237    // ── is_error_reason ────────────────────────────────────────────────────────
238
239    #[test]
240    fn empty_reason_is_not_error() {
241        assert!(!is_error_reason(""));
242    }
243
244    #[test]
245    fn connection_reset_is_not_error() {
246        assert!(!is_error_reason("Connection reset by peer"));
247    }
248
249    #[test]
250    fn connection_reset_with_errno_is_not_error() {
251        assert!(!is_error_reason("Connection reset by peer [104]"));
252    }
253
254    #[test]
255    fn connection_closed_is_not_error() {
256        assert!(!is_error_reason("Connection closed"));
257    }
258
259    #[test]
260    fn rejected_by_user_is_not_error() {
261        assert!(!is_error_reason("Rejected by user"));
262    }
263
264    #[test]
265    fn sip_busy_is_error() {
266        assert!(is_error_reason("486 Busy Here"));
267    }
268
269    #[test]
270    fn sip_not_found_is_error() {
271        assert!(is_error_reason("404 Not Found"));
272    }
273
274    #[test]
275    fn call_closed_error_flag() {
276        let extra = json!({"id": "call-1"});
277        let event = AppEvent::from(event_msg("BEVENT_CALL_CLOSED", "486 Busy Here", extra));
278        assert!(matches!(event, AppEvent::CallClosed { error: true, .. }));
279    }
280
281    #[test]
282    fn call_closed_no_error_flag_for_normal_close() {
283        let extra = json!({"id": "call-1"});
284        let event = AppEvent::from(event_msg(
285            "BEVENT_CALL_CLOSED",
286            "Connection reset by peer [104]",
287            extra,
288        ));
289        assert!(matches!(event, AppEvent::CallClosed { error: false, .. }));
290    }
291}