aboutsummaryrefslogtreecommitdiffstats
path: root/mumlib/src/config.rs
blob: 932e013424751f1eefcaf9800f9bdb7c53797246 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! Representations of the mumdrc configuration file.

use crate::error::ConfigError;
use crate::DEFAULT_PORT;

use log::*;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::fs;
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::{Path, PathBuf};
use toml::value::Array;
use toml::Value;

/// A TOML-friendly version of [Config].
///
/// Values need to be placed before tables due to how TOML works.
#[derive(Debug, Deserialize, Serialize)]
struct TOMLConfig {
    // Values
    accept_all_invalid_certs: Option<bool>,

    // Tables
    audio: Option<AudioConfig>,
    servers: Option<Array>,
}

/// Our representation of the mumdrc config file.
// Deserialized via [TOMLConfig].
#[derive(Clone, Debug, Default)]
pub struct Config {
    /// General audio configuration.
    pub audio: AudioConfig,
    /// Saved servers.
    pub servers: Vec<ServerConfig>,
    /// Whether we allow connecting to servers with invalid server certificates.
    ///
    /// None implies false but we can show a better message to the user.
    pub allow_invalid_server_cert: Option<bool>,
}

impl Config {
    /// Writes this config to the specified path.
    ///
    /// Pass create = true if you want the file to be created if it doesn't already exist.
    ///
    /// # Errors
    ///
    /// - [ConfigError::WontCreateFile] if the file doesn't exist and create = false was passed.
    /// - Any [ConfigError::TOMLErrorSer] encountered when serializing the config.
    /// - Any [ConfigError::IOError] encountered when writing the file.
    pub fn write(&self, path: &Path, create: bool) -> Result<(), ConfigError> {
        // Possible race here. It's fine since it shows when:
        //   1) the file doesn't exist when checked and is then created
        //   2) the file exists when checked but is then removed
        // If 1) we don't do anything anyway so it's fine, and if 2) we
        // immediately re-create the file which, while not perfect, at least
        // should work. Unless the file is removed AND the permissions
        // change, but then we don't have permissions so we can't
        // do anything anyways.

        if !create && !path.exists() {
            return Err(ConfigError::WontCreateFile);
        }

        Ok(fs::write(
            path,
            toml::to_string(&TOMLConfig::from(self.clone()))?,
        )?)
    }
}

/// Overwrite a specific sound effect with a file that should be played instead.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SoundEffect {
    /// During which event the effect should be played.
    pub event: String,
    /// The file that should be played.
    pub file: String,
}

/// General audio configuration.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AudioConfig {
    /// The microphone input sensitivity.
    pub input_volume: Option<f32>,
    /// The output main gain.
    pub output_volume: Option<f32>,
    /// Overriden sound effects.
    pub sound_effects: Option<Vec<SoundEffect>>,
}

/// A saved server.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ServerConfig {
    /// The alias of the server.
    pub name: String,
    /// The host (URL or IP-adress) of the server.
    pub host: String,
    /// The port, if non-default.
    pub port: Option<u16>,
    /// The username to connect with. Prompted on connection if omitted.
    pub username: Option<String>,
    /// The password to connect with. Nothing is sent to the server if omitted.
    pub password: Option<String>,
    /// Whether to accept invalid server certifications for this server.
    pub accept_invalid_cert: Option<bool>,
}

impl ServerConfig {
    /// Creates a [SocketAddr] for this server.
    /// 
    /// Returns `None` if no resolution could be made. See
    /// [std::net::ToSocketAddrs] for more information.
    pub fn to_socket_addr(&self) -> Option<SocketAddr> {
        Some((self.host.as_str(), self.port.unwrap_or(DEFAULT_PORT))
                .to_socket_addrs()
                .ok()?
                .next()?)
    }
}

/// Finds the default path of the configuration file.
/// 
/// The user config dir is looked for first (cross-platform friendly) and
/// `/etc/mumdrc` is used as a fallback.
pub fn default_cfg_path() -> PathBuf {
    match dirs::config_dir() {
        Some(mut p) => {
            p.push("mumdrc");
            p
        }
        //TODO This isn't cross platform.
        None => PathBuf::from("/etc/mumdrc"),
    }
}

impl TryFrom<TOMLConfig> for Config {
    type Error = toml::de::Error;

    fn try_from(config: TOMLConfig) -> Result<Self, Self::Error> {
        Ok(Config {
            audio: config.audio.unwrap_or_default(),
            servers: config
                .servers
                .map(|servers| {
                    servers
                        .into_iter()
                        .map(|s| s.try_into::<ServerConfig>())
                        .collect()
                })
                .transpose()?
                .unwrap_or_default(),
            allow_invalid_server_cert: config.accept_all_invalid_certs,
        })
    }
}

impl From<Config> for TOMLConfig {
    fn from(config: Config) -> Self {
        TOMLConfig {
            audio: if config.audio.output_volume.is_some() || config.audio.input_volume.is_some() {
                Some(config.audio)
            } else {
                None
            },
            servers: Some(
                config
                    .servers
                    .into_iter()
                    // Safe since all ServerConfigs are valid TOML
                    .map(|s| Value::try_from::<ServerConfig>(s).unwrap())
                    .collect(),
            ),
            accept_all_invalid_certs: config.allow_invalid_server_cert,
        }
    }
}

/// Reads the config at the specified path.
///
/// If the file isn't found, returns a default config.
///
/// # Errors
///
/// - Any [ConfigError::TOMLErrorDe] encountered when deserializing the config.
/// - Any [ConfigError::IOError] encountered when reading the file.
pub fn read_cfg(path: &Path) -> Result<Config, ConfigError> {
    match fs::read_to_string(path) {
        Ok(s) => {
            let toml_config: TOMLConfig = toml::from_str(&s)?;
            Ok(Config::try_from(toml_config)?)
        }
        Err(e) => {
            if matches!(e.kind(), std::io::ErrorKind::NotFound) && !path.exists() {
                warn!("Config file not found");
            } else {
                error!("Error reading config file: {}", e);
            }
            return Ok(Config::default());
        }
    }
}