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