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 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}