1 /**
2     Implemented in the D Programming Language 2016 by Laeeth Isharc and Kaleidic Associates Advisory UK Limited
3     MIT Licensed
4     Use at your own risk - this is not tested at all.
5 
6     API for pushover notification API
7     https://pushover.net/
8 */
9 module symmetry.api.pushover;
10 
11 
12 import std.datetime: SysTime, DateTime;
13 import std.json: JSONValue;
14 import std.net.curl: HTTP;
15 
16 
17 immutable PushoverMessageSounds = [
18     "pushover",
19     "bike",
20     "bugle",
21     "cashregister",
22     "classical",
23     "cosmic",
24     "falling",
25     "gamelan",
26     "incoming",
27     "intermission",
28     "magic",
29     "mechanical",
30     "pianobar",
31     "siren",
32     "spacealarm",
33     "tugboat",
34     "alien",
35     "climb",
36     "persistent",
37     "echo",
38     "updown",
39     "none"
40 ];
41 
42 
43 string joinUrl(string url, string endpoint)
44 {
45     import std.exception: enforce;
46     enforce(url.length > 0, "broken url");
47     if (url[$-1] == '/')
48         url = url[0..$-1];
49     return url ~ "/" ~ endpoint;
50 }
51 
52 
53 struct PushoverAPI
54 {
55     string endpoint = "https://api.pushover.net/1/";
56     string token;
57     string userKey = null;
58 
59     this(string token)
60     {
61         this.token = token;
62     }
63     this(string token, string userKey)
64     {
65         this.token = token;
66         this.userKey = userKey;
67     }
68 }
69 
70 enum PushoverMessagePriority
71 {
72     lowest = -2,
73     low = -1,
74     normal = 0,
75     high = 1,
76     emergency = 2,
77 }
78 
79 
80 struct PushoverMessage
81 {
82     import std.typecons: Nullable;
83     import std.datetime: SysTime;
84 
85     string messageText = null;
86     string device = null;
87     string title = null;
88     string url = null;
89     string urlTitle = null;
90     Nullable!PushoverMessagePriority priority;
91     Nullable!SysTime timeStamp;
92     string sound = null;
93 
94     this(string messageText)
95     {
96         this.messageText = messageText;
97     }
98 }
99 
100 auto ref setMessage(ref PushoverMessage message, string messageText)
101 {
102     message.messageText = messageText;
103     return message;
104 }
105 
106 auto ref setDevice(ref PushoverMessage message, string device)
107 {
108     message.device = device;
109     return message;
110 }
111 
112 auto ref setTitle(ref PushoverMessage message, string title)
113 {
114     message.title = title;
115     return message;
116 }
117 
118 auto ref setUrl(ref PushoverMessage message, string url)
119 {
120     message.url = url;
121     return message;
122 }
123 
124 auto ref setUrlTitle(ref PushoverMessage message, string urlTitle)
125 {
126     message.urlTitle = urlTitle;
127     return message;
128 }
129 auto ref setPriority(ref PushoverMessage message, PushoverMessagePriority priority)
130 {
131     message.priority = priority;
132     return message;
133 }
134 
135 auto ref setPriority(ref PushoverMessage message, int priority)
136 {
137     import std.conv: to;
138     message.priority = priority.to!PushoverMessagePriority;
139     return message;
140 }
141 
142 auto ref setTimeStamp(ref PushoverMessage message, DateTime timeStamp)
143 {
144     message.timeStamp = cast(SysTime) timeStamp;
145     return message;
146 }
147 
148 auto ref setTimeStamp(ref PushoverMessage message, SysTime timeStamp)
149 {
150     message.timeStamp = timeStamp;
151     return message;
152 }
153 
154 auto ref setSound(ref PushoverMessage message, string sound)
155 {
156     message.sound = sound;
157     return message;
158 }
159 
160 auto sendMessage(PushoverAPI api, PushoverMessage message, string user = null)
161 {
162     JSONValue params;
163     if (user is null)
164         params["user"] = api.userKey;
165     params["message"] = message.messageText;
166     if (message.device !is null)
167         params["device"] = message.device;
168     if (message.title !is null)
169         params["title"] = message.title;
170     if (message.url !is null)
171         params["url"] = message.url;
172     if (message.urlTitle !is null)
173         params["url_title"] = message.urlTitle;
174     if (!message.priority.isNull)
175         params["priority"] = message.priority.get;
176     if (!message.timeStamp.isNull)
177         params["time_stamp"] = message.timeStamp.get.toUnixTime;
178     if (message.sound !is null)
179         params["sound"] = message.sound;
180     return api.request("messages.json", HTTP.Method.post, params);
181 }
182 
183 auto listGroupMembers(PushoverAPI api, string groupKey)
184 {
185     return api.request("groups/" ~ groupKey ~ ".json", HTTP.Method.get);
186 }
187 
188 auto addUserToGroup(PushoverAPI api, string userKey, string groupKey, string device = null, string memo = null)
189 {
190     JSONValue params;
191     params["user"] = userKey;
192     if (device !is null)
193         params["device"] = device;
194     if (memo !is null)
195         params["memo"] = memo;
196     return api.request("groups/" ~ groupKey ~ "/add_user.json", HTTP.Method.post);
197 }
198 
199 auto removeUserFromGroup(PushoverAPI api, string userKey, string groupKey)
200 {
201     JSONValue params;
202     params["user"] = userKey;
203     return api.request("groups/" ~ groupKey ~ "/delete_user.json", HTTP.Method.post);
204 }
205 
206 auto disableUser(PushoverAPI api, string userKey, string groupKey)
207 {
208     JSONValue params;
209     params["user"] = userKey;
210     return api.request("groups/" ~ groupKey ~ "/disable_user.json", HTTP.Method.post);
211 }
212 
213 auto enableUser(PushoverAPI api, string userKey, string groupKey)
214 {
215     JSONValue params;
216     params["user"] = userKey;
217     return api.request("groups/" ~ groupKey ~ "/enable_user.json", HTTP.Method.post);
218 }
219 
220 auto renameGroup(PushoverAPI api, string oldName, string newName)
221 {
222     JSONValue params;
223     params["name"] = newName;
224     return api.request("groups/" ~ oldName ~ "/rename.json", HTTP.Method.post);
225 }
226 auto listSounds(PushoverAPI api)
227 {
228     return api.request("sounds.json", HTTP.Method.get);
229 }
230 
231 auto validate(PushoverAPI api, string user, string device = null)
232 {
233     JSONValue params;
234     params["user"] = user;
235     if(device.length>0)
236         params["device"] = device;
237     return api.request("users/validate.json", HTTP.Method.post, params);
238 }
239 
240 auto checkReceipt(PushoverAPI api, string receipt)
241 {
242     return api.request("receipts/" ~ receipt ~ ".json");
243 }
244 
245 auto cancelEmergencyDelivery(PushoverAPI api, string receipt)
246 {
247     return api.request("receipts/" ~ receipt ~ "/cancel.json");
248 }
249 
250 auto assignLicense(PushoverAPI api, string email = null, string os = null)
251 {
252     JSONValue params;
253     if (email !is null)
254         params["email"] = email;
255     if (os !is null)
256         params["os"] = os;
257     return api.request("licenses/assign.json");
258 }
259 
260 string stripQuotes(string s)
261 {
262     if (s.length < 2)
263         return s;
264     if (s[0] == '"')
265         s = s[1..$];
266     if (s.length < 1)
267         return s;
268     if (s[$-1] == '"')
269         s = s[0..$-1];
270     return s;
271 }
272 
273 JSONValue request(PushoverAPI api,
274                   string url,
275                   HTTP.Method method = HTTP.Method.get,
276                   JSONValue params = JSONValue(null))
277 {
278     import std.array: appender;
279     import std.uri: encodeComponent;
280     import std.conv: to;
281     import std.algorithm: canFind;
282     import std.exception: enforce;
283     import std.stdio: writefln;
284     import std.json: parseJSON;
285 
286     enforce(api.token.length > 0, "no token provided");
287     auto paramsData = appender!string;
288     paramsData.put("token=");
289     paramsData.put(api.token.encodeComponent);
290     paramsData.put("&");
291     if (!params.object.keys.canFind("user"))
292     {
293         paramsData.put("user=");
294         paramsData.put(api.userKey.encodeComponent);
295         paramsData.put("&");
296     }
297     foreach(i, param; params.object.keys)
298     {
299         if (i > 0)
300             paramsData.put("&");
301         paramsData.put(param.to!string.encodeComponent);
302         paramsData.put("=");
303         paramsData.put(params[param].toString.stripQuotes.encodeComponent);
304     }
305 
306     debug
307     {
308         writefln("%s", params.toString);
309         writefln("%s", paramsData.data.to!string);
310     }
311     url = api.endpoint.joinUrl(url);
312     auto client = HTTP(url);
313     auto response = appender!(ubyte[]);
314     client.method = method;
315     client.setPostData(cast(void[])paramsData.data, "application/x-www-form-urlencoded");
316     client.onReceive = (ubyte[] data)
317     {
318         response.put(data);
319         return data.length;
320     };
321     client.perform(); // rely on curl to throw exceptions on 204, >=500
322     return parseJSON(cast(string)response.data);
323 }
324 
325 version(StandAlone)
326 {
327     void main(string[] args)
328     {
329         import std.process : environment;
330         import std.stdio: writefln;
331 
332         auto api = PushoverAPI(environment.get("PUSHOVER_API_TOKEN",""),environment.get("PUSHOVER_API_KEY",""));
333         PushoverMessage message;
334         message = message.setMessage("message text")
335         .setTitle("message title")
336         .setUrl("google.com")
337         .setUrlTitle("google")
338         .setPriority(PushoverMessagePriority.high)
339         .setTimeStamp(DateTime(2013, 1, 1));
340         auto ret = api.sendMessage(message);
341         writefln("%s", ret["status"]);
342         writefln("%s", ret["request"]);
343     }
344 }