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 logging
16 import time
17 import urllib
18 import urllib2
19
20 from pushnotify import exceptions
21
22
23 PUBLIC_API_URL = u'https://api.pushover.net/1'
24 VERIFY_URL = u'/'.join([PUBLIC_API_URL, u'users/validate.json'])
25 NOTIFY_URL = u'/'.join([PUBLIC_API_URL, u'messages.json'])
26
27
29 """Client for sending push notifications to Android and iOS devices
30 with the Pushover application installed.
31
32 Member Vars:
33 token: A string containing a valid API token.
34
35 """
36
38 """Initialize the Pushover client.
39
40 Args:
41 token: A string of 30 characters containing a valid API
42 token.
43 users: A list containing 1 or 2-item tuples, where the first
44 item is a string of 30 characters containing a user
45 token, and the second is an optional string of up to 25
46 characters containing a device name for the given user.
47 (default: None)
48
49 """
50
51 self.logger = logging.getLogger('{0}.{1}'.format(
52 self.__module__, self.__class__.__name__))
53
54 self._browser = urllib2.build_opener(urllib2.HTTPSHandler())
55 self._last_code = None
56 self._last_device = None
57 self._last_errors = None
58 self._last_status = None
59 self._last_token = None
60 self._last_user = None
61
62 self.token = token
63 self.users = [] if users is None else users
64
66
67 response = stream.read()
68 self.logger.info('received response: {0}'.format(response))
69
70 response = json.loads(response)
71
72 self._last_code = stream.code
73 if 'device' in response.keys():
74 self._last_device = response['device']
75 else:
76 self._last_device = None
77 if 'errors' in response.keys():
78 self._last_errors = response['errors']
79 else:
80 self._last_errors = None
81 if 'status' in response.keys():
82 self._last_status = response['status']
83 else:
84 self._last_status = None
85 if 'token' in response.keys():
86 self._last_token = response['token']
87 if 'user' in response.keys():
88 self._last_user = response['user']
89 else:
90 self._last_user = None
91
92 return self._last_status
93
94 - def _post(self, url, data):
95
96 self.logger.debug('_post sending data: {0}'.format(data))
97 self.logger.debug('_post sending to url: {0}'.format(url))
98
99 request = urllib2.Request(url, data)
100 try:
101 response_stream = self._browser.open(request)
102 except urllib2.HTTPError, exc:
103 return exc
104 else:
105 return response_stream
106
108
109 msg = ''
110 if self._last_errors:
111 messages = []
112 for key, value in self._last_errors.items():
113 messages.append('{0} {1}'.format(key, value[0]))
114 msg = '; '.join(messages)
115
116 if self._last_device and 'invalid' in self._last_device:
117 raise exceptions.ApiKeyError('device invalid', self._last_code)
118
119 elif self._last_token and 'invalid' in self._last_token:
120 raise exceptions.ApiKeyError('token invalid', self._last_code)
121
122 elif self._last_user and 'invalid' in self._last_user:
123 raise exceptions.ApiKeyError('user invalid', self._last_code)
124
125 elif self._last_code == 429:
126
127
128 msg = 'too many messages sent this month' if not msg else msg
129 raise exceptions.RateLimitExceeded(msg, self._last_code)
130
131 elif self._last_code >= 500 and self._last_code <= 599:
132 raise exceptions.ServerError(msg, self._last_code)
133
134 elif self._last_errors:
135 raise exceptions.FormatError(msg, self._last_code)
136
137 else:
138 raise exceptions.UnrecognizedResponseError(msg, self._last_code)
139
140 - def notify(self, title, message, kwargs=None):
141 """Send a notification to each user/device in self.users.
142
143 As of 2012-09-18, this is not returning a 4xx status code as
144 per the Pushover API docs, but instead chopping the delivered
145 messages off at 512 characters.
146
147 Args:
148 title: A string of up to 100 characters containing the
149 title of the message (i.e. subject or brief description)
150 message: A string of up to 512 characters containing the
151 notification text.
152 kwargs: A dictionary with any of the following strings as
153 keys:
154 priority: The integer 1, which will make the
155 notification display in red and override any set
156 quiet hours.
157 url: A string of up to 500 characters containing a URL
158 to attach to the notification.
159 url_title: A string of up to 50 characters containing a
160 title to give the attached URL.
161 (default: None)
162
163 Raises:
164 pushnotify.exceptions.ApiKeyError
165 pushnotify.exceptions.FormatError
166 pushnotify.exceptions.RateLimitExceeded
167 pushnotify.exceptions.ServerError
168 pushnotify.exceptions.UnrecognizedResponseError
169
170 """
171
172 """Here we match the behavior of Notify My Android and Prowl:
173 raise a single exception if and only if every notification
174 fails"""
175
176 raise_exception = False
177
178 if not self.users:
179 self.logger.warn('notify called with no users set')
180
181 for user in self.users:
182 data = {'token': self.token,
183 'user': user[0],
184 'title': title,
185 'message': message,
186 'timestamp': int(time.time())}
187
188 if user[1]:
189 data['device'] = user[1]
190
191 if kwargs:
192 data.update(kwargs)
193
194 data = urllib.urlencode(data)
195
196 response = self._post(NOTIFY_URL, data)
197 status = self._parse_response(response)
198 if not status:
199 raise_exception = not status
200
201 if raise_exception:
202 self._raise_exception()
203
205 """Verify a user token.
206
207 Args:
208 user: A string containing a valid user token.
209
210 Returns:
211 A boolean containing True if the user token is valid, and
212 False if it is not.
213
214 """
215
216 data = {'token': self.token, 'user': user}
217
218 data = urllib.urlencode(data)
219 response_stream = self._post(VERIFY_URL, data)
220
221 self._parse_response(response_stream, True)
222
223 return self._last_status
224
226 """Verify a device for a user.
227
228 Args:
229 user: A string containing a valid user token.
230 device: A string containing a device name.
231
232 Raises:
233 pushnotify.exceptions.ApiKeyError
234
235 Returns:
236 A boolean containing True if the device is valid, and
237 False if it is not.
238
239 """
240
241 data = {'token': self.token, 'user': user, 'device': device}
242
243 data = urllib.urlencode(data)
244 response_stream = self._post(VERIFY_URL, data)
245
246 self._parse_response(response_stream, True)
247
248 if self._last_user and 'invalid' in self._last_user.lower():
249 self._raise_exception()
250
251 return self._last_status
252
253
254 if __name__ == '__main__':
255 pass
256