irc/client/data/
config.rs

1//! JSON configuration files using serde
2#[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/// Configuration for IRC clients.
27///
28/// # Building a configuration programmatically
29///
30/// For some use cases, it may be useful to build configurations programmatically. Since `Config` is
31/// an ordinary struct with public fields, this should be rather straightforward. However, it is
32/// important to note that the use of `Config::default()` is important, even when specifying all
33/// visible fields because `Config` keeps track of whether it was loaded from a file or
34/// programmatically defined, in order to produce better error messages. Using `Config::default()`
35/// as below will ensure that this process is handled correctly.
36///
37/// ```
38/// # extern crate irc;
39/// use irc::client::prelude::Config;
40///
41/// # fn main() {
42/// let config = Config {
43///     nickname: Some("test".to_owned()),
44///     server: Some("irc.example.com".to_owned()),
45///     ..Config::default()
46/// };
47/// # }
48/// ```
49///
50/// # Loading a configuration from a file
51///
52/// The standard method of using a configuration is to load it from a TOML file. You can find an
53/// example TOML configuration in the README, as well as a minimal example with code for loading the
54/// configuration below.
55///
56/// ## TOML (`config.toml`)
57/// ```toml
58/// nickname = "test"
59/// server = "irc.example.com"
60/// ```
61///
62/// ## Rust
63/// ```no_run
64/// # extern crate irc;
65/// use irc::client::prelude::Config;
66///
67/// # fn main() {
68/// let config = Config::load("config.toml").unwrap();
69/// # }
70/// ```
71#[derive(Clone, Default, PartialEq, Debug)]
72#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
73pub struct Config {
74    /// A list of the owners of the client by nickname (for bots).
75    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
76    #[cfg_attr(feature = "serde", serde(default))]
77    pub owners: Vec<String>,
78    /// The client's nickname.
79    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
80    pub nickname: Option<String>,
81    /// The client's NICKSERV password.
82    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
83    pub nick_password: Option<String>,
84    /// Alternative nicknames for the client, if the default is taken.
85    #[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    /// The client's username.
89    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
90    pub username: Option<String>,
91    /// The client's real name.
92    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
93    pub realname: Option<String>,
94    /// The server to connect to.
95    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
96    pub server: Option<String>,
97    /// The port to connect on.
98    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
99    pub port: Option<u16>,
100    /// The password to connect to the server.
101    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
102    pub password: Option<String>,
103    /// The proxy type to connect to.
104    #[cfg(feature = "proxy")]
105    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
106    pub proxy_type: Option<ProxyType>,
107    /// The proxy server to connect to.
108    #[cfg(feature = "proxy")]
109    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
110    pub proxy_server: Option<String>,
111    /// The proxy port to connect on.
112    #[cfg(feature = "proxy")]
113    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
114    pub proxy_port: Option<u16>,
115    /// The username to connect to the proxy server.
116    #[cfg(feature = "proxy")]
117    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
118    pub proxy_username: Option<String>,
119    /// The password to connect to the proxy server.
120    #[cfg(feature = "proxy")]
121    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
122    pub proxy_password: Option<String>,
123    /// Whether or not to use TLS.
124    /// Clients will automatically panic if this is enabled without TLS support.
125    #[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    /// The path to the TLS certificate for this server in DER format.
129    #[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    /// The path to a TLS certificate to use for CertFP client authentication in a DER-formatted
133    /// PKCS #12 archive.
134    #[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    /// The password for the certificate to use in CertFP authentication.
138    #[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    /// On `true`, all certificate validations are skipped. Defaults to `false`.
142    ///
143    /// # Warning
144    /// You should think very carefully before using this method. If invalid hostnames are trusted, *any* valid
145    /// certificate for *any* site will be trusted for use. This introduces significant vulnerabilities, and should
146    /// only be used as a last resort.
147    #[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    /// The encoding type used for this connection.
151    /// This is typically UTF-8, but could be something else.
152    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
153    pub encoding: Option<String>,
154    /// A list of channels to join on connection.
155    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
156    #[cfg_attr(feature = "serde", serde(default))]
157    pub channels: Vec<String>,
158    /// User modes to set on connect. Example: "+RB -x"
159    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
160    pub umodes: Option<String>,
161    /// The text that'll be sent in response to CTCP USERINFO requests.
162    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
163    pub user_info: Option<String>,
164    /// The text that'll be sent in response to CTCP VERSION requests.
165    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
166    pub version: Option<String>,
167    /// The text that'll be sent in response to CTCP SOURCE requests.
168    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
169    pub source: Option<String>,
170    /// The amount of inactivity in seconds before the client will ping the server.
171    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
172    pub ping_time: Option<u32>,
173    /// The amount of time in seconds for a client to reconnect due to no ping response.
174    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
175    pub ping_timeout: Option<u32>,
176    /// The length in seconds of a rolling window for message throttling. If more than
177    /// `max_messages_in_burst` messages are sent within `burst_window_length` seconds, additional
178    /// messages will be delayed automatically as appropriate. In particular, in the past
179    /// `burst_window_length` seconds, there will never be more than `max_messages_in_burst` messages
180    /// sent.
181    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
182    pub burst_window_length: Option<u32>,
183    /// The maximum number of messages that can be sent in a burst window before they'll be delayed.
184    /// Messages are automatically delayed as appropriate.
185    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
186    pub max_messages_in_burst: Option<u32>,
187    /// Whether the client should use NickServ GHOST to reclaim its primary nickname if it is in
188    /// use. This has no effect if `nick_password` is not set.
189    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
190    #[cfg_attr(feature = "serde", serde(default))]
191    pub should_ghost: bool,
192    /// The command(s) that should be sent to NickServ to recover a nickname. The nickname and
193    /// password will be appended in that order after the command.
194    /// E.g. `["RECOVER", "RELEASE"]` means `RECOVER nick pass` and `RELEASE nick pass` will be sent
195    /// in that order.
196    #[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    /// Whether or not to use a fake connection for testing purposes. You probably will never want
200    /// to enable this, but it is used in unit testing for the `irc` crate.
201    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
202    #[cfg_attr(feature = "serde", serde(default))]
203    pub use_mock_connection: bool,
204    /// The initial value used by the fake connection for testing. You probably will never need to
205    /// set this, but it is used in unit testing for the `irc` crate.
206    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
207    pub mock_initial_value: Option<String>,
208
209    /// A mapping of channel names to keys for join-on-connect.
210    #[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    /// A map of additional options to be stored in config.
214    #[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    /// The path that this configuration was loaded from.
219    ///
220    /// This should not be specified in any configuration. It will automatically be handled by the library.
221    #[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    /// Returns the location this Config was loaded from or `<none>`.
238    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    /// Loads a configuration from the desired path. This will use the file extension to detect
246    /// which format to parse the file as (json, toml, or yaml). Using each format requires having
247    /// its respective crate feature enabled. Only toml is available by default.
248    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    /// Saves a configuration to the desired path. This will use the file extension to detect
321    /// which format to parse the file as (json, toml, or yaml). Using each format requires having
322    /// its respective crate feature enabled. Only toml is available by default.
323    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    /// Determines whether or not the nickname provided is the owner of the bot.
399    pub fn is_owner(&self, nickname: &str) -> bool {
400        self.owners.iter().any(|n| n == nickname)
401    }
402
403    /// Gets the nickname specified in the configuration.
404    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    /// Gets the bot's nickserv password specified in the configuration.
412    /// This defaults to an empty string when not specified.
413    pub fn nick_password(&self) -> &str {
414        self.nick_password.as_ref().map_or("", String::as_str)
415    }
416
417    /// Gets the alternate nicknames specified in the configuration.
418    /// This defaults to an empty vector when not specified.
419    pub fn alternate_nicknames(&self) -> &[String] {
420        &self.alt_nicks
421    }
422
423    /// Gets the username specified in the configuration.
424    /// This defaults to the user's nickname when not specified.
425    pub fn username(&self) -> &str {
426        self.username
427            .as_ref()
428            .map_or(self.nickname().unwrap_or("user"), |s| s)
429    }
430
431    /// Gets the real name specified in the configuration.
432    /// This defaults to the user's nickname when not specified.
433    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    /// Gets the address of the server specified in the configuration.
440    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    /// Gets the port of the server specified in the configuration.
448    /// This defaults to 6697 (or 6667 if use_tls is specified as false) when not specified.
449    #[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    /// Gets the port of the server specified in the configuration.
458    /// This defaults to 6667 when not specified.
459    #[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    /// Gets the server password specified in the configuration.
465    /// This defaults to an empty string when not specified.
466    pub fn password(&self) -> &str {
467        self.password.as_ref().map_or("", String::as_str)
468    }
469
470    /// Gets the type of the proxy specified in the configuration.
471    /// This defaults to a None ProxyType when not specified.
472    #[cfg(feature = "proxy")]
473    pub fn proxy_type(&self) -> ProxyType {
474        self.proxy_type.as_ref().cloned().unwrap_or(ProxyType::None)
475    }
476
477    /// Gets the address of the proxy specified in the configuration.
478    /// This defaults to "localhost" string when not specified.
479    #[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    /// Gets the port of the proxy specified in the configuration.
487    /// This defaults to 1080 when not specified.
488    #[cfg(feature = "proxy")]
489    pub fn proxy_port(&self) -> u16 {
490        self.proxy_port.as_ref().cloned().unwrap_or(1080)
491    }
492
493    /// Gets the username of the proxy specified in the configuration.
494    /// This defaults to an empty string when not specified.
495    #[cfg(feature = "proxy")]
496    pub fn proxy_username(&self) -> &str {
497        self.proxy_username.as_ref().map_or("", String::as_str)
498    }
499
500    /// Gets the password of the proxy specified in the configuration.
501    /// This defaults to an empty string when not specified.
502    #[cfg(feature = "proxy")]
503    pub fn proxy_password(&self) -> &str {
504        self.proxy_password.as_ref().map_or("", String::as_str)
505    }
506
507    /// Gets whether or not to use TLS with this connection.
508    /// This defaults to true when not specified.
509    #[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    /// Gets the path to the TLS certificate in DER format if specified.
515    #[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    /// Gets whether or not to dangerously accept invalid certificates.
521    /// This defaults to `false` when not specified.
522    #[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    /// Gets the path to the client authentication certificate in DER format if specified.
531    #[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    /// Gets the password to the client authentication certificate.
537    #[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    /// Gets the encoding to use for this connection. This requires the encode feature to work.
543    /// This defaults to UTF-8 when not specified.
544    pub fn encoding(&self) -> &str {
545        self.encoding.as_ref().map_or("UTF-8", |s| s)
546    }
547
548    /// Gets the channels to join upon connection.
549    /// This defaults to an empty vector if it's not specified.
550    pub fn channels(&self) -> &[String] {
551        &self.channels
552    }
553
554    /// Gets the key for the specified channel if it exists in the configuration.
555    pub fn channel_key(&self, chan: &str) -> Option<&str> {
556        self.channel_keys.get(chan).map(String::as_str)
557    }
558
559    /// Gets the user modes to set on connect specified in the configuration.
560    /// This defaults to an empty string when not specified.
561    pub fn umodes(&self) -> &str {
562        self.umodes.as_ref().map_or("", String::as_str)
563    }
564
565    /// Gets the string to be sent in response to CTCP USERINFO requests.
566    /// This defaults to an empty string when not specified.
567    pub fn user_info(&self) -> &str {
568        self.user_info.as_ref().map_or("", String::as_str)
569    }
570
571    /// Gets the string to be sent in response to CTCP VERSION requests.
572    /// This defaults to `irc:version:env` when not specified.
573    /// For example, `irc:0.12.0:Compiled with rustc`
574    pub fn version(&self) -> &str {
575        self.version.as_ref().map_or(crate::VERSION_STR, |s| s)
576    }
577
578    /// Gets the string to be sent in response to CTCP SOURCE requests.
579    /// This defaults to `https://github.com/aatxe/irc` when not specified.
580    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    /// Gets the amount of time in seconds for the interval at which the client pings the server.
587    /// This defaults to 180 seconds when not specified.
588    pub fn ping_time(&self) -> u32 {
589        self.ping_time.as_ref().cloned().unwrap_or(180)
590    }
591
592    /// Gets the amount of time in seconds for the client to disconnect after not receiving a ping
593    /// response.
594    /// This defaults to 20 seconds when not specified.
595    pub fn ping_timeout(&self) -> u32 {
596        self.ping_timeout.as_ref().cloned().unwrap_or(20)
597    }
598
599    /// The amount of time in seconds to consider a window for burst messages. The message throttling
600    /// system maintains the invariant that in the past `burst_window_length` seconds, the maximum
601    /// number of messages sent is `max_messages_in_burst`.
602    /// This defaults to 8 seconds when not specified.
603    pub fn burst_window_length(&self) -> u32 {
604        self.burst_window_length.as_ref().cloned().unwrap_or(8)
605    }
606
607    /// The maximum number of messages that can be sent in a burst window before they'll be delayed.
608    /// Messages are automatically delayed until the start of the next window. The message throttling
609    /// system maintains the invariant that in the past `burst_window_length` seconds, the maximum
610    /// number of messages sent is `max_messages_in_burst`.
611    /// This defaults to 15 messages when not specified.
612    pub fn max_messages_in_burst(&self) -> u32 {
613        self.max_messages_in_burst.as_ref().cloned().unwrap_or(15)
614    }
615
616    /// Gets whether or not to attempt nickname reclamation using NickServ GHOST.
617    /// This defaults to false when not specified.
618    pub fn should_ghost(&self) -> bool {
619        self.should_ghost
620    }
621
622    /// Gets the NickServ command sequence to recover a nickname.
623    /// This defaults to `["GHOST"]` when not specified.
624    pub fn ghost_sequence(&self) -> Option<&[String]> {
625        self.ghost_sequence.as_deref()
626    }
627
628    /// Looks up the specified string in the options map.
629    pub fn get_option(&self, option: &str) -> Option<&str> {
630        self.options.get(option).map(String::as_str)
631    }
632
633    /// Gets whether or not to use a mock connection for testing.
634    /// This defaults to false when not specified.
635    pub fn use_mock_connection(&self) -> bool {
636        self.use_mock_connection
637    }
638
639    /// Gets the initial value for the mock connection.
640    /// This defaults to false when not specified.
641    /// This has no effect if `use_mock_connection` is not `true`.
642    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}