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
//! Implement persistant state for current location
use anyhow::{Context, Result};
use chrono::Utc;
use std::fs;
use tracing::{debug, info};

use crate::mattermost::MMStatus;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// If more than MAX_SECS_BEFORE_FORCE_UPDATE are elapsed, we forcibly update
/// mattermost custom status to the expected value even if there was no change in visible
/// wifi SSIDs.
const MAX_SECS_BEFORE_FORCE_UPDATE: i64 = 60 * 60;

/// Struct implementing a cache for the application state
#[derive(Debug)]
pub struct Cache {
    path: PathBuf,
}

impl Cache {
    /// Create a cache at location `path`.
    pub fn new(path: impl Into<PathBuf>) -> Self {
        Self { path: path.into() }
    }
}

/// Wifi locations
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
pub enum Location {
    /// Known location based on wifi ssid substring match
    Known(String),
    /// Unknown location
    Unknown,
}

/// State containing at least location info
#[derive(Serialize, Deserialize, Debug)]
pub struct State {
    location: Location,
    timestamp: i64,
}

impl State {
    /// Build a state, either by reading current persisted state in `cache`
    /// or by creating an empty default one.
    pub fn new(cache: &Cache) -> Result<Self> {
        let res: State;
        if let Ok(json) = &fs::read(&cache.path) {
            res = serde_json::from_str(&String::from_utf8_lossy(json)).context(format!(
                "Unable to deserialize state file {:?} (try to remove it)",
                &cache.path
            ))?;
        } else {
            res = Self {
                location: Location::Unknown,
                timestamp: 0,
            };
        }
        debug!("Previous known location `{:?}`", res.location);
        Ok(res)
    }

    /// Update state with location and ensure persisting of state on disk
    pub fn set_location(&mut self, location: Location, cache: &Cache) -> Result<()> {
        info!("Set location to `{:?}`", location);
        self.location = location;
        self.timestamp = Utc::now().timestamp();
        fs::write(
            &cache.path,
            serde_json::to_string(&self)
                .unwrap_or_else(|_| panic!("Serialization of State Failed :{:?}", &self)),
        )
        .with_context(|| format!("Writing to cache file {:?}", cache.path))?;
        Ok(())
    }

    /// Update mattermost status depending upon current state
    ///
    /// If `current_location` is Unknown, then nothing is changed.
    /// If `current_location` is still the same for more than `MAX_SECS_BEFORE_FORCE_UPDATE`
    /// then we force update the mattermost status in order to catch up with desynchronise state
    /// Else we update mattermost status to the one associated to `current_location`.
    pub fn update_status(
        &mut self,
        current_location: Location,
        status: Option<&MMStatus>,
        cache: &Cache,
    ) -> Result<()> {
        if current_location == Location::Unknown {
            return Ok(());
        } else if current_location == self.location {
            // Less than max seconds have elapsed.
            // No need to update MM status again
            if Utc::now().timestamp() - self.timestamp <= MAX_SECS_BEFORE_FORCE_UPDATE {
                debug!(
                    "No change for {}s : no update to mattermost status",
                    MAX_SECS_BEFORE_FORCE_UPDATE
                );
                return Ok(());
            }
        }
        self.set_location(current_location, cache)?;
        // We update the status on MM
        status.unwrap().send()?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    mod should {
        use super::*;
        use mktemp::Temp;
        #[test]
        fn remember_state() -> Result<()> {
            let temp = Temp::new_file().unwrap().to_path_buf();
            let cache = Cache::new(&temp);
            let mut state = State::new(&cache)?;
            assert_eq!(state.location, Location::Unknown);
            state.set_location(Location::Known("abcd".to_string()), &cache)?;
            assert_eq!(state.location, Location::Known("abcd".to_string()));
            let mut state = State::new(&cache)?;
            assert_eq!(state.location, Location::Known("abcd".to_string()));
            state.set_location(Location::Known("work".to_string()), &cache)?;
            assert_eq!(state.location, Location::Known("work".to_string()));
            Ok(())
        }
    }
}