aboutsummaryrefslogtreecommitdiffstats
path: root/mumlib/src/config.rs
blob: 3edef37583e659dfd3068cdddf6f8a931286c080 (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
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>,
}

#[derive(Clone, Debug, Default)]
pub struct Config {
    pub audio: AudioConfig,
    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 {
    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()))?,
        )?)
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SoundEffect {
    pub event: String,
    pub file: String,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AudioConfig {
    pub input_volume: Option<f32>,
    pub output_volume: Option<f32>,
    pub sound_effects: Option<Vec<SoundEffect>>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ServerConfig {
    pub name: String,
    pub host: String,
    pub port: Option<u16>,
    pub username: Option<String>,
    pub password: Option<String>,
    pub accept_invalid_cert: Option<bool>,
}

impl ServerConfig {
    pub fn to_socket_addr(&self) -> Option<SocketAddr> {
        match (self.host.as_str(), self.port.unwrap_or(DEFAULT_PORT))
            .to_socket_addrs()
            .map(|mut e| e.next())
        {
            Ok(Some(addr)) => Some(addr),
            _ => None,
        }
    }
}

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

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());
        }
    }
}