lib/
lib.rs

1#![warn(missing_docs)]
2//! Automattermostatus main components and helper functions used by `main`
3use anyhow::{bail, Context, Result};
4use std::fs;
5use std::path::PathBuf;
6use std::thread::sleep;
7use std::{collections::HashMap, time};
8use tracing::{debug, error, info, warn};
9use tracing_subscriber::prelude::*;
10use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter};
11
12pub mod config;
13pub mod mattermost;
14pub mod offtime;
15pub mod state;
16pub mod utils;
17pub mod wifiscan;
18pub use config::{Args, SecretType, WifiStatusConfig};
19pub use mattermost::{BaseSession, MMStatus, Session};
20use offtime::Off;
21pub use state::{Cache, Location, State};
22pub use wifiscan::{WiFi, WifiInterface};
23
24/// Setup logging to stdout
25/// (Tracing is a bit more involving to set up but will provide much more feature if needed)
26pub fn setup_tracing(args: &Args) -> Result<()> {
27    let fmt_layer = fmt::layer().with_target(false);
28    let filter_layer = EnvFilter::try_new(args.verbose.get_level_filter().to_string()).unwrap();
29
30    tracing_subscriber::registry()
31        .with(filter_layer)
32        .with(fmt_layer)
33        .init();
34    Ok(())
35}
36
37/// Return a [`Cache`] used to persist state.
38pub fn get_cache(dir: Option<PathBuf>) -> Result<Cache> {
39    let mut state_file_name: PathBuf;
40    if let Some(ref state_dir) = dir {
41        state_file_name = PathBuf::from(state_dir);
42        fs::create_dir_all(&state_dir)
43            .with_context(|| format!("Creating cache dir {:?}", &state_dir))?;
44    } else {
45        bail!("Internal Error, no `state_dir` configured");
46    }
47
48    state_file_name.push("automattermostatus.state");
49    Ok(Cache::new(state_file_name))
50}
51
52/// Prepare a dictionnary of [`MMStatus`] ready to be send to mattermost
53/// server depending upon the location being found.
54pub fn prepare_status(args: &Args) -> Result<HashMap<Location, MMStatus>> {
55    let mut res = HashMap::new();
56    for s in &args.status {
57        let sc: WifiStatusConfig = s.parse().with_context(|| format!("Parsing {}", s))?;
58        debug!("Adding : {:?}", sc);
59        res.insert(
60            Location::Known(sc.wifi_string),
61            MMStatus::new(sc.text, sc.emoji),
62        );
63    }
64    Ok(res)
65}
66
67/// Create [`Session`] according to `args.secret_type`.
68pub fn create_session(args: &Args) -> Result<Box<dyn BaseSession>> {
69    args.mm_url.as_ref().expect("Mattermost URL is not defined");
70    args.secret_type
71        .as_ref()
72        .expect("Internal Error: secret_type is not defined");
73    args.mm_secret.as_ref().expect("Secret is not defined");
74    let mut session = Session::new(args.mm_url.as_ref().unwrap());
75    let mut session: Box<dyn BaseSession> = match args.secret_type.as_ref().unwrap() {
76        SecretType::Password => Box::new(session.with_credentials(
77            args.mm_user.as_ref().unwrap(),
78            args.mm_secret.as_ref().unwrap(),
79        )),
80        SecretType::Token => Box::new(session.with_token(args.mm_secret.as_ref().unwrap())),
81    };
82    session.login()?;
83    Ok(session)
84}
85
86/// Main application loop, looking for a known SSID and updating
87/// mattermost custom status accordingly.
88pub fn get_wifi_and_update_status_loop(
89    args: Args,
90    mut status_dict: HashMap<Location, MMStatus>,
91) -> Result<()> {
92    let cache = get_cache(args.state_dir.to_owned()).context("Reading cached state")?;
93    let mut state = State::new(&cache).context("Creating cache")?;
94    let delay_duration = time::Duration::new(
95        args.delay
96            .expect("Internal error: args.delay shouldn't be None")
97            .into(),
98        0,
99    );
100    let wifi = WiFi::new(
101        &args
102            .interface_name
103            .clone()
104            .expect("Internal error: args.interface_name shouldn't be None"),
105    );
106    if !wifi
107        .is_wifi_enabled()
108        .context("Checking if wifi is enabled")?
109    {
110        error!("wifi is disabled");
111    } else {
112        info!("Wifi is enabled");
113    }
114    let mut session = create_session(&args)?;
115    loop {
116        if !&args.is_off_time() {
117            let ssids = wifi.visible_ssid().context("Getting visible SSIDs")?;
118            debug!("Visible SSIDs {:#?}", ssids);
119            let mut found_ssid = false;
120            // Search for known wifi in visible ssids
121            for (l, mmstatus) in status_dict.iter_mut() {
122                if let Location::Known(wifi_substring) = l {
123                    if ssids.iter().any(|x| x.contains(wifi_substring)) {
124                        if wifi_substring.is_empty() {
125                            debug!("We do not match against empty SSID reserved for off time");
126                            continue;
127                        }
128                        debug!("known wifi '{}' detected", wifi_substring);
129                        found_ssid = true;
130                        mmstatus.expires_at(&args.expires_at);
131                        if let Err(e) = state.update_status(
132                            l.clone(),
133                            Some(mmstatus),
134                            &mut session,
135                            &cache,
136                            delay_duration.as_secs(),
137                        ) {
138                            error!("Fail to update status : {}", e)
139                        }
140                        break;
141                    }
142                }
143            }
144            if !found_ssid {
145                debug!("Unknown wifi");
146                if let Err(e) = state.update_status(
147                    Location::Unknown,
148                    None,
149                    &mut session,
150                    &cache,
151                    delay_duration.as_secs(),
152                ) {
153                    error!("Fail to update status : {}", e)
154                }
155            }
156        } else {
157            // Send status for Off time (the one with empty wifi_substring).
158            let off_location = Location::Known("".to_string());
159            if let Some(offstatus) = status_dict.get_mut(&off_location) {
160                debug!("Setting state for Offtime");
161                if let Err(e) = state.update_status(
162                    off_location,
163                    Some(offstatus),
164                    &mut session,
165                    &cache,
166                    delay_duration.as_secs(),
167                ) {
168                    error!("Fail to update status : {}", e)
169                }
170            }
171        }
172        if let Some(0) = args.delay {
173            break;
174        } else {
175            sleep(delay_duration);
176        }
177    }
178    Ok(())
179}
180
181#[cfg(test)]
182mod get_cache_should {
183    use super::*;
184    use anyhow::anyhow;
185    use test_log::test; // Automatically trace tests
186
187    #[test]
188    //#[should_panic(expected = "Internal error, no `state_dir` configured")]
189    fn panic_when_called_with_none() -> Result<()> {
190        match get_cache(None) {
191            Ok(_) => Err(anyhow!("Expected an error")),
192            Err(e) => {
193                assert_eq!(e.to_string(), "Internal Error, no `state_dir` configured");
194                Ok(())
195            }
196        }
197    }
198}
199
200#[cfg(test)]
201mod prepare_status_should {
202    use super::*;
203    use test_log::test; // Automatically trace tests
204
205    #[test]
206    fn prepare_expected_status() -> Result<()> {
207        let args = Args {
208            status: vec!["a::b::c", "d::e::f", "::off::off text"]
209                .iter()
210                .map(|s| s.to_string())
211                .collect(),
212            mm_secret: Some("AAA".to_string()),
213            ..Default::default()
214        };
215        let res = prepare_status(&args)?;
216        let mut expected: HashMap<state::Location, mattermost::MMStatus> = HashMap::new();
217        expected.insert(
218            Location::Known("".to_string()),
219            MMStatus::new("off text".to_string(), "off".to_string()),
220        );
221        expected.insert(
222            Location::Known("a".to_string()),
223            MMStatus::new("c".to_string(), "b".to_string()),
224        );
225        expected.insert(
226            Location::Known("d".to_string()),
227            MMStatus::new("f".to_string(), "e".to_string()),
228        );
229        assert_eq!(res, expected);
230        Ok(())
231    }
232}
233
234#[cfg(test)]
235mod create_session_should {
236    use super::*;
237    #[test]
238    #[should_panic(expected = "Mattermost URL is not defined")]
239    fn panic_when_mm_url_is_none() {
240        let args = Args {
241            status: vec!["a::b::c".to_string()],
242            mm_secret: Some("AAA".to_string()),
243            mm_url: None,
244            ..Default::default()
245        };
246        let _res = create_session(&args);
247    }
248}
249
250#[cfg(test)]
251mod main_loop_should {
252    use super::*;
253
254    #[test]
255    #[should_panic(expected = "Internal error: args.delay shouldn't be None")]
256    fn panic_when_args_delay_is_none() {
257        let args = Args {
258            status: vec!["a::b::c".to_string()],
259            delay: None,
260            ..Default::default()
261        };
262        let _res = get_wifi_and_update_status_loop(args, HashMap::new());
263    }
264}