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
198 pub callrecord: Option<CallRecordConfig>,
199 #[serde(default = "default_config_media_cache_path")]
200 pub media_cache_path: String,
201 pub ambiance: Option<AmbianceOption>,
202 pub ice_servers: Option<Vec<IceServer>>,
203 #[serde(default)]
204 pub recording: Option<RecordingPolicy>,
205 pub rewrites: Option<Vec<RewriteRule>>,
206}
207
208#[derive(Debug, Deserialize, Clone, Serialize)]
209#[serde(rename_all = "snake_case")]
210#[serde(tag = "type")]
211pub enum InviteHandlerConfig {
212 Webhook {
213 url: Option<String>,
214 urls: Option<Vec<String>>,
215 method: Option<String>,
216 headers: Option<Vec<(String, String)>>,
217 },
218 Playbook {
219 rules: Option<Vec<PlaybookRule>>,
220 default: Option<String>,
221 },
222}
223
224#[derive(Debug, Deserialize, Clone, Serialize)]
225#[serde(rename_all = "snake_case")]
226pub struct PlaybookRule {
227 pub caller: Option<String>,
228 pub callee: Option<String>,
229 pub playbook: String,
230}
231
232#[derive(Debug, Deserialize, Clone, Serialize)]
233#[serde(rename_all = "snake_case")]
234pub enum S3Vendor {
235 Aliyun,
236 Tencent,
237 Minio,
238 AWS,
239 GCP,
240 Azure,
241 DigitalOcean,
242}
243
244#[derive(Debug, Deserialize, Clone, Serialize)]
245#[serde(tag = "type")]
246#[serde(rename_all = "snake_case")]
247pub enum CallRecordConfig {
248 Local {
249 root: String,
250 },
251 S3 {
252 vendor: S3Vendor,
253 bucket: String,
254 region: String,
255 access_key: String,
256 secret_key: String,
257 endpoint: String,
258 root: String,
259 with_media: Option<bool>,
260 keep_media_copy: Option<bool>,
261 },
262 Http {
263 url: String,
264 headers: Option<HashMap<String, String>>,
265 with_media: Option<bool>,
266 keep_media_copy: Option<bool>,
267 },
268}
269
270impl Default for CallRecordConfig {
271 fn default() -> Self {
272 Self::Local {
273 #[cfg(target_os = "windows")]
274 root: "./config/cdr".to_string(),
275 #[cfg(not(target_os = "windows"))]
276 root: "./config/cdr".to_string(),
277 }
278 }
279}
280
281impl Default for Config {
282 fn default() -> Self {
283 Self {
284 http_addr: default_config_http_addr(),
285 log_level: None,
286 log_file: None,
287 http_access_skip_paths: Vec::new(),
288 addr: default_sip_addr(),
289 udp_port: default_sip_port(),
290 useragent: None,
291 register_users: None,
292 graceful_shutdown: Some(true),
293 handler: None,
294 accept_timeout: Some("50s".to_string()),
295 media_cache_path: default_config_media_cache_path(),
296 ambiance: None,
297 callrecord: None,
298 ice_servers: None,
299 codecs: None,
300 external_ip: None,
301 rtp_start_port: default_config_rtp_start_port(),
302 rtp_end_port: default_config_rtp_end_port(),
303 enable_rtp_latching: Some(true),
304 recording: None,
305 rewrites: None,
306 }
307 }
308}
309
310impl Clone for Config {
311 fn clone(&self) -> Self {
312 let s = toml::to_string(self).unwrap();
315 toml::from_str(&s).unwrap()
316 }
317}
318
319impl Config {
320 pub fn load(path: &str) -> Result<Self, Error> {
321 let config: Self = toml::from_str(
322 &std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("{}: {}", e, path))?,
323 )?;
324 Ok(config)
325 }
326
327 pub fn recorder_path(&self) -> String {
328 self.recording
329 .as_ref()
330 .map(|policy| policy.recorder_path())
331 .unwrap_or_else(default_config_recorder_path)
332 }
333
334 pub fn recorder_format(&self) -> RecorderFormat {
335 self.recording
336 .as_ref()
337 .map(|policy| policy.recorder_format())
338 .unwrap_or_default()
339 }
340
341 pub fn ensure_recording_defaults(&mut self) -> bool {
342 let mut fallback = false;
343
344 if let Some(policy) = self.recording.as_mut() {
345 fallback |= policy.ensure_defaults();
346 }
347
348 fallback
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_playbook_handler_config_parsing() {
358 let toml_config = r#"
359http_addr = "0.0.0.0:8080"
360addr = "0.0.0.0"
361udp_port = 25060
362
363[handler]
364type = "playbook"
365default = "default.md"
366
367[[handler.rules]]
368caller = "^\\+1\\d{10}$"
369callee = "^sip:support@.*"
370playbook = "support.md"
371
372[[handler.rules]]
373caller = "^\\+86\\d+"
374playbook = "chinese.md"
375
376[[handler.rules]]
377callee = "^sip:sales@.*"
378playbook = "sales.md"
379"#;
380
381 let config: Config = toml::from_str(toml_config).unwrap();
382
383 assert!(config.handler.is_some());
384 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
385 assert_eq!(default, Some("default.md".to_string()));
386 let rules = rules.unwrap();
387 assert_eq!(rules.len(), 3);
388
389 assert_eq!(rules[0].caller, Some(r"^\+1\d{10}$".to_string()));
390 assert_eq!(rules[0].callee, Some("^sip:support@.*".to_string()));
391 assert_eq!(rules[0].playbook, "support.md");
392
393 assert_eq!(rules[1].caller, Some(r"^\+86\d+".to_string()));
394 assert_eq!(rules[1].callee, None);
395 assert_eq!(rules[1].playbook, "chinese.md");
396
397 assert_eq!(rules[2].caller, None);
398 assert_eq!(rules[2].callee, Some("^sip:sales@.*".to_string()));
399 assert_eq!(rules[2].playbook, "sales.md");
400 } else {
401 panic!("Expected Playbook handler config");
402 }
403 }
404
405 #[test]
406 fn test_playbook_handler_config_without_default() {
407 let toml_config = r#"
408http_addr = "0.0.0.0:8080"
409addr = "0.0.0.0"
410udp_port = 25060
411
412[handler]
413type = "playbook"
414
415[[handler.rules]]
416caller = "^\\+1.*"
417playbook = "us.md"
418"#;
419
420 let config: Config = toml::from_str(toml_config).unwrap();
421
422 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
423 assert_eq!(default, None);
424 let rules = rules.unwrap();
425 assert_eq!(rules.len(), 1);
426 } else {
427 panic!("Expected Playbook handler config");
428 }
429 }
430
431 #[test]
432 fn test_webhook_handler_config_still_works() {
433 let toml_config = r#"
434http_addr = "0.0.0.0:8080"
435addr = "0.0.0.0"
436udp_port = 25060
437
438[handler]
439type = "webhook"
440url = "http://example.com/webhook"
441"#;
442
443 let config: Config = toml::from_str(toml_config).unwrap();
444
445 if let Some(InviteHandlerConfig::Webhook { url, .. }) = config.handler {
446 assert_eq!(url, Some("http://example.com/webhook".to_string()));
447 } else {
448 panic!("Expected Webhook handler config");
449 }
450 }
451}