1#[cfg(feature = "serde")]
3use serde::{Deserialize, Serialize};
4use std::{
5 collections::HashMap,
6 fs::File,
7 io::prelude::*,
8 path::{Path, PathBuf},
9};
10
11#[cfg(feature = "json_config")]
12use serde_json;
13#[cfg(feature = "yaml_config")]
14use serde_yaml;
15#[cfg(feature = "toml_config")]
16use toml;
17
18#[cfg(feature = "proxy")]
19use crate::client::data::proxy::ProxyType;
20
21use crate::error::Error::InvalidConfig;
22#[cfg(feature = "toml_config")]
23use crate::error::TomlError;
24use crate::error::{ConfigError, Result};
25
26#[derive(Clone, Default, PartialEq, Debug)]
72#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
73pub struct Config {
74 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
76 #[cfg_attr(feature = "serde", serde(default))]
77 pub owners: Vec<String>,
78 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
80 pub nickname: Option<String>,
81 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
83 pub nick_password: Option<String>,
84 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
86 #[cfg_attr(feature = "serde", serde(default))]
87 pub alt_nicks: Vec<String>,
88 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
90 pub username: Option<String>,
91 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
93 pub realname: Option<String>,
94 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
96 pub server: Option<String>,
97 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
99 pub port: Option<u16>,
100 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
102 pub password: Option<String>,
103 #[cfg(feature = "proxy")]
105 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
106 pub proxy_type: Option<ProxyType>,
107 #[cfg(feature = "proxy")]
109 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
110 pub proxy_server: Option<String>,
111 #[cfg(feature = "proxy")]
113 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
114 pub proxy_port: Option<u16>,
115 #[cfg(feature = "proxy")]
117 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
118 pub proxy_username: Option<String>,
119 #[cfg(feature = "proxy")]
121 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
122 pub proxy_password: Option<String>,
123 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
126 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
127 pub use_tls: Option<bool>,
128 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
130 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
131 pub cert_path: Option<String>,
132 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
135 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
136 pub client_cert_path: Option<String>,
137 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
139 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
140 pub client_cert_pass: Option<String>,
141 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
148 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
149 pub dangerously_accept_invalid_certs: Option<bool>,
150 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
153 pub encoding: Option<String>,
154 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
156 #[cfg_attr(feature = "serde", serde(default))]
157 pub channels: Vec<String>,
158 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
160 pub umodes: Option<String>,
161 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
163 pub user_info: Option<String>,
164 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
166 pub version: Option<String>,
167 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
169 pub source: Option<String>,
170 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
172 pub ping_time: Option<u32>,
173 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
175 pub ping_timeout: Option<u32>,
176 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
182 pub burst_window_length: Option<u32>,
183 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
186 pub max_messages_in_burst: Option<u32>,
187 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
190 #[cfg_attr(feature = "serde", serde(default))]
191 pub should_ghost: bool,
192 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
197 #[cfg_attr(feature = "serde", serde(default))]
198 pub ghost_sequence: Option<Vec<String>>,
199 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
202 #[cfg_attr(feature = "serde", serde(default))]
203 pub use_mock_connection: bool,
204 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
207 pub mock_initial_value: Option<String>,
208
209 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))]
211 #[cfg_attr(feature = "serde", serde(default))]
212 pub channel_keys: HashMap<String, String>,
213 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))]
215 #[cfg_attr(feature = "serde", serde(default))]
216 pub options: HashMap<String, String>,
217
218 #[cfg_attr(feature = "serde", serde(skip_serializing))]
222 #[doc(hidden)]
223 pub path: Option<PathBuf>,
224}
225
226#[cfg(feature = "serde")]
227fn is_false(v: &bool) -> bool {
228 !v
229}
230
231impl Config {
232 fn with_path<P: AsRef<Path>>(mut self, path: P) -> Config {
233 self.path = Some(path.as_ref().to_owned());
234 self
235 }
236
237 pub(crate) fn path(&self) -> String {
239 self.path
240 .as_ref()
241 .map(|buf| buf.to_string_lossy().into_owned())
242 .unwrap_or_else(|| "<none>".to_owned())
243 }
244
245 pub fn load<P: AsRef<Path>>(path: P) -> Result<Config> {
249 let mut file = File::open(&path)?;
250 let mut data = String::new();
251 file.read_to_string(&mut data)?;
252
253 let res = match path.as_ref().extension().and_then(|s| s.to_str()) {
254 Some("json") => Config::load_json(&path, &data),
255 Some("toml") => Config::load_toml(&path, &data),
256 Some("yaml") | Some("yml") => Config::load_yaml(&path, &data),
257 Some(ext) => Err(InvalidConfig {
258 path: path.as_ref().to_string_lossy().into_owned(),
259 cause: ConfigError::UnknownConfigFormat {
260 format: ext.to_owned(),
261 },
262 }),
263 None => Err(InvalidConfig {
264 path: path.as_ref().to_string_lossy().into_owned(),
265 cause: ConfigError::MissingExtension,
266 }),
267 };
268
269 res.map(|config| config.with_path(path))
270 }
271
272 #[cfg(feature = "json_config")]
273 fn load_json<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
274 serde_json::from_str(data).map_err(|e| InvalidConfig {
275 path: path.as_ref().to_string_lossy().into_owned(),
276 cause: ConfigError::InvalidJson(e),
277 })
278 }
279
280 #[cfg(not(feature = "json_config"))]
281 fn load_json<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
282 Err(InvalidConfig {
283 path: path.as_ref().to_string_lossy().into_owned(),
284 cause: ConfigError::ConfigFormatDisabled { format: "JSON" },
285 })
286 }
287
288 #[cfg(feature = "toml_config")]
289 fn load_toml<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
290 toml::from_str(data).map_err(|e| InvalidConfig {
291 path: path.as_ref().to_string_lossy().into_owned(),
292 cause: ConfigError::InvalidToml(TomlError::Read(e)),
293 })
294 }
295
296 #[cfg(not(feature = "toml_config"))]
297 fn load_toml<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
298 Err(InvalidConfig {
299 path: path.as_ref().to_string_lossy().into_owned(),
300 cause: ConfigError::ConfigFormatDisabled { format: "TOML" },
301 })
302 }
303
304 #[cfg(feature = "yaml_config")]
305 fn load_yaml<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
306 serde_yaml::from_str(data).map_err(|e| InvalidConfig {
307 path: path.as_ref().to_string_lossy().into_owned(),
308 cause: ConfigError::InvalidYaml(e),
309 })
310 }
311
312 #[cfg(not(feature = "yaml_config"))]
313 fn load_yaml<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
314 Err(InvalidConfig {
315 path: path.as_ref().to_string_lossy().into_owned(),
316 cause: ConfigError::ConfigFormatDisabled { format: "YAML" },
317 })
318 }
319
320 pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
324 let _ = self.path.take();
325 let mut file = File::create(&path)?;
326 let data = match path.as_ref().extension().and_then(|s| s.to_str()) {
327 Some("json") => self.save_json(&path)?,
328 Some("toml") => self.save_toml(&path)?,
329 Some("yaml") | Some("yml") => self.save_yaml(&path)?,
330 Some(ext) => {
331 return Err(InvalidConfig {
332 path: path.as_ref().to_string_lossy().into_owned(),
333 cause: ConfigError::UnknownConfigFormat {
334 format: ext.to_owned(),
335 },
336 })
337 }
338 None => {
339 return Err(InvalidConfig {
340 path: path.as_ref().to_string_lossy().into_owned(),
341 cause: ConfigError::MissingExtension,
342 })
343 }
344 };
345 file.write_all(data.as_bytes())?;
346 self.path = Some(path.as_ref().to_owned());
347 Ok(())
348 }
349
350 #[cfg(feature = "json_config")]
351 fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
352 serde_json::to_string(self).map_err(|e| InvalidConfig {
353 path: path.as_ref().to_string_lossy().into_owned(),
354 cause: ConfigError::InvalidJson(e),
355 })
356 }
357
358 #[cfg(not(feature = "json_config"))]
359 fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
360 Err(InvalidConfig {
361 path: path.as_ref().to_string_lossy().into_owned(),
362 cause: ConfigError::ConfigFormatDisabled { format: "JSON" },
363 })
364 }
365
366 #[cfg(feature = "toml_config")]
367 fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
368 toml::to_string(self).map_err(|e| InvalidConfig {
369 path: path.as_ref().to_string_lossy().into_owned(),
370 cause: ConfigError::InvalidToml(TomlError::Write(e)),
371 })
372 }
373
374 #[cfg(not(feature = "toml_config"))]
375 fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
376 Err(InvalidConfig {
377 path: path.as_ref().to_string_lossy().into_owned(),
378 cause: ConfigError::ConfigFormatDisabled { format: "TOML" },
379 })
380 }
381
382 #[cfg(feature = "yaml_config")]
383 fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
384 serde_yaml::to_string(self).map_err(|e| InvalidConfig {
385 path: path.as_ref().to_string_lossy().into_owned(),
386 cause: ConfigError::InvalidYaml(e),
387 })
388 }
389
390 #[cfg(not(feature = "yaml_config"))]
391 fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
392 Err(InvalidConfig {
393 path: path.as_ref().to_string_lossy().into_owned(),
394 cause: ConfigError::ConfigFormatDisabled { format: "YAML" },
395 })
396 }
397
398 pub fn is_owner(&self, nickname: &str) -> bool {
400 self.owners.iter().any(|n| n == nickname)
401 }
402
403 pub fn nickname(&self) -> Result<&str> {
405 self.nickname.as_deref().ok_or_else(|| InvalidConfig {
406 path: self.path(),
407 cause: ConfigError::NicknameNotSpecified,
408 })
409 }
410
411 pub fn nick_password(&self) -> &str {
414 self.nick_password.as_ref().map_or("", String::as_str)
415 }
416
417 pub fn alternate_nicknames(&self) -> &[String] {
420 &self.alt_nicks
421 }
422
423 pub fn username(&self) -> &str {
426 self.username
427 .as_ref()
428 .map_or(self.nickname().unwrap_or("user"), |s| s)
429 }
430
431 pub fn real_name(&self) -> &str {
434 self.realname
435 .as_ref()
436 .map_or(self.nickname().unwrap_or("irc"), |s| s)
437 }
438
439 pub fn server(&self) -> Result<&str> {
441 self.server.as_deref().ok_or_else(|| InvalidConfig {
442 path: self.path(),
443 cause: ConfigError::ServerNotSpecified,
444 })
445 }
446
447 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
450 pub fn port(&self) -> u16 {
451 self.port.as_ref().cloned().unwrap_or(match self.use_tls() {
452 true => 6697,
453 false => 6667,
454 })
455 }
456
457 #[cfg(not(any(feature = "tls-native", feature = "tls-rust")))]
460 pub fn port(&self) -> u16 {
461 self.port.as_ref().cloned().unwrap_or(6667)
462 }
463
464 pub fn password(&self) -> &str {
467 self.password.as_ref().map_or("", String::as_str)
468 }
469
470 #[cfg(feature = "proxy")]
473 pub fn proxy_type(&self) -> ProxyType {
474 self.proxy_type.as_ref().cloned().unwrap_or(ProxyType::None)
475 }
476
477 #[cfg(feature = "proxy")]
480 pub fn proxy_server(&self) -> &str {
481 self.proxy_server
482 .as_ref()
483 .map_or("localhost", String::as_str)
484 }
485
486 #[cfg(feature = "proxy")]
489 pub fn proxy_port(&self) -> u16 {
490 self.proxy_port.as_ref().cloned().unwrap_or(1080)
491 }
492
493 #[cfg(feature = "proxy")]
496 pub fn proxy_username(&self) -> &str {
497 self.proxy_username.as_ref().map_or("", String::as_str)
498 }
499
500 #[cfg(feature = "proxy")]
503 pub fn proxy_password(&self) -> &str {
504 self.proxy_password.as_ref().map_or("", String::as_str)
505 }
506
507 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
510 pub fn use_tls(&self) -> bool {
511 self.use_tls.as_ref().cloned().map_or(true, |s| s)
512 }
513
514 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
516 pub fn cert_path(&self) -> Option<&str> {
517 self.cert_path.as_deref()
518 }
519
520 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
523 pub fn dangerously_accept_invalid_certs(&self) -> bool {
524 self.dangerously_accept_invalid_certs
525 .as_ref()
526 .cloned()
527 .unwrap_or(false)
528 }
529
530 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
532 pub fn client_cert_path(&self) -> Option<&str> {
533 self.client_cert_path.as_deref()
534 }
535
536 #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
538 pub fn client_cert_pass(&self) -> &str {
539 self.client_cert_pass.as_ref().map_or("", String::as_str)
540 }
541
542 pub fn encoding(&self) -> &str {
545 self.encoding.as_ref().map_or("UTF-8", |s| s)
546 }
547
548 pub fn channels(&self) -> &[String] {
551 &self.channels
552 }
553
554 pub fn channel_key(&self, chan: &str) -> Option<&str> {
556 self.channel_keys.get(chan).map(String::as_str)
557 }
558
559 pub fn umodes(&self) -> &str {
562 self.umodes.as_ref().map_or("", String::as_str)
563 }
564
565 pub fn user_info(&self) -> &str {
568 self.user_info.as_ref().map_or("", String::as_str)
569 }
570
571 pub fn version(&self) -> &str {
575 self.version.as_ref().map_or(crate::VERSION_STR, |s| s)
576 }
577
578 pub fn source(&self) -> &str {
581 self.source
582 .as_ref()
583 .map_or("https://github.com/aatxe/irc", String::as_str)
584 }
585
586 pub fn ping_time(&self) -> u32 {
589 self.ping_time.as_ref().cloned().unwrap_or(180)
590 }
591
592 pub fn ping_timeout(&self) -> u32 {
596 self.ping_timeout.as_ref().cloned().unwrap_or(20)
597 }
598
599 pub fn burst_window_length(&self) -> u32 {
604 self.burst_window_length.as_ref().cloned().unwrap_or(8)
605 }
606
607 pub fn max_messages_in_burst(&self) -> u32 {
613 self.max_messages_in_burst.as_ref().cloned().unwrap_or(15)
614 }
615
616 pub fn should_ghost(&self) -> bool {
619 self.should_ghost
620 }
621
622 pub fn ghost_sequence(&self) -> Option<&[String]> {
625 self.ghost_sequence.as_deref()
626 }
627
628 pub fn get_option(&self, option: &str) -> Option<&str> {
630 self.options.get(option).map(String::as_str)
631 }
632
633 pub fn use_mock_connection(&self) -> bool {
636 self.use_mock_connection
637 }
638
639 pub fn mock_initial_value(&self) -> &str {
643 self.mock_initial_value.as_ref().map_or("", |s| s)
644 }
645}
646
647#[cfg(test)]
648mod test {
649 use super::Config;
650 use std::collections::HashMap;
651
652 #[cfg(any(
653 feature = "json_config",
654 feature = "toml_config",
655 feature = "yaml_config"
656 ))]
657 use super::Result;
658
659 #[allow(unused)]
660 fn test_config() -> Config {
661 Config {
662 owners: vec!["test".to_string()],
663 nickname: Some("test".to_string()),
664 username: Some("test".to_string()),
665 realname: Some("test".to_string()),
666 password: Some(String::new()),
667 umodes: Some("+BR".to_string()),
668 server: Some("irc.test.net".to_string()),
669 port: Some(6667),
670 encoding: Some("UTF-8".to_string()),
671 channels: vec!["#test".to_string(), "#test2".to_string()],
672
673 ..Default::default()
674 }
675 }
676
677 #[test]
678 fn is_owner() {
679 let cfg = Config {
680 owners: vec!["test".to_string(), "test2".to_string()],
681 ..Default::default()
682 };
683 assert!(cfg.is_owner("test"));
684 assert!(cfg.is_owner("test2"));
685 assert!(!cfg.is_owner("test3"));
686 }
687
688 #[test]
689 fn get_option() {
690 let cfg = Config {
691 options: {
692 let mut map = HashMap::new();
693 map.insert("testing".to_string(), "test".to_string());
694 map
695 },
696 ..Default::default()
697 };
698 assert_eq!(cfg.get_option("testing"), Some("test"));
699 assert_eq!(cfg.get_option("not"), None);
700 }
701
702 #[test]
703 #[cfg(feature = "json_config")]
704 fn load_from_json() -> Result<()> {
705 const DATA: &str = include_str!("client_config.json");
706 assert_eq!(
707 Config::load_json("client_config.json", DATA)?.with_path("client_config.json"),
708 test_config().with_path("client_config.json")
709 );
710 Ok(())
711 }
712
713 #[test]
714 #[cfg(feature = "toml_config")]
715 fn load_from_toml() -> Result<()> {
716 const DATA: &str = include_str!("client_config.toml");
717 assert_eq!(
718 Config::load_toml("client_config.toml", DATA)?.with_path("client_config.toml"),
719 test_config().with_path("client_config.toml")
720 );
721 Ok(())
722 }
723
724 #[test]
725 #[cfg(feature = "yaml_config")]
726 fn load_from_yaml() -> Result<()> {
727 const DATA: &str = include_str!("client_config.yaml");
728 assert_eq!(
729 Config::load_yaml("client_config.yaml", DATA)?.with_path("client_config.yaml"),
730 test_config().with_path("client_config.yaml")
731 );
732 Ok(())
733 }
734}