active_call/
config.rs

1use crate::useragent::RegisterOption;
2use anyhow::{Error, Result};
3use clap::Parser;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use voice_engine::{IceServer, media::recorder::RecorderFormat};
7
8#[derive(Parser, Debug)]
9#[command(version)]
10pub struct Cli {
11    #[clap(long)]
12    pub conf: Option<String>,
13
14    #[clap(long)]
15    pub http: Option<String>,
16
17    #[clap(long)]
18    pub sip: Option<String>,
19}
20
21pub(crate) fn default_config_recorder_path() -> String {
22    #[cfg(target_os = "windows")]
23    return "./config/recorders".to_string();
24    #[cfg(not(target_os = "windows"))]
25    return "./config/recorders".to_string();
26}
27
28fn default_config_media_cache_path() -> String {
29    #[cfg(target_os = "windows")]
30    return "./config/mediacache".to_string();
31    #[cfg(not(target_os = "windows"))]
32    return "./config/mediacache".to_string();
33}
34
35fn default_config_http_addr() -> String {
36    "0.0.0.0:8080".to_string()
37}
38
39fn default_sip_addr() -> String {
40    "0.0.0.0".to_string()
41}
42
43fn default_sip_port() -> u16 {
44    25060
45}
46
47fn default_config_rtp_start_port() -> Option<u16> {
48    Some(12000)
49}
50
51fn default_config_rtp_end_port() -> Option<u16> {
52    Some(42000)
53}
54
55#[derive(Debug, Clone, Deserialize, Serialize, Default)]
56#[serde(rename_all = "snake_case")]
57pub struct RecordingPolicy {
58    #[serde(default)]
59    pub enabled: bool,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub auto_start: Option<bool>,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub filename_pattern: Option<String>,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub samplerate: Option<u32>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub ptime: Option<u32>,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub path: Option<String>,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub format: Option<RecorderFormat>,
72}
73
74impl RecordingPolicy {
75    pub fn recorder_path(&self) -> String {
76        self.path
77            .as_ref()
78            .map(|p| p.trim())
79            .filter(|p| !p.is_empty())
80            .map(|p| p.to_string())
81            .unwrap_or_else(default_config_recorder_path)
82    }
83
84    pub fn recorder_format(&self) -> RecorderFormat {
85        self.format.unwrap_or_default()
86    }
87
88    pub fn ensure_defaults(&mut self) -> bool {
89        if self
90            .path
91            .as_ref()
92            .map(|p| p.trim().is_empty())
93            .unwrap_or(true)
94        {
95            self.path = Some(default_config_recorder_path());
96        }
97
98        let original = self.format.unwrap_or_default();
99        let effective = original.effective();
100        self.format = Some(effective);
101        original != effective
102    }
103}
104
105#[derive(Debug, Clone, Deserialize, Serialize)]
106pub struct RewriteRule {
107    pub r#match: String,
108    pub rewrite: String,
109}
110
111#[derive(Debug, Deserialize, Serialize)]
112pub struct Config {
113    #[serde(default = "default_config_http_addr")]
114    pub http_addr: String,
115    pub addr: String,
116    pub udp_port: u16,
117
118    pub log_level: Option<String>,
119    pub log_file: Option<String>,
120    #[serde(default, skip_serializing_if = "Vec::is_empty")]
121    pub http_access_skip_paths: Vec<String>,
122
123    pub useragent: Option<String>,
124    pub register_users: Option<Vec<RegisterOption>>,
125    pub graceful_shutdown: Option<bool>,
126    pub handler: Option<InviteHandlerConfig>,
127    pub accept_timeout: Option<String>,
128    pub codecs: Option<Vec<String>>,
129    pub external_ip: Option<String>,
130    #[serde(default = "default_config_rtp_start_port")]
131    pub rtp_start_port: Option<u16>,
132    #[serde(default = "default_config_rtp_end_port")]
133    pub rtp_end_port: Option<u16>,
134
135    pub callrecord: Option<CallRecordConfig>,
136    #[serde(default = "default_config_media_cache_path")]
137    pub media_cache_path: String,
138    pub ice_servers: Option<Vec<IceServer>>,
139    #[serde(default)]
140    pub recording: Option<RecordingPolicy>,
141    pub rewrites: Option<Vec<RewriteRule>>,
142}
143
144#[derive(Debug, Deserialize, Clone, Serialize)]
145#[serde(rename_all = "snake_case")]
146#[serde(tag = "type")]
147pub enum InviteHandlerConfig {
148    Webhook {
149        url: Option<String>,
150        urls: Option<Vec<String>>,
151        method: Option<String>,
152        headers: Option<Vec<(String, String)>>,
153    },
154}
155
156#[derive(Debug, Deserialize, Clone, Serialize)]
157#[serde(rename_all = "snake_case")]
158pub enum S3Vendor {
159    Aliyun,
160    Tencent,
161    Minio,
162    AWS,
163    GCP,
164    Azure,
165    DigitalOcean,
166}
167
168#[derive(Debug, Deserialize, Clone, Serialize)]
169#[serde(tag = "type")]
170#[serde(rename_all = "snake_case")]
171pub enum CallRecordConfig {
172    Local {
173        root: String,
174    },
175    S3 {
176        vendor: S3Vendor,
177        bucket: String,
178        region: String,
179        access_key: String,
180        secret_key: String,
181        endpoint: String,
182        root: String,
183        with_media: Option<bool>,
184        keep_media_copy: Option<bool>,
185    },
186    Http {
187        url: String,
188        headers: Option<HashMap<String, String>>,
189        with_media: Option<bool>,
190        keep_media_copy: Option<bool>,
191    },
192}
193
194impl Default for CallRecordConfig {
195    fn default() -> Self {
196        Self::Local {
197            #[cfg(target_os = "windows")]
198            root: "./config/cdr".to_string(),
199            #[cfg(not(target_os = "windows"))]
200            root: "./config/cdr".to_string(),
201        }
202    }
203}
204
205impl Default for Config {
206    fn default() -> Self {
207        Self {
208            http_addr: default_config_http_addr(),
209            log_level: None,
210            log_file: None,
211            http_access_skip_paths: Vec::new(),
212            addr: default_sip_addr(),
213            udp_port: default_sip_port(),
214            useragent: None,
215            register_users: None,
216            graceful_shutdown: Some(true),
217            handler: None,
218            accept_timeout: Some("50s".to_string()),
219            media_cache_path: default_config_media_cache_path(),
220            callrecord: None,
221            ice_servers: None,
222            codecs: None,
223            external_ip: None,
224            rtp_start_port: default_config_rtp_start_port(),
225            rtp_end_port: default_config_rtp_end_port(),
226            recording: None,
227            rewrites: None,
228        }
229    }
230}
231
232impl Clone for Config {
233    fn clone(&self) -> Self {
234        // This is a bit expensive but Config is not cloned often in hot paths
235        // and implementing Clone manually for all nested structs is tedious
236        let s = toml::to_string(self).unwrap();
237        toml::from_str(&s).unwrap()
238    }
239}
240
241impl Config {
242    pub fn load(path: &str) -> Result<Self, Error> {
243        let mut config: Self = toml::from_str(
244            &std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("{}: {}", e, path))?,
245        )?;
246        if config.ensure_recording_defaults() {
247            tracing::warn!(
248                "recorder_format=ogg requires compiling with the 'opus' feature; falling back to wav"
249            );
250        }
251        Ok(config)
252    }
253
254    pub fn recorder_path(&self) -> String {
255        self.recording
256            .as_ref()
257            .map(|policy| policy.recorder_path())
258            .unwrap_or_else(default_config_recorder_path)
259    }
260
261    pub fn recorder_format(&self) -> RecorderFormat {
262        self.recording
263            .as_ref()
264            .map(|policy| policy.recorder_format())
265            .unwrap_or_default()
266    }
267
268    pub fn ensure_recording_defaults(&mut self) -> bool {
269        let mut fallback = false;
270
271        if let Some(policy) = self.recording.as_mut() {
272            fallback |= policy.ensure_defaults();
273        }
274
275        fallback
276    }
277}