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