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