1
2
3
4 """Module for sending push notifications to iOS devices that have
5 Prowl installed. See http://www.prowlapp.com/ 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 try:
15 from xml.etree import cElementTree
16 ElementTree = cElementTree
17 except ImportError:
18 from xml.etree import ElementTree
19
20 from pushnotify import abstract
21 from pushnotify import exceptions
22
23
24 PUBLIC_API_URL = u'https://api.prowlapp.com/publicapi'
25 VERIFY_URL = u'/'.join([PUBLIC_API_URL, 'verify'])
26 NOTIFY_URL = u'/'.join([PUBLIC_API_URL, 'add'])
27 RETRIEVE_TOKEN_URL = u'/'.join([PUBLIC_API_URL, 'retrieve', 'token'])
28 RETRIEVE_APIKEY_URL = u'/'.join([PUBLIC_API_URL, 'retrieve', 'apikey'])
29
30 DESC_LIMIT = 10000
31
32
33 -class Client(abstract.AbstractClient):
34 """Client for sending push notificiations to iOS devices with
35 the Prowl application installed.
36
37 Member Vars:
38 developerkey: A string containing a valid provider key for the
39 Prowl application.
40 application: A string containing the name of the application on
41 behalf of whom the Prowl client will be sending messages.
42 apikeys: A dictionary where the keys are strings containing
43 valid user API keys, and the values are lists of strings,
44 each containing a valid user device key. Device keys are not
45 used by this client.
46
47 """
48
49 - def __init__(self, developerkey='', application=''):
50 """Initialize the Prowl client.
51
52 Args:
53 developerkey: A string containing a valid provider key for
54 the Prowl application.
55 application: A string containing the name of the application
56 on behalf of whom the Prowl client will be sending
57 messages.
58
59 """
60
61 super(self.__class__, self).__init__(developerkey, application)
62
63 self._type = 'prowl'
64 self._urls = {'notify': NOTIFY_URL, 'verify': VERIFY_URL,
65 'retrieve_token': RETRIEVE_TOKEN_URL,
66 'retrieve_apikey': RETRIEVE_APIKEY_URL}
67
69
70 xmlresp = response_stream.read()
71 self.logger.info('received response: {0}'.format(xmlresp))
72
73 root = ElementTree.fromstring(xmlresp)
74
75 self._last['type'] = root[0].tag.lower()
76 self._last['code'] = root[0].attrib['code']
77
78 if self._last['type'] == 'success':
79 self._last['message'] = None
80 self._last['remaining'] = root[0].attrib['remaining']
81 self._last['resetdate'] = root[0].attrib['resetdate']
82 elif self._last['type'] == 'error':
83 self._last['message'] = root[0].text
84 self._last['remaining'] = None
85 self._last['resetdate'] = None
86
87 if (not verify or
88 (self._last['code'] != '400' and
89 self._last['code'] != '401')):
90 self._raise_exception()
91 else:
92 raise exceptions.UnrecognizedResponseError(xmlresp, -1)
93
94 if len(root) > 1:
95 if root[1].tag.lower() == 'retrieve':
96 if 'token' in root[1].attrib:
97 self._last['token'] = root[1].attrib['token']
98 self._last['token_url'] = root[1].attrib['url']
99 self._last['apikey'] = None
100 elif 'apikey' in root[1].attrib:
101 self._last['token'] = None
102 self._last['token_url'] = None
103 self._last['apikey'] = root[1].attrib['apikey']
104 else:
105 raise exceptions.UnrecognizedResponseError(xmlresp, -1)
106 else:
107 raise exceptions.UnrecognizedResponseError(xmlresp, -1)
108
109 return root
110
112
113 if self._last['code'] == '400':
114 raise exceptions.FormatError(self._last['message'],
115 int(self._last['code']))
116 elif self._last['code'] == '401':
117 if 'provider' not in self._last['message'].lower():
118 raise exceptions.ApiKeyError(self._last['message'],
119 int(self._last['code']))
120 else:
121 raise exceptions.ProviderKeyError(self._last['message'],
122 int(self._last['code']))
123 elif self._last['code'] == '406':
124 raise exceptions.RateLimitExceeded(self._last['message'],
125 int(self._last['code']))
126 elif self._last['code'] == '409':
127 raise exceptions.PermissionDenied(self._last['message'],
128 int(self._last['code']))
129 elif self._last['code'] == '500':
130 raise exceptions.ServerError(self._last['message'],
131 int(self._last['code']))
132 else:
133 raise exceptions.UnknownError(self._last['message'],
134 int(self._last['code']))
135
136 - def notify(self, description, event, split=True, kwargs=None):
137 """Send a notification to each user's apikey in self.apikeys.
138
139 Args:
140 description: A string of up to DESC_LIMIT characters
141 containing the notification text.
142 event: A string of up to 1024 characters containing a
143 subject or brief description of the event.
144 split: A boolean indicating whether to split long
145 descriptions among multiple notifications (True) or to
146 possibly raise an exception (False). (default True)
147 kwargs: A dictionary with any of the following strings as
148 keys:
149 priority: An integer between -2 and 2, indicating the
150 priority of the notification. -2 is the lowest, 2 is
151 the highest, and 0 is normal.
152 url: A string of up to 512 characters containing a URL
153 to attach to the notification.
154 (default: None)
155
156 Raises:
157 pushnotify.exceptions.ApiKeyError
158 pushnotify.exceptions.FormatError
159 pushnotify.exceptions.RateLimitExceeded
160 pushnotify.exceptions.ServerError
161 pushnotify.exceptions.UnknownError
162 pushnotify.exceptions.UnrecognizedResponseError
163
164 """
165
166 def send_notify(description, event, kwargs):
167 data = {'apikey': ','.join(self.apikeys),
168 'application': self.application,
169 'event': event,
170 'description': description}
171
172 if self.developerkey:
173 data['providerkey'] = self.developerkey
174
175 if kwargs:
176 data.update(kwargs)
177
178 response_stream = self._post(self._urls['notify'], data)
179 self._parse_response_stream(response_stream)
180
181 if not self.apikeys:
182 self.logger.warn('notify called with no users set')
183 return
184
185 if split:
186 while description:
187 this_desc = description[0:DESC_LIMIT]
188 description = description[DESC_LIMIT:]
189 send_notify(this_desc, event, kwargs)
190 else:
191 send_notify(description, event, kwargs)
192
194 """Get a user's API key for a given registration token.
195
196 Once a user has approved you sending them push notifications,
197 you can supply the returned token here and get an API key.
198
199 Args:
200 reg_token: A string containing a registration token returned
201 from the retrieve_token method.
202
203 Raises:
204 pushnotify.exceptions.ProviderKeyError
205
206 Returns:
207 A string containing the API key.
208
209 """
210
211 data = {'providerkey': self.developerkey,
212 'token': reg_token}
213
214 response_stream = self._get(self._urls['retrieve_apikey'], data)
215 self._parse_response_stream(response_stream)
216
217 return self._last['apikey']
218
220 """Get a registration token and approval URL.
221
222 A user follows the approval URL and logs in to the Prowl website
223 to approve you sending them push notifications. If you have
224 associated a 'Retrieve success URL' with your provider key, they
225 will be redirected there.
226
227 Raises:
228 pushnotify.exceptions.ProviderKeyError
229
230 Returns:
231 A two-item tuple where the first item is a string containing
232 a registration token, and the second item is a string
233 containing the associated URL.
234 """
235
236 data = {'providerkey': self.developerkey}
237
238 response_stream = self._get(self._urls['retrieve_token'], data)
239 self._parse_response_stream(response_stream)
240
241 return self._last['token'], self._last['token_url']
242
244 """Verify a user's API key.
245
246 Args:
247 apikey: A string of 40 characters containing a user's API
248 key.
249
250 Raises:
251 pushnotify.exceptions.RateLimitExceeded
252 pushnotify.exceptions.ServerError
253 pushnotify.exceptions.UnknownError
254 pushnotify.exceptions.UnrecognizedResponseError
255
256 Returns:
257 A boolean containing True if the user's API key is valid,
258 and False if it is not.
259
260 """
261
262 data = {'apikey': apikey}
263
264 if self.developerkey:
265 data['providerkey'] = self.developerkey
266
267 response_stream = self._get(self._urls['verify'], data)
268 self._parse_response_stream(response_stream, True)
269
270 return self._last['code'] == '200'
271
272 if __name__ == '__main__':
273 pass
274