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