//! JSON configuration files using serde #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, fs::File, io::prelude::*, path::{Path, PathBuf}, }; #[cfg(feature = "json_config")] use serde_json; #[cfg(feature = "yaml_config")] use serde_yaml; #[cfg(feature = "toml_config")] use toml; #[cfg(feature = "proxy")] use crate::client::data::proxy::ProxyType; use crate::error::Error::InvalidConfig; #[cfg(feature = "toml_config")] use crate::error::TomlError; use crate::error::{ConfigError, Result}; /// Configuration for IRC clients. /// /// # Building a configuration programmatically /// /// For some use cases, it may be useful to build configurations programmatically. Since `Config` is /// an ordinary struct with public fields, this should be rather straightforward. However, it is /// important to note that the use of `Config::default()` is important, even when specifying all /// visible fields because `Config` keeps track of whether it was loaded from a file or /// programmatically defined, in order to produce better error messages. Using `Config::default()` /// as below will ensure that this process is handled correctly. /// /// ``` /// # extern crate irc; /// use irc::client::prelude::Config; /// /// # fn main() { /// let config = Config { /// nickname: Some("test".to_owned()), /// server: Some("irc.example.com".to_owned()), /// ..Config::default() /// }; /// # } /// ``` /// /// # Loading a configuration from a file /// /// The standard method of using a configuration is to load it from a TOML file. You can find an /// example TOML configuration in the README, as well as a minimal example with code for loading the /// configuration below. /// /// ## TOML (`config.toml`) /// ```toml /// nickname = "test" /// server = "irc.example.com" /// ``` /// /// ## Rust /// ```no_run /// # extern crate irc; /// use irc::client::prelude::Config; /// /// # fn main() { /// let config = Config::load("config.toml").unwrap(); /// # } /// ``` #[derive(Clone, Default, PartialEq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Config { /// A list of the owners of the client by nickname (for bots). #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))] #[cfg_attr(feature = "serde", serde(default))] pub owners: Vec, /// The client's nickname. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub nickname: Option, /// The client's NICKSERV password. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub nick_password: Option, /// Alternative nicknames for the client, if the default is taken. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))] #[cfg_attr(feature = "serde", serde(default))] pub alt_nicks: Vec, /// The client's username. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub username: Option, /// The client's real name. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub realname: Option, /// The server to connect to. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub server: Option, /// The port to connect on. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub port: Option, /// The password to connect to the server. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub password: Option, /// The proxy type to connect to. #[cfg(feature = "proxy")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub proxy_type: Option, /// The proxy server to connect to. #[cfg(feature = "proxy")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub proxy_server: Option, /// The proxy port to connect on. #[cfg(feature = "proxy")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub proxy_port: Option, /// The username to connect to the proxy server. #[cfg(feature = "proxy")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub proxy_username: Option, /// The password to connect to the proxy server. #[cfg(feature = "proxy")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub proxy_password: Option, /// Whether or not to use TLS. /// Clients will automatically panic if this is enabled without TLS support. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub use_tls: Option, /// The path to the TLS certificate for this server in DER format. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub cert_path: Option, /// The path to a TLS certificate to use for CertFP client authentication in DER format. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub client_cert_path: Option, /// The password for the certificate to use in CertFP authentication. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub client_cert_pass: Option, /// The encoding type used for this connection. /// This is typically UTF-8, but could be something else. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub encoding: Option, /// A list of channels to join on connection. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))] #[cfg_attr(feature = "serde", serde(default))] pub channels: Vec, /// User modes to set on connect. Example: "+RB -x" #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub umodes: Option, /// The text that'll be sent in response to CTCP USERINFO requests. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub user_info: Option, /// The text that'll be sent in response to CTCP VERSION requests. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub version: Option, /// The text that'll be sent in response to CTCP SOURCE requests. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub source: Option, /// The amount of inactivity in seconds before the client will ping the server. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub ping_time: Option, /// The amount of time in seconds for a client to reconnect due to no ping response. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub ping_timeout: Option, /// The length in seconds of a rolling window for message throttling. If more than /// `max_messages_in_burst` messages are sent within `burst_window_length` seconds, additional /// messages will be delayed automatically as appropriate. In particular, in the past /// `burst_window_length` seconds, there will never be more than `max_messages_in_burst` messages /// sent. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub burst_window_length: Option, /// The maximum number of messages that can be sent in a burst window before they'll be delayed. /// Messages are automatically delayed as appropriate. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub max_messages_in_burst: Option, /// Whether the client should use NickServ GHOST to reclaim its primary nickname if it is in /// use. This has no effect if `nick_password` is not set. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] #[cfg_attr(feature = "serde", serde(default))] pub should_ghost: bool, /// The command(s) that should be sent to NickServ to recover a nickname. The nickname and /// password will be appended in that order after the command. /// E.g. `["RECOVER", "RELEASE"]` means `RECOVER nick pass` and `RELEASE nick pass` will be sent /// in that order. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(default))] pub ghost_sequence: Option>, /// Whether or not to use a fake connection for testing purposes. You probably will never want /// to enable this, but it is used in unit testing for the `irc` crate. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] #[cfg_attr(feature = "serde", serde(default))] pub use_mock_connection: bool, /// The initial value used by the fake connection for testing. You probably will never need to /// set this, but it is used in unit testing for the `irc` crate. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub mock_initial_value: Option, /// A mapping of channel names to keys for join-on-connect. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))] #[cfg_attr(feature = "serde", serde(default))] pub channel_keys: HashMap, /// A map of additional options to be stored in config. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))] #[cfg_attr(feature = "serde", serde(default))] pub options: HashMap, /// The path that this configuration was loaded from. /// /// This should not be specified in any configuration. It will automatically be handled by the library. #[cfg_attr(feature = "serde", serde(skip_serializing))] #[doc(hidden)] pub path: Option, } #[cfg(feature = "serde")] fn is_false(v: &bool) -> bool { !v } impl Config { fn with_path>(mut self, path: P) -> Config { self.path = Some(path.as_ref().to_owned()); self } /// Returns the location this Config was loaded from or ``. pub(crate) fn path(&self) -> String { self.path .as_ref() .map(|buf| buf.to_string_lossy().into_owned()) .unwrap_or_else(|| "".to_owned()) } /// Loads a configuration from the desired path. This will use the file extension to detect /// which format to parse the file as (json, toml, or yaml). Using each format requires having /// its respective crate feature enabled. Only json is available by default. pub fn load>(path: P) -> Result { let mut file = File::open(&path)?; let mut data = String::new(); file.read_to_string(&mut data)?; let res = match path.as_ref().extension().and_then(|s| s.to_str()) { Some("json") => Config::load_json(&path, &data), Some("toml") => Config::load_toml(&path, &data), Some("yaml") | Some("yml") => Config::load_yaml(&path, &data), Some(ext) => Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::UnknownConfigFormat { format: ext.to_owned(), }, }), None => Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::MissingExtension, }), }; res.map(|config| config.with_path(path)) } #[cfg(feature = "json_config")] fn load_json>(path: P, data: &str) -> Result { serde_json::from_str(data).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidJson(e), }) } #[cfg(not(feature = "json_config"))] fn load_json>(path: P, _: &str) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "JSON" }, }) } #[cfg(feature = "toml_config")] fn load_toml>(path: P, data: &str) -> Result { toml::from_str(data).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidToml(TomlError::Read(e)), }) } #[cfg(not(feature = "toml_config"))] fn load_toml>(path: P, _: &str) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "TOML" }, }) } #[cfg(feature = "yaml_config")] fn load_yaml>(path: P, data: &str) -> Result { serde_yaml::from_str(data).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidYaml(e), }) } #[cfg(not(feature = "yaml_config"))] fn load_yaml>(path: P, _: &str) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "YAML" }, }) } /// Saves a configuration to the desired path. This will use the file extension to detect /// which format to parse the file as (json, toml, or yaml). Using each format requires having /// its respective crate feature enabled. Only json is available by default. pub fn save>(&mut self, path: P) -> Result<()> { let _ = self.path.take(); let mut file = File::create(&path)?; let data = match path.as_ref().extension().and_then(|s| s.to_str()) { Some("json") => self.save_json(&path)?, Some("toml") => self.save_toml(&path)?, Some("yaml") | Some("yml") => self.save_yaml(&path)?, Some(ext) => { return Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::UnknownConfigFormat { format: ext.to_owned(), }, }) } None => { return Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::MissingExtension, }) } }; file.write_all(data.as_bytes())?; self.path = Some(path.as_ref().to_owned()); Ok(()) } #[cfg(feature = "json_config")] fn save_json>(&self, path: &P) -> Result { serde_json::to_string(self).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidJson(e), }) } #[cfg(not(feature = "json_config"))] fn save_json>(&self, path: &P) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "JSON" }, }) } #[cfg(feature = "toml_config")] fn save_toml>(&self, path: &P) -> Result { toml::to_string(self).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidToml(TomlError::Write(e)), }) } #[cfg(not(feature = "toml_config"))] fn save_toml>(&self, path: &P) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "TOML" }, }) } #[cfg(feature = "yaml_config")] fn save_yaml>(&self, path: &P) -> Result { serde_yaml::to_string(self).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidYaml(e), }) } #[cfg(not(feature = "yaml_config"))] fn save_yaml>(&self, path: &P) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "YAML" }, }) } /// Determines whether or not the nickname provided is the owner of the bot. pub fn is_owner(&self, nickname: &str) -> bool { self.owners.iter().find(|n| *n == nickname).is_some() } /// Gets the nickname specified in the configuration. pub fn nickname(&self) -> Result<&str> { self.nickname .as_ref() .map(String::as_str) .ok_or_else(|| InvalidConfig { path: self.path(), cause: ConfigError::NicknameNotSpecified, }) } /// Gets the bot's nickserv password specified in the configuration. /// This defaults to an empty string when not specified. pub fn nick_password(&self) -> &str { self.nick_password.as_ref().map_or("", String::as_str) } /// Gets the alternate nicknames specified in the configuration. /// This defaults to an empty vector when not specified. pub fn alternate_nicknames(&self) -> &[String] { &self.alt_nicks } /// Gets the username specified in the configuration. /// This defaults to the user's nickname when not specified. pub fn username(&self) -> &str { self.username .as_ref() .map_or(self.nickname().unwrap_or("user"), |s| &s) } /// Gets the real name specified in the configuration. /// This defaults to the user's nickname when not specified. pub fn real_name(&self) -> &str { self.realname .as_ref() .map_or(self.nickname().unwrap_or("irc"), |s| &s) } /// Gets the address of the server specified in the configuration. pub fn server(&self) -> Result<&str> { self.server .as_ref() .map(String::as_str) .ok_or_else(|| InvalidConfig { path: self.path(), cause: ConfigError::ServerNotSpecified, }) } /// Gets the port of the server specified in the configuration. /// This defaults to 6697 (or 6667 if use_tls is specified as false) when not specified. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn port(&self) -> u16 { self.port.as_ref().cloned().unwrap_or(match self.use_tls() { true => 6697, false => 6667, }) } /// Gets the port of the server specified in the configuration. /// This defaults to 6667 when not specified. #[cfg(not(any(feature = "tls-native", feature = "tls-rust")))] pub fn port(&self) -> u16 { self.port.as_ref().cloned().unwrap_or(6667) } /// Gets the server password specified in the configuration. /// This defaults to an empty string when not specified. pub fn password(&self) -> &str { self.password.as_ref().map_or("", String::as_str) } /// Gets the type of the proxy specified in the configuration. /// This defaults to a None ProxyType when not specified. #[cfg(feature = "proxy")] pub fn proxy_type(&self) -> ProxyType { self.proxy_type.as_ref().cloned().unwrap_or(ProxyType::None) } /// Gets the address of the proxy specified in the configuration. /// This defaults to "localhost" string when not specified. #[cfg(feature = "proxy")] pub fn proxy_server(&self) -> &str { self.proxy_server .as_ref() .map_or("localhost", String::as_str) } /// Gets the port of the proxy specified in the configuration. /// This defaults to 1080 when not specified. #[cfg(feature = "proxy")] pub fn proxy_port(&self) -> u16 { self.proxy_port.as_ref().cloned().unwrap_or(1080) } /// Gets the username of the proxy specified in the configuration. /// This defaults to an empty string when not specified. #[cfg(feature = "proxy")] pub fn proxy_username(&self) -> &str { self.proxy_username.as_ref().map_or("", String::as_str) } /// Gets the password of the proxy specified in the configuration. /// This defaults to an empty string when not specified. #[cfg(feature = "proxy")] pub fn proxy_password(&self) -> &str { self.proxy_password.as_ref().map_or("", String::as_str) } /// Gets whether or not to use TLS with this connection. /// This defaults to true when not specified. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn use_tls(&self) -> bool { self.use_tls.as_ref().cloned().map_or(true, |s| s) } /// Gets the path to the TLS certificate in DER format if specified. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn cert_path(&self) -> Option<&str> { self.cert_path.as_ref().map(String::as_str) } /// Gets the path to the client authentication certificate in DER format if specified. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn client_cert_path(&self) -> Option<&str> { self.client_cert_path.as_ref().map(String::as_str) } /// Gets the password to the client authentication certificate. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn client_cert_pass(&self) -> &str { self.client_cert_pass.as_ref().map_or("", String::as_str) } /// Gets the encoding to use for this connection. This requires the encode feature to work. /// This defaults to UTF-8 when not specified. pub fn encoding(&self) -> &str { self.encoding.as_ref().map_or("UTF-8", |s| &s) } /// Gets the channels to join upon connection. /// This defaults to an empty vector if it's not specified. pub fn channels(&self) -> &[String] { &self.channels } /// Gets the key for the specified channel if it exists in the configuration. pub fn channel_key(&self, chan: &str) -> Option<&str> { self.channel_keys.get(chan).map(String::as_str) } /// Gets the user modes to set on connect specified in the configuration. /// This defaults to an empty string when not specified. pub fn umodes(&self) -> &str { self.umodes.as_ref().map_or("", String::as_str) } /// Gets the string to be sent in response to CTCP USERINFO requests. /// This defaults to an empty string when not specified. pub fn user_info(&self) -> &str { self.user_info.as_ref().map_or("", String::as_str) } /// Gets the string to be sent in response to CTCP VERSION requests. /// This defaults to `irc:version:env` when not specified. /// For example, `irc:0.12.0:Compiled with rustc` pub fn version(&self) -> &str { self.version.as_ref().map_or(crate::VERSION_STR, |s| &s) } /// Gets the string to be sent in response to CTCP SOURCE requests. /// This defaults to `https://github.com/aatxe/irc` when not specified. pub fn source(&self) -> &str { self.source .as_ref() .map_or("https://github.com/aatxe/irc", String::as_str) } /// Gets the amount of time in seconds for the interval at which the client pings the server. /// This defaults to 180 seconds when not specified. pub fn ping_time(&self) -> u32 { self.ping_time.as_ref().cloned().unwrap_or(180) } /// Gets the amount of time in seconds for the client to disconnect after not receiving a ping /// response. /// This defaults to 10 seconds when not specified. pub fn ping_timeout(&self) -> u32 { self.ping_timeout.as_ref().cloned().unwrap_or(20) } /// The amount of time in seconds to consider a window for burst messages. The message throttling /// system maintains the invariant that in the past `burst_window_length` seconds, the maximum /// number of messages sent is `max_messages_in_burst`. /// This defaults to 8 seconds when not specified. pub fn burst_window_length(&self) -> u32 { self.burst_window_length.as_ref().cloned().unwrap_or(8) } /// The maximum number of messages that can be sent in a burst window before they'll be delayed. /// Messages are automatically delayed until the start of the next window. The message throttling /// system maintains the invariant that in the past `burst_window_length` seconds, the maximum /// number of messages sent is `max_messages_in_burst`. /// This defaults to 15 messages when not specified. pub fn max_messages_in_burst(&self) -> u32 { self.max_messages_in_burst.as_ref().cloned().unwrap_or(15) } /// Gets whether or not to attempt nickname reclamation using NickServ GHOST. /// This defaults to false when not specified. pub fn should_ghost(&self) -> bool { self.should_ghost } /// Gets the NickServ command sequence to recover a nickname. /// This defaults to `["GHOST"]` when not specified. pub fn ghost_sequence(&self) -> Option<&[String]> { self.ghost_sequence.as_ref().map(Vec::as_slice) } /// Looks up the specified string in the options map. pub fn get_option(&self, option: &str) -> Option<&str> { self.options.get(option).map(String::as_str) } /// Gets whether or not to use a mock connection for testing. /// This defaults to false when not specified. pub fn use_mock_connection(&self) -> bool { self.use_mock_connection } /// Gets the initial value for the mock connection. /// This defaults to false when not specified. /// This has no effect if `use_mock_connection` is not `true`. pub fn mock_initial_value(&self) -> &str { self.mock_initial_value.as_ref().map_or("", |s| &s) } } #[cfg(test)] mod test { use super::Config; use std::collections::HashMap; #[cfg(any( feature = "json_config", feature = "toml_config", feature = "yaml_config" ))] use super::Result; #[allow(unused)] fn test_config() -> Config { Config { owners: vec![format!("test")], nickname: Some(format!("test")), username: Some(format!("test")), realname: Some(format!("test")), password: Some(String::new()), umodes: Some(format!("+BR")), server: Some(format!("irc.test.net")), port: Some(6667), encoding: Some(format!("UTF-8")), channels: vec![format!("#test"), format!("#test2")], ..Default::default() } } #[test] fn is_owner() { let cfg = Config { owners: vec![format!("test"), format!("test2")], ..Default::default() }; assert!(cfg.is_owner("test")); assert!(cfg.is_owner("test2")); assert!(!cfg.is_owner("test3")); } #[test] fn get_option() { let cfg = Config { options: { let mut map = HashMap::new(); map.insert(format!("testing"), format!("test")); map }, ..Default::default() }; assert_eq!(cfg.get_option("testing"), Some("test")); assert_eq!(cfg.get_option("not"), None); } #[test] #[cfg(feature = "json_config")] fn load_from_json() -> Result<()> { const DATA: &str = include_str!("client_config.json"); assert_eq!( Config::load_json("client_config.json", DATA)?.with_path("client_config.json"), test_config().with_path("client_config.json") ); Ok(()) } #[test] #[cfg(feature = "toml_config")] fn load_from_toml() -> Result<()> { const DATA: &str = include_str!("client_config.toml"); assert_eq!( Config::load_toml("client_config.toml", DATA)?.with_path("client_config.toml"), test_config().with_path("client_config.toml") ); Ok(()) } #[test] #[cfg(feature = "yaml_config")] fn load_from_yaml() -> Result<()> { const DATA: &str = include_str!("client_config.yaml"); assert_eq!( Config::load_yaml("client_config.yaml", DATA)?.with_path("client_config.yaml"), test_config().with_path("client_config.yaml") ); Ok(()) } }