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