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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
use redacted_debug::RedactedDebug;

use failure::{Error, format_err};
use reqwest;
use serde::{Serialize, Serializer};

static MESSAGE_API_URL: &'static str = "https://api.pushover.net/1/messages.json";

#[cfg(feature = "async")]
type Client = reqwest::Client;
#[cfg(not(feature = "async"))]
type Client = reqwest::blocking::Client;

#[derive(Debug)]
/// The notification with which the notification will be sent.
pub enum Priority {
    /// generate no notification/alert.
    NoNotification,
    /// always send as a quiet notification.
    QuietNotification,
    /// display as high-priority and bypass the user's quiet hours.
    HighPriority,
    /// to also require confirmation from the user.
    RequireConfirmation,
}

impl Serialize for Priority {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let pri = match self {
            Priority::NoNotification => -2,
            Priority::QuietNotification => -1,
            Priority::HighPriority => 1,
            Priority::RequireConfirmation => 2,
        };
        serializer.serialize_i8(pri)
    }
}

#[derive(Serialize, RedactedDebug)]
pub struct Notification<'a> {
    #[redacted]
    token: &'a str,
    user: &'a str,
    message: &'a str,
    // attachment - an image attachment to send with the message; see attachments for more information on how to upload files
    // device - your user's device name to send the message directly to that device, rather than all of the user's devices (multiple devices may be separated by a comma)
    title: Option<String>,
    // TODO(richo) why isn't this an actual url type
    url: Option<String>,
    url_title: Option<String>,
    priority: Option<Priority>,
    // sound - the name of one of the sounds supported by device clients to override the user's default sound choice
    // timestamp - a Unix timestamp of your message's date and time to display to the user, rather than the time your message is received by our API
}

macro_rules! setter {
    ($field:ident, $ty:ty, $doc:expr) => {
        #[doc = $doc]
        pub fn $field(mut self, $field: $ty) -> Notification<'a> {
            self.$field = Some($field);
            self
        }
    }
}

impl<'a> Notification<'a> {
    setter!(title, String, "your message's title, otherwise your app's name is used");
    setter!(url, String, "a supplementary URL to show with your message");
    setter!(url_title, String, "a title for your supplementary URL, otherwise just the URL is shown");
    setter!(priority, Priority, "The notification priority for this message");
}

#[derive(RedactedDebug)]
pub struct PushoverClient {
    #[redacted]
    token: String,
    client: Client,
}

impl PushoverClient {
    pub fn new(token: String) -> PushoverClient {
        #[cfg(feature = "async")]
        let client = reqwest::Client::new();
        #[cfg(not(feature = "async"))]
        let client = reqwest::blocking::Client::new();

        PushoverClient {
            token,
            client,
        }
    }

    pub fn build_notification<'a>(&'a self, user: &'a str, message: &'a str) -> Notification<'a> {
        Notification {
            token: &self.token,
            user: user,
            message: message,
            title: None,
            url: None,
            url_title: None,
            priority: None,
        }
    }

#[cfg(not(feature = "async"))]
    pub fn send<'a>(&'a self, notification: &'a Notification<'_>) -> Result<reqwest::blocking::Response, Error> {
        self.client
            .post(MESSAGE_API_URL)
            .form(&notification)
            .send()
            .map_err(|e| format_err!("HTTP error: {:?}", e))
    }

#[cfg(feature = "async")]
    pub async fn send<'a>(&'a self, notification: &'a Notification<'_>) -> Result<reqwest::Response, Error> {
        self.client
            .post(MESSAGE_API_URL)
            .form(&notification)
            .send()
            .await
            .map_err(|e| format_err!("HTTP error: {:?}", e))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::env;

    use serde_json;
    use tokio;

    #[test]
    fn test_serialized_priorities_dtrt() {
        let client = PushoverClient::new("".into());
        let req = client
            .build_notification("richo", "test")
            .priority(Priority::HighPriority);
        assert!(
            serde_json::to_string(&req)
                .unwrap()
                .contains("\"priority\":1"),
            "Serialization failed"
        );
    }

    #[test]
    fn test_setters_all_work() -> Result<(), Error> {
        let client = PushoverClient::new("".into());
        let notification = client.build_notification("richo", "this is a test_notification");
        let out = notification
            .title("test title".into())
            .url("http://butts.lol".into())
            .url_title("loool".into())
            .priority(Priority::HighPriority);
        // client.send(&out)?;
        Ok(())
    }

    #[tokio::test]
    #[ignore]
    #[cfg(feature = "async")]
    async fn test_sends_notification() -> Result<(), Error> {
        let pushover = PushoverClient::new(
            env::var("ARCHIVER_TEST_PUSHOVER_KEY").expect("Didn't provide test key"),
            );
        let user_key: String = "redacted".into();
        let req = pushover.build_notification(&user_key, "hi there");
        pushover.send(&req).await?;
        Ok(())
    }

    #[test]
    #[ignore]
    #[cfg(not(feature = "async"))]
    fn test_sends_notification() -> Result<(), Error> {
        let pushover = PushoverClient::new(
            env::var("ARCHIVER_TEST_PUSHOVER_KEY").expect("Didn't provide test key"),
            );
        let user_key: String = "redacted".into();
        let req = pushover.build_notification(&user_key, "hi there");
        pushover.send(&req)?;
        Ok(())
    }
}