1 /**
2     Pushover Dlang API by Laeeth Isharc and Kaleidic Associates Advisory Limited
3     2016, 2017
4 
5     Beta bindings for https://pushover.net/api notification API
6 
7     Generated documentation http://pushover.code.kaleidic.io
8 
9     "Pushover uses a simple, versioned REST API to receive messages from your application and send them to devices running device clients.
10     To simplify the user registration process and usage of our API, there are no complicated out-of-band authentication mechanisms or
11     per-call signing libraries required, such as OAuth. HTTP libraries available in just about every language, or even from the command line,
12     can be used without any custom modules or extra dependencies needed. See our FAQ for examples in different programming languages."
13 
14     Boost-licensed.  Use at your peril.
15 */
16 /**
17     Example:
18     ---
19         import kaleidic.api.pushover;
20         import std.datetime:Clock;
21 
22         enum applicationKey = "set me".ApplicationToken;
23         enum targetUserKey = "set me".UserKey;
24         enum groupKey = "set me".GroupKey;
25         enum targetUserMemo ="memo field here";
26         
27         auto api=PushoverAPI(applicationToken);
28         writefln("validate target user: %s",api.validate(targetUserKey));
29         writeln("result of adding target user to group:",
30             api.addUserToGroup(
31                         targetUserKey,
32                         groupKey,
33                         null.DeviceName,
34                         targetUserMemo)
35         );
36         PushoverMessage message;
37 
38         message=message.setMessage("as the CNBC anchor said, is buying GS here like D&G on sale?")
39             .setTitle("Kaleidic Market Alert - GS")
40             .setUrl("kaleidic.io")
41             .setUrlTitle("GS chart")
42             .setPriority(PushoverMessagePriority.high)
43             .setTimeStamp(Clock.currTime());
44         writefln("%s",message);
45         auto ret=api.sendMessage(message,targetUserKey);
46         writefln("message status: %s",ret["status"]);
47         writefln("message request: %s",ret["request"]);
48     ---
49 */
50 
51 module kaleidic.api.pushover;
52 import std.stdio;
53 import std.json;
54 import std.net.curl;
55 import std.exception:Exception,enforce,assumeUnique;
56 import std.conv:to;
57 import std.algorithm:countUntil,map,each;
58 import std.traits:EnumMembers;
59 import std.array:array,appender;
60 import std.format:format;
61 import std.variant:Algebraic;
62 import std.typecons:Nullable;
63 import std.datetime:SysTime,DateTime;
64 
65 ///
66 static this()
67 {
68     PushoverMessageSounds=[ "pushover",
69                             "bike",
70                             "bugle",
71                             "cashregister",
72                             "classical",
73                             "cosmic",
74                             "falling",
75                             "gamelan",
76                             "incoming",
77                             "intermission",
78                             "magic",
79                             "mechanical",
80                             "pianobar",
81                             "siren",
82                             "spacealarm",
83                             "tugboat",
84                             "alien",
85                             "climb",
86                             "persistent",
87                             "echo",
88                             "updown",
89                             "none"  ];
90 
91 }
92 
93 ///
94 string joinUrl(string url, string endpoint)
95 {
96     enforce(url.length>0, "broken url");
97     if (url[$-1]=='/')
98         url=url[0..$-1];
99     return url~"/"~endpoint;
100 }
101 
102 ///
103 struct ApplicationKey
104 {
105     string key;
106     alias key this;
107 }
108 
109 ///
110 struct UserKey
111 {
112     string key;
113     alias key this;
114 }
115 
116 ///
117 struct GroupKey
118 {
119     string key;
120     alias key this;
121 }
122 
123 ///
124 struct APIToken
125 {
126     string token;
127     alias token this;
128 }
129 
130 ///
131 struct DeviceName
132 {
133     string name;
134     alias name this;
135 }
136 
137 ///
138 struct PushoverAPI
139 {
140     string endpoint = "https://api.pushover.net/1/";
141     APIToken token;
142     UserKey userKey=null.UserKey;
143 
144     this(APIToken token)
145     {
146         this.token=token;
147     }
148     this(APIToken token, UserKey userKey)
149     {
150         this.token=token;
151         this.userKey=userKey;
152     }
153 }
154 
155 ///
156 enum PushoverMessagePriority
157 {
158     lowest=-2,
159     low=-1,
160     normal=0,
161     high=1,
162     emergency=2, 
163 }
164 
165 ///
166 string[] PushoverMessageSounds;
167 
168 ///
169 struct PushoverMessage
170 {
171     string messageText=null;
172     DeviceName device=null.DeviceName;
173     string title=null;
174     string url=null;
175     string urlTitle=null;
176     Nullable!PushoverMessagePriority priority;
177     Nullable!SysTime timeStamp;
178     string sound=null;
179 
180     this(string messageText)
181     {
182         this.messageText=messageText;
183     }
184 }
185 
186 ///
187 auto ref setMessage(ref PushoverMessage message, string messageText)
188 {
189     message.messageText=messageText;
190     return message;
191 }
192 
193 ///
194 auto ref setDevice(ref PushoverMessage message, DeviceName device)
195 {
196     message.device=device.name;
197     return message;
198 }
199 
200 ///
201 auto ref setTitle(ref PushoverMessage message, string title)
202 {
203     message.title=title;
204     return message;
205 }
206 
207 ///
208 auto ref setUrl(ref PushoverMessage message, string url)
209 {
210     message.url=url;
211     return message;
212 }
213 
214 ///
215 auto ref setUrlTitle(ref PushoverMessage message, string urlTitle)
216 {
217     message.urlTitle=urlTitle;
218     return message;
219 }
220 
221 ///
222 auto ref setPriority(ref PushoverMessage message, PushoverMessagePriority priority)
223 {
224     message.priority=priority;
225     return message;
226 }
227 
228 ///
229 auto ref setPriority(ref PushoverMessage message, int priority)
230 {
231     message.priority=priority.to!PushoverMessagePriority;
232     return message;
233 }
234 
235 ///
236 auto ref setTimeStamp(ref PushoverMessage message, DateTime timeStamp)
237 {
238     message.timeStamp=cast(SysTime) timeStamp;
239     return message;
240 }
241 
242 ///
243 auto ref setTimeStamp(ref PushoverMessage message, SysTime timeStamp)
244 {
245     message.timeStamp=timeStamp;
246     return message;
247 }
248 
249 ///
250 auto ref setSound(ref PushoverMessage message, string sound)
251 {
252     message.sound=sound;
253     return message;
254 }
255 
256 ///
257 auto sendMessage(PushoverAPI api, PushoverMessage message, UserKey user=null.UserKey)
258 {
259     JSONValue params = ["message": message.messageText];
260     if (user.key.length==0)
261     {
262         enforce(api.userKey.key.length>0,"PushOverAPI.sendMessage - you must specify either a user in the sendMessage call or in the API constructor");
263         params["user"]=api.userKey.key;
264     }
265     else
266     {
267         params["user"] = user.key;
268     }
269     if (message.device.name !is null)
270         params["device"]=message.device.name;
271     if (message.title !is null)
272         params["title"] = message.title;
273     if (message.url !is null)
274         params["url"] = message.url;
275     if (message.urlTitle !is null)
276         params["url_title"] = message.urlTitle;
277     if (!message.priority.isNull)
278         params["priority"] = message.priority;
279     if (!message.timeStamp.isNull)
280         params["time_stamp"] = message.timeStamp.toUnixTime;
281     if (message.sound !is null)
282         params["sound"] = message.sound;
283     return api.request("messages.json", HTTP.Method.post,params);
284 }
285 
286 ///
287 auto listGroupMembers(PushoverAPI api, GroupKey groupKey)
288 {
289     return api.request("groups/"~groupKey~".json",HTTP.Method.get);
290 }
291 
292 
293 ///
294 auto addUserToGroup(PushoverAPI api, UserKey userKey, GroupKey groupKey, DeviceName device=null.DeviceName, string memo=null)
295 {
296     import std.uri:encodeComponent;
297     JSONValue params;
298     params["user"] = userKey.key;
299     if (device.name !is null)
300         params["device"] = device.name;
301     if (memo !is null)
302         params["memo"] = memo;
303     return api.request("groups/"~groupKey.key.encodeComponent~"/add_user.json",HTTP.Method.post,params);
304 }
305 
306 ///
307 auto removeUserFromGroup(PushoverAPI api, UserKey userKey, GroupKey groupKey)
308 {
309     import std.uri:encodeComponent;
310     JSONValue params;
311     params["user"]=userKey;
312     return api.request("groups/"~groupKey.key.encodeComponent~"/delete_user.json",HTTP.Method.post,params);
313 }
314 
315 ///
316 auto disableUser(PushoverAPI api, UserKey userKey, GroupKey groupKey)
317 {
318     import std.uri:encodeComponent;
319     JSONValue params;
320     params["user"]=userKey;
321     return api.request("groups/"~groupKey.key.encodeComponent~"/disable_user.json",HTTP.Method.post,params);
322 }
323 
324 ///
325 auto enableUser(PushoverAPI api, UserKey userKey, GroupKey groupKey)
326 {
327     import std.uri:encodeComponent;
328     JSONValue params =[ "user": userKey.key ];
329     return api.request("groups/"~groupKey.key.encodeComponent~"/enable_user.json",HTTP.Method.post,params);
330 }
331 
332 ///
333 auto renameGroup(PushoverAPI api, string oldName, string newName)
334 {
335     import std.uri:encodeComponent;
336     JSONValue params;
337     params["name"]=newName;
338     return api.request("groups/"~oldName.encodeComponent~"/rename.json",HTTP.Method.post,params);
339 }
340 
341 ///
342 auto listSounds(PushoverAPI api)
343 {
344     return api.request("sounds.json",HTTP.Method.get);
345 }
346 
347 ///
348 auto validate(PushoverAPI api, UserKey user, DeviceName device=null.DeviceName)
349 {
350     JSONValue params;
351     params["user"]=user.key;
352     if(device.length>0)
353         params["device"]=device.name;
354     return api.request("users/validate.json",HTTP.Method.post,params);
355 }
356 
357 ///
358 auto checkReceipt(PushoverAPI api, string receipt)
359 {
360     import std.uri:encodeComponent;
361     return api.request("receipts/"~receipt.encodeComponent~".json");
362 }
363 
364 ///
365 auto cancelEmergencyDelivery(PushoverAPI api, string receipt)
366 {
367     import std.uri:encodeComponent;
368     return api.request("receipts/"~receipt.encodeComponent~"/cancel.json");
369 }
370 
371 ///
372 auto assignLicense(PushoverAPI api, string email=null, string os=null)
373 {
374     JSONValue params;
375     if (email !is null)
376         params["email"]=email;
377     if (os !is null)
378         params["os"]=os;
379     return api.request("licenses/assign.json");
380 }
381 
382 ///
383 string stripQuotes(string s)
384 {
385     if (s.length<2)
386         return s;
387     if (s[0]=='"')
388         s=s[1..$];
389     if (s.length<1)
390         return s;
391     if (s[$-1]=='"')
392         s=s[0..$-1];
393     return s;
394 }
395 
396 ///
397 string fixUrl(string s) // sorry - not sure why urls are being escaped - kludge for now
398 {
399     import std.string:replace;
400     return s.replace("\\/","/");
401 }
402 
403 ///
404 auto request(PushoverAPI api, string url, HTTP.Method method=HTTP.Method.get, JSONValue params=JSONValue(null))
405 {
406     import std.array:appender;
407     import std.uri:encodeComponent;
408     import std.conv:to;
409     import std.algorithm:canFind;
410     enforce(api.token.length>0,"no token provided");
411     auto paramsData=appender!string;
412     paramsData.put("token=");
413     paramsData.put(api.token.encodeComponent);
414     paramsData.put("&");
415     if (params.type != JSON_TYPE.OBJECT) 
416     {
417         params=["user": api.userKey.key];
418     }
419     else if (!params.object.keys.canFind("user"))
420     {
421         params["user"]=api.userKey.key;
422     }
423 
424 
425     if (params.type == JSON_TYPE.OBJECT)
426     {
427         foreach(i,param;params.object.keys)
428         {
429             if (i>0)
430                 paramsData.put("&");
431             paramsData.put(param.to!string.encodeComponent);
432             paramsData.put("=");
433             paramsData.put(param.to!string=="url"?params[param].toString.stripQuotes.fixUrl:params[param].toString.stripQuotes.encodeComponent);
434         }
435     }
436     debug
437     {
438         writefln("request: %s",paramsData.data);
439     }
440     url=api.endpoint.joinUrl(url);
441     auto client=HTTP(url);
442     auto response=appender!(ubyte[]);
443     client.method=method;
444     client.setPostData(cast(void[])paramsData.data,"application/x-www-form-urlencoded");
445     client.onReceive = (ubyte[] data)
446     {
447         response.put(data);
448         return data.length;
449     };
450     client.perform();                 // rely on curl to throw exceptions on 204, >=500
451     debug writeln(cast(string)response.data);
452     return parseJSON(cast(string)response.data);
453 }
454 
455 
456     
457 ///
458 unittest
459 {
460     import std.datetime:Clock;
461 
462     enum applicationKey = "set me".ApplicationToken;
463     enum targetUserKey = "set me".UserKey;
464     enum groupKey = "set me".GroupKey;
465     enum targetUserMemo ="memo field here";
466     
467     auto api=PushoverAPI(applicationToken);
468     writefln("validate target user: %s",api.validate(targetUserKey));
469     writeln("result of adding target user to group:",api.addUserToGroup(targetUserKey,groupKey,null.DeviceName,targetUserMemo));
470     PushoverMessage message;
471 
472     message=message.setMessage("as the CNBC anchor said, is buying GS here like D&G on sale?")
473         .setTitle("Kaleidic Market Alert - GS")
474         .setUrl("kaleidic.io")
475         .setUrlTitle("GS chart")
476         .setPriority(PushoverMessagePriority.high)
477         .setTimeStamp(Clock.currTime());
478     writefln("%s",message);
479     auto ret=api.sendMessage(message,targetUserKey);
480     writefln("message status: %s",ret["status"]);
481     writefln("message request: %s",ret["request"]);
482 }
483