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