~goorzhel/sota-slack-spotter

ref: b52cec66b2f63aa4cd638dff3c0aeaccf0de241b sota-slack-spotter/src/sota/spot.rs -rw-r--r-- 3.1 KiB
b52cec66 — Antonio Gurgel Use serde_with to resolve ""/None discrepancy 4 months ago
                                                                                
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
//! Statements of activations in progress.

use std::{
    fmt::{Debug, Display},
    hash::{Hash, Hasher},
    time::Duration,
};

use anyhow::{anyhow, Result};
use reqwest::blocking::get;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use time::{format_description::parse as parse_format, OffsetDateTime};

use crate::{
    callsign::Callsign,
    sota::{Mode, SOTAItem},
};

/// Fetch spots from SOTA API.
pub fn all_spots(hours: u8) -> Result<Vec<Spot>> {
    let url = format!("https://api2.sota.org.uk/api/spots/-{}/all", hours);
    let response = get(&url)?;
    if !response.status().is_success() {
        return Err(anyhow!("Got {} from API", response.status()));
    }
    Ok(response.json()?)
}

/// A SOTA spot.
#[derive(Debug, Clone, Deserialize, Serialize, Eq)]
#[serde_as]
#[serde(rename_all = "camelCase")]
pub struct Spot {
    pub activator_callsign: Callsign,
    pub association_code: String,
    pub summit_code: String,
    pub summit_details: String,
    /// In megahertz
    pub frequency: String, // No need for f32's features yet.
    pub mode: Mode,
    #[serde(with = "time::serde::rfc3339")]
    pub time_stamp: OffsetDateTime,
    pub callsign: Callsign,
    #[serde_as(as = "NoneAsEmptyString")]
    pub comments: Option<String>,
}

impl SOTAItem for Spot {
    fn callsign(&self) -> &Callsign {
        &self.activator_callsign
    }

    fn is_too_old(&self) -> bool {
        let an_hour_ago = OffsetDateTime::now_utc() - Duration::from_secs(4200);
        // 1h10m gives some overlap for the spot to fall out of the one-hour query.
        self.time_stamp < an_hour_ago
    }

    fn format_timestamp(&self) -> String {
        let time = self
            .time_stamp
            .time()
            .format(&parse_format("[hour]:[minute]:[second]").unwrap())
            .unwrap();
        format!("{}Z", time)
    }
}

impl Display for Spot {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
        write!(
            f,
            "on {} MHz ({}) at {} by {}",
            self.frequency,
            self.mode,
            self.format_timestamp(),
            self.callsign
        )?;
        if self.comments.is_some() {
            write!(f, " (\"{}\")", self.comments.as_ref().unwrap())?;
        }
        Ok(())
    }
}

// Only essential fields need be hashed/compared, to prevent duplicates
// from things like edited comments.
impl Hash for Spot {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.activator_callsign.hash(state);
        self.association_code.hash(state);
        self.summit_code.hash(state);
        self.frequency.hash(state);
        self.mode.hash(state);
        self.time_stamp.hash(state);
    }
}

impl PartialEq for Spot {
    fn eq(&self, other: &Self) -> bool {
        self.activator_callsign == other.activator_callsign
            && self.association_code == other.association_code
            && self.summit_code == other.summit_code
            && self.frequency == other.frequency
            && self.mode == other.mode
            && self.time_stamp == other.time_stamp
    }
}