1
2
3
4 """Module for sending push notificiations to Android and iOS devices
5 that have Pushover installed. See https://pushover.net/ for more
6 information.
7
8 copyright: Copyright (c) Jeffrey Goettsch and other contributors.
9 license: BSD, see LICENSE for details.
10
11 """
12
13
14 import json
15 import time
16
17 from pushnotify import abstract
18 from pushnotify import exceptions
19
20
21 PUBLIC_API_URL = u'https://api.pushover.net/1'
22 VERIFY_URL = u'/'.join([PUBLIC_API_URL, u'users/validate.json'])
23 NOTIFY_URL = u'/'.join([PUBLIC_API_URL, u'messages.json'])
24
25 DESC_LIMIT = 512
26
27
28 -class Client(abstract.AbstractClient):
29 """Client for sending push notifications to Android and iOS devices
30 with the Pushover application installed.
31
32 Member Vars:
33 developerkey: A string containing a valid token for the Pushover
34 application.
35 application: A string containing the name of the application on
36 behalf of whom the Pushover client will be sending messages.
37 Not used by this client.
38 apikeys: A dictionary where the keys are strings containing
39 valid user identifier, and the values are lists of strings,
40 each containing a valid device identifier.
41
42 """
43
44 - def __init__(self, developerkey, application=''):
45 """Initialize the Pushover client.
46
47 Args:
48 developerkey: A string containing a valid token for the
49 Pushover application.
50 application: A string containing the name of the application
51 on behalf of whom the Pushover client will be sending
52 messages. Not used by this client. (default: '')
53
54 """
55
56 super(self.__class__, self).__init__(developerkey, application)
57
58 self._type = 'pushover'
59 self._urls = {'notify': NOTIFY_URL, 'verify': VERIFY_URL}
60
62
63 response = stream.read()
64 self.logger.info('received response: {0}'.format(response))
65
66 response = json.loads(response)
67
68 self._last['code'] = stream.code
69 if 'device' in response.keys():
70 self._last['device'] = response['device']
71 else:
72 self._last['device'] = None
73 if 'errors' in response.keys():
74 self._last['errors'] = response['errors']
75 else:
76 self._last['errors'] = None
77 if 'status' in response.keys():
78 self._last['status'] = response['status']
79 else:
80 self._last['status'] = None
81 if 'token' in response.keys():
82 self._last['token'] = response['token']
83 else:
84 self._last['token'] = None
85 if 'user' in response.keys():
86 self._last['user'] = response['user']
87 else:
88 self._last['user'] = None
89
90 return self._last['status']
91
93
94 msg = ''
95 if self._last['errors']:
96 messages = []
97 for key, value in self._last['errors'].items():
98 messages.append('{0} {1}'.format(key, value[0]))
99 msg = '; '.join(messages)
100
101 if self._last['device'] and 'invalid' in self._last['device']:
102 raise exceptions.ApiKeyError('device invalid', self._last['code'])
103
104 elif self._last['token'] and 'invalid' in self._last['token']:
105 raise exceptions.ApiKeyError('token invalid', self._last['code'])
106
107 elif self._last['user'] and 'invalid' in self._last['user']:
108 raise exceptions.ApiKeyError('user invalid', self._last['code'])
109
110 elif self._last['code'] == 429:
111
112
113 msg = 'too many messages sent this month' if not msg else msg
114 raise exceptions.RateLimitExceeded(msg, self._last['code'])
115
116 elif self._last['code'] >= 500 and self._last['code'] <= 599:
117 raise exceptions.ServerError(msg, self._last['code'])
118
119 elif self._last['errors']:
120 raise exceptions.FormatError(msg, self._last['code'])
121
122 else:
123 raise exceptions.UnrecognizedResponseError(msg, self._last['code'])
124
125 - def notify(self, description, event, split=True, kwargs=None):
126 """Send a notification to each user/device combintation in
127 self.apikeys.
128
129 As of 2012-09-18, this is not returning a 4xx status code as
130 per the Pushover API docs, but instead chopping the delivered
131 messages off at 512 characters.
132
133 Args:
134 description: A string of up to DESC_LIMIT characters
135 containing the notification text.
136 event: A string of up to 100 characters containing a
137 subject or brief description of the event.
138 split: A boolean indicating whether to split long
139 descriptions among multiple notifications (True) or to
140 possibly raise an exception (False). (default True)
141 kwargs: A dictionary with any of the following strings as
142 keys:
143 priority: The integer 1, which will make the
144 notification display in red and override any set
145 quiet hours.
146 url: A string of up to 500 characters containing a URL
147 to attach to the notification.
148 url_title: A string of up to 50 characters containing a
149 title to give the attached URL.
150 (default: None)
151
152 Raises:
153 pushnotify.exceptions.ApiKeyError
154 pushnotify.exceptions.FormatError
155 pushnotify.exceptions.RateLimitExceeded
156 pushnotify.exceptions.ServerError
157 pushnotify.exceptions.UnrecognizedResponseError
158
159 """
160
161 def send_notify(desc_list, event, kwargs, apikey, device_key=''):
162 all_successful = True
163
164 for description in desc_list:
165 data = {'token': self.developerkey,
166 'user': apikey,
167 'title': event,
168 'message': description,
169 'timestamp': int(time.time())}
170
171 if device_key:
172 data['device'] = device_key
173
174 if kwargs:
175 data.update(kwargs)
176
177 response_stream = self._post(self._urls['notify'], data)
178 this_successful = self._parse_response_stream(response_stream)
179
180 all_successful = all_successful and this_successful
181
182 return all_successful
183
184 if not self.apikeys:
185 self.logger.warn('notify called with no users set')
186 return
187
188 desc_list = []
189 if split:
190 while description:
191 desc_list.append(description[0:DESC_LIMIT])
192 description = description[DESC_LIMIT:]
193 else:
194 desc_list = [description]
195
196
197
198
199
200 all_ok = True
201
202 for apikey, device_keys in self.apikeys.items():
203 if not device_keys:
204 this_ok = send_notify(desc_list, event, kwargs,
205 apikey)
206 else:
207 for device_key in device_keys:
208 this_ok = send_notify(
209 desc_list, event, kwargs, apikey, device_key)
210
211 all_ok = all_ok and this_ok
212
213 if not all_ok:
214 self._raise_exception()
215
217 """Verify a user identifier.
218
219 Args:
220 apikey: A string containing a user identifer.
221
222 Returns:
223 A boolean containing True if apikey is valid, and
224 False if it is not.
225
226 """
227
228 data = {'token': self.developerkey, 'user': apikey}
229
230 response_stream = self._post(self._urls['verify'], data)
231
232 self._parse_response_stream(response_stream, True)
233
234 return self._last['status']
235
237 """Verify a device identifier for the user given by apikey.
238
239 Args:
240 apikey: A string containing a user identifer.
241 device_key: A string containing a device identifier.
242
243 Raises:
244 pushnotify.exceptions.ApiKeyError
245
246 Returns:
247 A boolean containing True if device_key is valid for
248 apikey, and False if it is not.
249
250 """
251
252 data = {'token': self.developerkey, 'user': apikey,
253 'device': device_key}
254
255 response_stream = self._post(self._urls['verify'], data)
256
257 self._parse_response_stream(response_stream, True)
258
259 if self._last['user'] and 'invalid' in self._last['user'].lower():
260 self._raise_exception()
261
262 return self._last['status']
263
264
265 if __name__ == '__main__':
266 pass
267