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(¶m);
111 AppEvent::CallClosed {
112 call_id: call_id(),
113 reason: param,
114 error,
115 }
116 }
117
118 "MWI_NOTIFY" => parse_mwi(¶m),
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 #[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 #[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 #[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}