lib/
state.rs

1//! Implement persistant state for current location
2//!
3//! The [`State`] also provide the [`State::update_status`] function used to propagate the custom status
4//! state to the mattermost instance
5use anyhow::{Context, Result};
6use chrono::Utc;
7use std::fs;
8use tracing::{debug, info};
9
10use crate::mattermost::{BaseSession, MMStatus};
11use serde::{Deserialize, Serialize};
12use std::path::PathBuf;
13
14/// If more than MAX_SECS_BEFORE_FORCE_UPDATE are elapsed, we forcibly update
15/// mattermost custom status to the expected value even if there was no change in visible
16/// wifi SSIDs.
17const MAX_SECS_BEFORE_FORCE_UPDATE: u64 = 60 * 60;
18
19/// Struct implementing a cache for the application state
20#[derive(Debug)]
21pub struct Cache {
22    path: PathBuf,
23}
24
25impl Cache {
26    /// Create a cache at location `path`.
27    pub fn new(path: impl Into<PathBuf>) -> Self {
28        Self { path: path.into() }
29    }
30}
31
32/// Wifi locations
33#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
34pub enum Location {
35    /// Known location based on wifi ssid substring match
36    Known(String),
37    /// Unknown location
38    Unknown,
39}
40
41/// State containing at least location info
42#[derive(Serialize, Deserialize, Debug)]
43pub struct State {
44    location: Location,
45    timestamp: i64,
46}
47
48impl State {
49    /// Build a state, either by reading current persisted state in `cache`
50    /// or by creating an empty default one.
51    pub fn new(cache: &Cache) -> Result<Self> {
52        let res: State;
53        if let Ok(json) = &fs::read(&cache.path) {
54            res = serde_json::from_str(&String::from_utf8_lossy(json)).context(format!(
55                "Unable to deserialize state file {:?} (try to remove it)",
56                &cache.path
57            ))?;
58        } else {
59            res = Self {
60                location: Location::Unknown,
61                timestamp: 0,
62            };
63        }
64        debug!("Previous known location `{:?}`", res.location);
65        Ok(res)
66    }
67
68    /// Update state with location and ensure persisting of state on disk
69    pub fn set_location(&mut self, location: Location, cache: &Cache) -> Result<()> {
70        info!("Set location to `{:?}`", location);
71        self.location = location;
72        self.timestamp = Utc::now().timestamp();
73        fs::write(
74            &cache.path,
75            serde_json::to_string(&self)
76                .unwrap_or_else(|_| panic!("Serialization of State Failed :{:?}", &self)),
77        )
78        .with_context(|| format!("Writing to cache file {:?}", cache.path))?;
79        Ok(())
80    }
81
82    /// Update mattermost status depending upon current state
83    ///
84    /// If `current_location` is Unknown, then nothing is changed.
85    /// If `current_location` is still the same for more than `MAX_SECS_BEFORE_FORCE_UPDATE`
86    /// then we force update the mattermost status in order to catch up with desynchronise state
87    /// Else we update mattermost status to the one associated to `current_location`.
88    pub fn update_status(
89        &mut self,
90        current_location: Location,
91        status: Option<&mut MMStatus>,
92        session: &mut Box<dyn BaseSession>,
93        cache: &Cache,
94        delay_between_polling: u64,
95    ) -> Result<()> {
96        if current_location == Location::Unknown {
97            return Ok(());
98        } else if current_location == self.location {
99            // Less than max seconds have elapsed.
100            // No need to update MM status again
101            let elapsed_sec: u64 = (Utc::now().timestamp() - self.timestamp)
102                .try_into()
103                .unwrap();
104            if delay_between_polling * 2 < elapsed_sec
105                && elapsed_sec <= MAX_SECS_BEFORE_FORCE_UPDATE
106            {
107                debug!(
108                    "No change for {}s : no update to mattermost status",
109                    MAX_SECS_BEFORE_FORCE_UPDATE
110                );
111                return Ok(());
112            }
113        }
114        // We update the status on MM
115        status.unwrap().send(session)?;
116        // We update the location (only if setting mattermost status succeed)
117        self.set_location(current_location, cache)?;
118        Ok(())
119    }
120}
121
122#[cfg(test)]
123mod should {
124    use super::*;
125    use mktemp::Temp;
126    use test_log::test; // Automatically trace tests
127    #[test]
128    fn remember_state() -> Result<()> {
129        let temp = Temp::new_file().unwrap().to_path_buf();
130        let cache = Cache::new(&temp);
131        let mut state = State::new(&cache)?;
132        assert_eq!(state.location, Location::Unknown);
133        state.set_location(Location::Known("abcd".to_string()), &cache)?;
134        assert_eq!(state.location, Location::Known("abcd".to_string()));
135        let mut state = State::new(&cache)?;
136        assert_eq!(state.location, Location::Known("abcd".to_string()));
137        state.set_location(Location::Known("work".to_string()), &cache)?;
138        assert_eq!(state.location, Location::Known("work".to_string()));
139        Ok(())
140    }
141}