1use crate::media::{ambiance::AmbianceOption, recorder::RecorderFormat};
2use crate::useragent::RegisterOption;
3use anyhow::{Error, Result};
4use clap::Parser;
5use rustrtc::IceServer;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Parser, Debug)]
10#[command(version)]
11pub struct Cli {
12 #[clap(long)]
14 pub conf: Option<String>,
15 #[clap(long)]
17 pub http: Option<String>,
18
19 #[clap(long)]
21 pub sip: Option<String>,
22
23 #[clap(long)]
25 pub handler: Option<String>,
26
27 #[clap(long)]
29 pub call: Option<String>,
30
31 #[clap(long)]
33 pub external_ip: Option<String>,
34
35 #[clap(long, value_delimiter = ',')]
37 pub codecs: Option<Vec<String>>,
38
39 #[cfg(feature = "offline")]
41 #[clap(long)]
42 pub download_models: Option<String>,
43
44 #[cfg(feature = "offline")]
46 #[clap(long, default_value = "./models")]
47 pub models_dir: String,
48
49 #[cfg(feature = "offline")]
51 #[clap(long)]
52 pub exit_after_download: bool,
53}
54
55pub(crate) fn default_config_recorder_path() -> String {
56 #[cfg(target_os = "windows")]
57 return "./config/recorders".to_string();
58 #[cfg(not(target_os = "windows"))]
59 return "./config/recorders".to_string();
60}
61
62fn default_config_media_cache_path() -> String {
63 #[cfg(target_os = "windows")]
64 return "./config/mediacache".to_string();
65 #[cfg(not(target_os = "windows"))]
66 return "./config/mediacache".to_string();
67}
68
69fn default_config_http_addr() -> String {
70 "0.0.0.0:8080".to_string()
71}
72
73fn default_sip_addr() -> String {
74 "0.0.0.0".to_string()
75}
76
77fn default_sip_port() -> u16 {
78 25060
79}
80
81fn default_config_rtp_start_port() -> Option<u16> {
82 Some(12000)
83}
84
85fn default_config_rtp_end_port() -> Option<u16> {
86 Some(42000)
87}
88
89fn default_config_rtp_latching() -> Option<bool> {
90 Some(true)
91}
92
93fn default_graceful_shutdown() -> Option<bool> {
94 Some(true)
95}
96
97fn default_config_useragent() -> Option<String> {
98 Some(format!(
99 "active-call({} miuda.ai)",
100 env!("CARGO_PKG_VERSION")
101 ))
102}
103
104fn default_enable_options_response() -> Option<bool> {
105 Some(true)
106}
107
108fn default_codecs() -> Option<Vec<String>> {
109 let codecs = vec![
110 "pcmu".to_string(),
111 "pcma".to_string(),
112 "g722".to_string(),
113 "g729".to_string(),
114 "opus".to_string(),
115 "telephone_event".to_string(),
116 ];
117 Some(codecs)
118}
119
120#[derive(Debug, Clone, Deserialize, Serialize, Default)]
121#[serde(rename_all = "snake_case")]
122pub struct RecordingPolicy {
123 #[serde(default)]
124 pub enabled: bool,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub auto_start: Option<bool>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub filename_pattern: Option<String>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub samplerate: Option<u32>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub ptime: Option<u32>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub path: Option<String>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub format: Option<RecorderFormat>,
137}
138
139impl RecordingPolicy {
140 pub fn recorder_path(&self) -> String {
141 self.path
142 .as_ref()
143 .map(|p| p.trim())
144 .filter(|p| !p.is_empty())
145 .map(|p| p.to_string())
146 .unwrap_or_else(default_config_recorder_path)
147 }
148
149 pub fn recorder_format(&self) -> RecorderFormat {
150 self.format.unwrap_or_default()
151 }
152
153 pub fn ensure_defaults(&mut self) -> bool {
154 if self
155 .path
156 .as_ref()
157 .map(|p| p.trim().is_empty())
158 .unwrap_or(true)
159 {
160 self.path = Some(default_config_recorder_path());
161 }
162
163 false
164 }
165}
166
167#[derive(Debug, Clone, Deserialize, Serialize)]
168pub struct RewriteRule {
169 pub r#match: String,
170 pub rewrite: String,
171}
172
173#[derive(Debug, Deserialize, Serialize)]
174pub struct Config {
175 #[serde(default = "default_config_http_addr")]
176 pub http_addr: String,
177 pub addr: String,
178 pub udp_port: u16,
179 pub auto_learn_public_address: Option<bool>,
180
181 pub log_level: Option<String>,
182 pub log_file: Option<String>,
183 #[serde(default, skip_serializing_if = "Vec::is_empty")]
184 pub http_access_skip_paths: Vec<String>,
185
186 #[serde(default = "default_config_useragent")]
187 pub useragent: Option<String>,
188 pub register_users: Option<Vec<RegisterOption>>,
189 #[serde(default = "default_graceful_shutdown")]
190 pub graceful_shutdown: Option<bool>,
191 pub handler: Option<InviteHandlerConfig>,
192 pub accept_timeout: Option<String>,
193 #[serde(default = "default_codecs")]
194 pub codecs: Option<Vec<String>>,
195 pub external_ip: Option<String>,
196 #[serde(default = "default_config_rtp_start_port")]
197 pub rtp_start_port: Option<u16>,
198 #[serde(default = "default_config_rtp_end_port")]
199 pub rtp_end_port: Option<u16>,
200 #[serde(default = "default_config_rtp_latching")]
201 pub enable_rtp_latching: Option<bool>,
202 pub enable_ice_lite: Option<bool>,
203 pub rtp_bind_ip: Option<String>,
204 pub tls_port: Option<u16>,
205 pub tls_cert_file: Option<String>,
206 pub tls_key_file: Option<String>,
207
208 pub enable_srtp: Option<bool>,
209
210 pub callrecord: Option<CallRecordConfig>,
211 #[serde(default = "default_config_media_cache_path")]
212 pub media_cache_path: String,
213 pub ambiance: Option<AmbianceOption>,
214 pub ice_servers: Option<Vec<IceServer>>,
215 #[serde(default)]
216 pub recording: Option<RecordingPolicy>,
217 pub rewrites: Option<Vec<RewriteRule>>,
218 #[serde(default = "default_enable_options_response")]
219 pub enable_options_response: Option<bool>,
220}
221
222#[derive(Debug, Deserialize, Clone, Serialize)]
223#[serde(rename_all = "snake_case")]
224#[serde(tag = "type")]
225pub enum InviteHandlerConfig {
226 Webhook {
227 url: Option<String>,
228 urls: Option<Vec<String>>,
229 method: Option<String>,
230 headers: Option<Vec<(String, String)>>,
231 },
232 Playbook {
233 rules: Option<Vec<PlaybookRule>>,
234 default: Option<String>,
235 },
236}
237
238#[derive(Debug, Deserialize, Clone, Serialize)]
239#[serde(rename_all = "snake_case")]
240pub struct PlaybookRule {
241 pub caller: Option<String>,
242 pub callee: Option<String>,
243 pub playbook: String,
244}
245
246#[derive(Debug, Deserialize, Clone, Serialize)]
247#[serde(rename_all = "snake_case")]
248pub enum S3Vendor {
249 Aliyun,
250 Tencent,
251 Minio,
252 AWS,
253 GCP,
254 Azure,
255 DigitalOcean,
256}
257
258#[derive(Debug, Deserialize, Clone, Serialize)]
259#[serde(tag = "type")]
260#[serde(rename_all = "snake_case")]
261pub enum CallRecordConfig {
262 Local {
263 root: String,
264 },
265 S3 {
266 vendor: S3Vendor,
267 bucket: String,
268 region: String,
269 access_key: String,
270 secret_key: String,
271 endpoint: String,
272 root: String,
273 with_media: Option<bool>,
274 keep_media_copy: Option<bool>,
275 },
276 Http {
277 url: String,
278 headers: Option<HashMap<String, String>>,
279 with_media: Option<bool>,
280 keep_media_copy: Option<bool>,
281 },
282}
283
284impl Default for CallRecordConfig {
285 fn default() -> Self {
286 Self::Local {
287 #[cfg(target_os = "windows")]
288 root: "./config/cdr".to_string(),
289 #[cfg(not(target_os = "windows"))]
290 root: "./config/cdr".to_string(),
291 }
292 }
293}
294
295impl Default for Config {
296 fn default() -> Self {
297 Self {
298 http_addr: default_config_http_addr(),
299 log_level: None,
300 log_file: None,
301 http_access_skip_paths: Vec::new(),
302 addr: default_sip_addr(),
303 udp_port: default_sip_port(),
304 auto_learn_public_address: None,
305 useragent: None,
306 register_users: None,
307 graceful_shutdown: Some(true),
308 handler: None,
309 accept_timeout: Some("50s".to_string()),
310 media_cache_path: default_config_media_cache_path(),
311 ambiance: None,
312 callrecord: None,
313 ice_servers: None,
314 codecs: None,
315 external_ip: None,
316 rtp_start_port: default_config_rtp_start_port(),
317 rtp_end_port: default_config_rtp_end_port(),
318 enable_rtp_latching: Some(true),
319 rtp_bind_ip: None,
320 enable_ice_lite: None,
321 tls_port: None,
322 tls_cert_file: None,
323 tls_key_file: None,
324 enable_srtp: None,
325 recording: None,
326 rewrites: None,
327 enable_options_response: default_enable_options_response(),
328 }
329 }
330}
331
332impl Clone for Config {
333 fn clone(&self) -> Self {
334 let s = toml::to_string(self).unwrap();
337 toml::from_str(&s).unwrap()
338 }
339}
340
341impl Config {
342 pub fn load(path: &str) -> Result<Self, Error> {
343 let config: Self = toml::from_str(
344 &std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("{}: {}", e, path))?,
345 )?;
346 Ok(config)
347 }
348
349 pub fn recorder_path(&self) -> String {
350 self.recording
351 .as_ref()
352 .map(|policy| policy.recorder_path())
353 .unwrap_or_else(default_config_recorder_path)
354 }
355
356 pub fn recorder_format(&self) -> RecorderFormat {
357 self.recording
358 .as_ref()
359 .map(|policy| policy.recorder_format())
360 .unwrap_or_default()
361 }
362
363 pub fn ensure_recording_defaults(&mut self) -> bool {
364 let mut fallback = false;
365
366 if let Some(policy) = self.recording.as_mut() {
367 fallback |= policy.ensure_defaults();
368 }
369
370 fallback
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_playbook_handler_config_parsing() {
380 let toml_config = r#"
381http_addr = "0.0.0.0:8080"
382addr = "0.0.0.0"
383udp_port = 25060
384
385[handler]
386type = "playbook"
387default = "default.md"
388
389[[handler.rules]]
390caller = "^\\+1\\d{10}$"
391callee = "^sip:support@.*"
392playbook = "support.md"
393
394[[handler.rules]]
395caller = "^\\+86\\d+"
396playbook = "chinese.md"
397
398[[handler.rules]]
399callee = "^sip:sales@.*"
400playbook = "sales.md"
401"#;
402
403 let config: Config = toml::from_str(toml_config).unwrap();
404
405 assert!(config.handler.is_some());
406 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
407 assert_eq!(default, Some("default.md".to_string()));
408 let rules = rules.unwrap();
409 assert_eq!(rules.len(), 3);
410
411 assert_eq!(rules[0].caller, Some(r"^\+1\d{10}$".to_string()));
412 assert_eq!(rules[0].callee, Some("^sip:support@.*".to_string()));
413 assert_eq!(rules[0].playbook, "support.md");
414
415 assert_eq!(rules[1].caller, Some(r"^\+86\d+".to_string()));
416 assert_eq!(rules[1].callee, None);
417 assert_eq!(rules[1].playbook, "chinese.md");
418
419 assert_eq!(rules[2].caller, None);
420 assert_eq!(rules[2].callee, Some("^sip:sales@.*".to_string()));
421 assert_eq!(rules[2].playbook, "sales.md");
422 } else {
423 panic!("Expected Playbook handler config");
424 }
425 }
426
427 #[test]
428 fn test_playbook_handler_config_without_default() {
429 let toml_config = r#"
430http_addr = "0.0.0.0:8080"
431addr = "0.0.0.0"
432udp_port = 25060
433
434[handler]
435type = "playbook"
436
437[[handler.rules]]
438caller = "^\\+1.*"
439playbook = "us.md"
440"#;
441
442 let config: Config = toml::from_str(toml_config).unwrap();
443
444 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
445 assert_eq!(default, None);
446 let rules = rules.unwrap();
447 assert_eq!(rules.len(), 1);
448 } else {
449 panic!("Expected Playbook handler config");
450 }
451 }
452
453 #[test]
454 fn test_webhook_handler_config_still_works() {
455 let toml_config = r#"
456http_addr = "0.0.0.0:8080"
457addr = "0.0.0.0"
458udp_port = 25060
459
460[handler]
461type = "webhook"
462url = "http://example.com/webhook"
463"#;
464
465 let config: Config = toml::from_str(toml_config).unwrap();
466
467 if let Some(InviteHandlerConfig::Webhook { url, .. }) = config.handler {
468 assert_eq!(url, Some("http://example.com/webhook".to_string()));
469 } else {
470 panic!("Expected Webhook handler config");
471 }
472 }
473}