Merge lp:~robru/friends/instagram into lp:friends
- Merge into trunk
Proposed by
Robert Bruce Park
Status: | Merged |
---|---|
Approved by: | Robert Bruce Park |
Approved revision: | 201 |
Merged at revision: | 218 |
Proposed branch: | lp:~robru/friends/instagram |
Merge into: | lp:friends |
Diff against target: |
959 lines (+927/-0) 6 files modified
debian/control (+6/-0) debian/friends-instagram.install (+1/-0) friends/protocols/instagram.py (+200/-0) friends/tests/data/instagram-full.dat (+473/-0) friends/tests/data/instagram-login.dat (+18/-0) friends/tests/test_instagram.py (+229/-0) |
To merge this branch: | bzr merge lp:~robru/friends/instagram |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot (community) | continuous-integration | Approve | |
Andrew Starr-Bochicchio | Pending | ||
Review via email: mp+173277@code.launchpad.net |
Commit message
Add instagram support (LP: #1167449)
Description of the change
Thanks again for all your hard work, Andrew! I hope my *enormous* delay in landing this hasn't soured you on contributing to friends in the future.
To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : | # |
review:
Approve
(continuous-integration)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'debian/control' |
2 | --- debian/control 2013-06-06 18:06:19 +0000 |
3 | +++ debian/control 2013-07-05 20:42:17 +0000 |
4 | @@ -120,3 +120,9 @@ |
5 | account-plugin-flickr, |
6 | Description: Social integration with the desktop - Flickr |
7 | Provides social networking integration with the desktop |
8 | + |
9 | +Package: friends-instagram |
10 | +Architecture: all |
11 | +Depends: friends, ${misc:Depends}, ${python3:Depends} |
12 | +Description: Social integration with the desktop - Instagram |
13 | + Provides social networking integration with the desktop |
14 | |
15 | === added file 'debian/friends-instagram.install' |
16 | --- debian/friends-instagram.install 1970-01-01 00:00:00 +0000 |
17 | +++ debian/friends-instagram.install 2013-07-05 20:42:17 +0000 |
18 | @@ -0,0 +1,1 @@ |
19 | +usr/lib/python3/dist-packages/friends/protocols/instagram* |
20 | |
21 | === added file 'friends/protocols/instagram.py' |
22 | --- friends/protocols/instagram.py 1970-01-01 00:00:00 +0000 |
23 | +++ friends/protocols/instagram.py 2013-07-05 20:42:17 +0000 |
24 | @@ -0,0 +1,200 @@ |
25 | +# friends-dispatcher -- send & receive messages from any social network |
26 | +# Copyright (C) 2013 Canonical Ltd |
27 | +# |
28 | +# This program is free software: you can redistribute it and/or modify |
29 | +# it under the terms of the GNU General Public License as published by |
30 | +# the Free Software Foundation, version 3 of the License. |
31 | +# |
32 | +# This program is distributed in the hope that it will be useful, |
33 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
34 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
35 | +# GNU General Public License for more details. |
36 | +# |
37 | +# You should have received a copy of the GNU General Public License |
38 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
39 | + |
40 | +"""The Instagram protocol plugin.""" |
41 | + |
42 | + |
43 | +__all__ = [ |
44 | + 'Instagram', |
45 | + ] |
46 | + |
47 | +import time |
48 | +import logging |
49 | + |
50 | +from friends.utils.avatar import Avatar |
51 | +from friends.utils.base import Base, feature |
52 | +from friends.utils.cache import JsonCache |
53 | +from friends.utils.http import Downloader, Uploader |
54 | +from friends.utils.time import parsetime, iso8601utc |
55 | +from friends.errors import FriendsError |
56 | + |
57 | +log = logging.getLogger(__name__) |
58 | + |
59 | +class Instagram(Base): |
60 | + _api_base = 'https://api.instagram.com/v1/{endpoint}?access_token={token}' |
61 | + def _whoami(self, authdata): |
62 | + """Identify the authenticating user.""" |
63 | + url = self._api_base.format( |
64 | + endpoint='users/self', |
65 | + token=self._get_access_token()) |
66 | + result = Downloader(url).get_json() |
67 | + self._account.user_id = result.get('data').get('id') |
68 | + self._account.user_name = result.get('data').get('username') |
69 | + |
70 | + def _publish_entry(self, entry, stream='messages'): |
71 | + """Publish a single update into the Dee.SharedModel.""" |
72 | + message_id = entry.get('id') |
73 | + |
74 | + if message_id is None: |
75 | + # We can't do much with this entry. |
76 | + return |
77 | + |
78 | + person = entry.get('user') |
79 | + nick = person.get('username') |
80 | + name = person.get('full_name') |
81 | + person_id = person.get('id') |
82 | + message= '%s shared a picture on Instagram.' % nick |
83 | + person_icon = Avatar.get_image(person.get('profile_picture')) |
84 | + person_url = 'http://instagram.com/' + nick |
85 | + picture = entry.get('images').get('thumbnail').get('url') |
86 | + if entry.get('caption'): |
87 | + desc = entry.get('caption').get('text', '') |
88 | + else: |
89 | + desc = '' |
90 | + url = entry.get('link') |
91 | + timestamp = entry.get('created_time') |
92 | + if timestamp is not None: |
93 | + timestamp = iso8601utc(parsetime(timestamp)) |
94 | + likes = entry.get('likes').get('count') |
95 | + liked = entry.get('user_has_liked') |
96 | + location = entry.get('location', {}) |
97 | + if location: |
98 | + latitude = location.get('latitude', '') |
99 | + longitude = location.get('longitude', '') |
100 | + else: |
101 | + latitude = 0 |
102 | + longitude = 0 |
103 | + |
104 | + args = dict( |
105 | + message_id=message_id, |
106 | + message=message, |
107 | + stream=stream, |
108 | + likes=likes, |
109 | + sender_id=person_id, |
110 | + sender=name, |
111 | + sender_nick=nick, |
112 | + url=person_url, |
113 | + icon_uri=person_icon, |
114 | + link_url=url, |
115 | + link_picture=picture, |
116 | + link_desc=desc, |
117 | + timestamp=timestamp, |
118 | + liked=liked, |
119 | + latitude=latitude, |
120 | + longitude=longitude |
121 | + ) |
122 | + |
123 | + self._publish(**args) |
124 | + |
125 | + # If there are any replies, publish them as well. |
126 | + parent_id = message_id |
127 | + for comment in entry.get('comments', {}).get('data', []): |
128 | + if comment: |
129 | + self._publish_comment( |
130 | + comment, stream='reply_to/{}'.format(parent_id)) |
131 | + return args['url'] |
132 | + |
133 | + def _publish_comment(self, comment, stream): |
134 | + message_id = comment.get('id') |
135 | + if message_id is None: |
136 | + return |
137 | + message = comment.get('text', '') |
138 | + person = comment.get('from', {}) |
139 | + sender_nick = person.get('username') |
140 | + timestamp = comment.get('created_time') |
141 | + if timestamp is not None: |
142 | + timestamp = iso8601utc(parsetime(timestamp)) |
143 | + icon_uri = Avatar.get_image(person.get('profile_picture')) |
144 | + sender_id = person.get('id') |
145 | + sender = person.get('full_name') |
146 | + |
147 | + args = dict( |
148 | + stream=stream, |
149 | + message_id=message_id, |
150 | + message=message, |
151 | + timestamp=timestamp, |
152 | + sender_nick=sender_nick, |
153 | + icon_uri=icon_uri, |
154 | + sender_id=sender_id, |
155 | + sender=sender, |
156 | + ) |
157 | +# print(args) |
158 | + self._publish(**args) |
159 | + |
160 | + @feature |
161 | + def home(self): |
162 | + """Gather and publish public timeline messages.""" |
163 | + url = self._api_base.format( |
164 | + endpoint='users/self/feed', |
165 | + token=self._get_access_token()) |
166 | + result = Downloader(url).get_json() |
167 | + values = result.get('data', {}) |
168 | + for update in values: |
169 | + self._publish_entry(update) |
170 | + |
171 | + @feature |
172 | + def receive(self): |
173 | + """Gather and publish all incoming messages.""" |
174 | + self.home() |
175 | + return self._get_n_rows() |
176 | + |
177 | + def _send(self, obj_id, message, endpoint, stream='messages'): |
178 | + token = self._get_access_token() |
179 | + |
180 | + url = self._api_base.format(endpoint=endpoint, token=token) |
181 | + |
182 | + result = Downloader( |
183 | + url, |
184 | + method='POST', |
185 | + params=dict(access_token=token, text=message)).get_json() |
186 | + new_id = result.get('id') |
187 | + if new_id is None: |
188 | + raise FriendsError('Failed sending to Instagram: {!r}'.format(result)) |
189 | + enpoint = 'media/{}/comments'.format(new_id) |
190 | + url = self._api_base.format(endpoint=endpoint, token=token) |
191 | + comment = Downloader(url, params=dict(access_token=token)).get_json() |
192 | + return self._publish_entry(entry=comment, stream=stream) |
193 | + |
194 | + @feature |
195 | + def send_thread(self, obj_id, message): |
196 | + """Write a comment on some existing picture. |
197 | + """ |
198 | + return self._send(obj_id, message, 'media/{}/comments'.format(obj_id), |
199 | + stream='reply_to/{}'.format(obj_id)) |
200 | + |
201 | + def _like(self, obj_id, endpoint, method): |
202 | + token = self._get_access_token() |
203 | + url = self._api_base.format(endpoint=endpoint, token=token) |
204 | + |
205 | + if not Downloader(url, method=method, |
206 | + params=dict(access_token=token)).get_json(): |
207 | + raise FriendsError('Failed to {} like {} on Instagram'.format( |
208 | + method, obj_id)) |
209 | + |
210 | + @feature |
211 | + def like(self, obj_id): |
212 | + endpoint = 'media/{}/likes'.format(obj_id) |
213 | + self._like(obj_id, endpoint, 'POST') |
214 | + self._inc_cell(obj_id, 'likes') |
215 | + self._set_cell(obj_id, 'liked', True) |
216 | + return obj_id |
217 | + |
218 | + @feature |
219 | + def unlike(self, obj_id): |
220 | + endpoint = 'media/{}/likes'.format(obj_id) |
221 | + self._like(obj_id, endpoint, 'DELETE') |
222 | + self._dec_cell(obj_id, 'likes') |
223 | + self._set_cell(obj_id, 'liked', False) |
224 | + return obj_id |
225 | |
226 | === added file 'friends/tests/data/instagram-full.dat' |
227 | --- friends/tests/data/instagram-full.dat 1970-01-01 00:00:00 +0000 |
228 | +++ friends/tests/data/instagram-full.dat 2013-07-05 20:42:17 +0000 |
229 | @@ -0,0 +1,473 @@ |
230 | +{ |
231 | + "pagination": { |
232 | + "next_url": "https://api.instagram.com/v1/users/self/feed?access_token=4abc&max_id=431429392441037546_46931811", |
233 | + "next_max_id": "431429392441037546_46931811" |
234 | + }, |
235 | + "meta": { |
236 | + "code": 200 |
237 | + }, |
238 | + "data": [ |
239 | + { |
240 | + "attribution": null, |
241 | + "tags": [], |
242 | + "type": "image", |
243 | + "location": null, |
244 | + "comments": { |
245 | + "count": 1, |
246 | + "data": [ |
247 | + { |
248 | + "created_time": "1365680633", |
249 | + "text": "Wtf is that from?", |
250 | + "from": { |
251 | + "username": "ellllliottttt", |
252 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_13811917_75sq_1322106636.jpg", |
253 | + "id": "13811917", |
254 | + "full_name": "Elliott Markowitz" |
255 | + }, |
256 | + "id": "431682899841631793" |
257 | + } |
258 | + ] |
259 | + }, |
260 | + "filter": "Normal", |
261 | + "created_time": "1365655801", |
262 | + "link": "http://instagram.com/p/X859raK8fx/", |
263 | + "likes": { |
264 | + "count": 8, |
265 | + "data": [ |
266 | + { |
267 | + "username": "noahvargass", |
268 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_1037856_75sq_1354178959.jpg", |
269 | + "id": "1037856", |
270 | + "full_name": "Noah Vargas" |
271 | + }, |
272 | + { |
273 | + "username": "deniboring", |
274 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_16828740_75sq_1345748703.jpg", |
275 | + "id": "16828740", |
276 | + "full_name": "deniboring" |
277 | + }, |
278 | + { |
279 | + "username": "gabriel_cordero", |
280 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_185127499_75sq_1340358021.jpg", |
281 | + "id": "185127499", |
282 | + "full_name": "cheeseburger pocket" |
283 | + }, |
284 | + { |
285 | + "username": "inmyheadache", |
286 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_22645548_75sq_1343529900.jpg", |
287 | + "id": "22645548", |
288 | + "full_name": "Tim F" |
289 | + }, |
290 | + { |
291 | + "username": "ellllliottttt", |
292 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_13811917_75sq_1322106636.jpg", |
293 | + "id": "13811917", |
294 | + "full_name": "Elliott Markowitz" |
295 | + }, |
296 | + { |
297 | + "username": "eyesofsatan", |
298 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_2526196_75sq_1352254863.jpg", |
299 | + "id": "2526196", |
300 | + "full_name": "Christopher Hansell" |
301 | + }, |
302 | + { |
303 | + "username": "lakeswimming", |
304 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_3610143_75sq_1363648525.jpg", |
305 | + "id": "3610143", |
306 | + "full_name": "lakeswimming" |
307 | + }, |
308 | + { |
309 | + "username": "jtesnakis", |
310 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_214679263_75sq_1346185198.jpg", |
311 | + "id": "214679263", |
312 | + "full_name": "Jonathan" |
313 | + } |
314 | + ] |
315 | + }, |
316 | + "images": { |
317 | + "low_resolution": { |
318 | + "url": "http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_6.jpg", |
319 | + "width": 306, |
320 | + "height": 306 |
321 | + }, |
322 | + "thumbnail": { |
323 | + "url": "http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_5.jpg", |
324 | + "width": 150, |
325 | + "height": 150 |
326 | + }, |
327 | + "standard_resolution": { |
328 | + "url": "http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_7.jpg", |
329 | + "width": 612, |
330 | + "height": 612 |
331 | + } |
332 | + }, |
333 | + "caption": null, |
334 | + "user_has_liked": false, |
335 | + "id": "431474591469914097_223207800", |
336 | + "user": { |
337 | + "username": "joshwolp", |
338 | + "website": "", |
339 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_223207800_75sq_1347753109.jpg", |
340 | + "full_name": "Josh", |
341 | + "bio": "", |
342 | + "id": "223207800" |
343 | + } |
344 | + }, |
345 | + { |
346 | + "attribution": null, |
347 | + "tags": [ |
348 | + "nofilter" |
349 | + ], |
350 | + "type": "image", |
351 | + "location": { |
352 | + "latitude": 40.702485554, |
353 | + "longitude": -73.929230548 |
354 | + }, |
355 | + "comments": { |
356 | + "count": 3, |
357 | + "data": [ |
358 | + { |
359 | + "created_time": "1365654315", |
360 | + "text": "I remember pushing that little guy of the swings a few times....", |
361 | + "from": { |
362 | + "username": "squidneylol", |
363 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_5917696_75sq_1336705905.jpg", |
364 | + "id": "5917696", |
365 | + "full_name": "Syd" |
366 | + }, |
367 | + "id": "431462132263145102" |
368 | + }, |
369 | + { |
370 | + "created_time": "1365665741", |
371 | + "text": "Stop it!!!", |
372 | + "from": { |
373 | + "username": "nightruiner", |
374 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_31239607_75sq_1358968326.jpg", |
375 | + "id": "31239607", |
376 | + "full_name": "meredith gaydosh" |
377 | + }, |
378 | + "id": "431557973837598336" |
379 | + }, |
380 | + { |
381 | + "created_time": "1365691283", |
382 | + "text": "You know I hate being held.", |
383 | + "from": { |
384 | + "username": "piratemaddi", |
385 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_191388805_75sq_1341897870.jpg", |
386 | + "id": "191388805", |
387 | + "full_name": "piratemaddi" |
388 | + }, |
389 | + "id": "431772240369143639" |
390 | + } |
391 | + ] |
392 | + }, |
393 | + "filter": "Normal", |
394 | + "created_time": "1365651440", |
395 | + "link": "http://instagram.com/p/X8xpYwk82w/", |
396 | + "likes": { |
397 | + "count": 17, |
398 | + "data": [ |
399 | + { |
400 | + "username": "meaghanlou", |
401 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_41007792_75sq_1334443633.jpg", |
402 | + "id": "41007792", |
403 | + "full_name": "meaghanlou" |
404 | + }, |
405 | + { |
406 | + "username": "shahwiththat", |
407 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_40353923_75sq_1361330343.jpg", |
408 | + "id": "40353923", |
409 | + "full_name": "Soraya Shah" |
410 | + }, |
411 | + { |
412 | + "username": "marsyred", |
413 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_216361031_75sq_1346524176.jpg", |
414 | + "id": "216361031", |
415 | + "full_name": "marsyred" |
416 | + }, |
417 | + { |
418 | + "username": "thewhitewizzard", |
419 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_189027880_75sq_1341183134.jpg", |
420 | + "id": "189027880", |
421 | + "full_name": "Drew Mack" |
422 | + }, |
423 | + { |
424 | + "username": "juddymagee", |
425 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_8993099_75sq_1315066258.jpg", |
426 | + "id": "8993099", |
427 | + "full_name": "juddymagee" |
428 | + }, |
429 | + { |
430 | + "username": "pixxamonster", |
431 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_1672624_75sq_1364673157.jpg", |
432 | + "id": "1672624", |
433 | + "full_name": "Sonia 👓 + Dita 🐈" |
434 | + }, |
435 | + { |
436 | + "username": "tongue_in_cheek", |
437 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_5710964_75sq_1345359909.jpg", |
438 | + "id": "5710964", |
439 | + "full_name": "merrily." |
440 | + }, |
441 | + { |
442 | + "username": "nightruiner", |
443 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_31239607_75sq_1358968326.jpg", |
444 | + "id": "31239607", |
445 | + "full_name": "meredith gaydosh" |
446 | + }, |
447 | + { |
448 | + "username": "eliz_mortals", |
449 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_36417303_75sq_1364179217.jpg", |
450 | + "id": "36417303", |
451 | + "full_name": "eliz_mortals" |
452 | + }, |
453 | + { |
454 | + "username": "caranicoletti", |
455 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_14162814_75sq_1359848230.jpg", |
456 | + "id": "14162814", |
457 | + "full_name": "Cara Nicoletti" |
458 | + } |
459 | + ] |
460 | + }, |
461 | + "images": { |
462 | + "low_resolution": { |
463 | + "url": "http://distilleryimage8.s3.amazonaws.com/1d99da5ca25911e2822f22000a9f09ca_6.jpg", |
464 | + "width": 306, |
465 | + "height": 306 |
466 | + }, |
467 | + "thumbnail": { |
468 | + "url": "http://distilleryimage8.s3.amazonaws.com/1d99da5ca25911e2822f22000a9f09ca_5.jpg", |
469 | + "width": 150, |
470 | + "height": 150 |
471 | + }, |
472 | + "standard_resolution": { |
473 | + "url": "http://distilleryimage8.s3.amazonaws.com/1d99da5ca25911e2822f22000a9f09ca_7.jpg", |
474 | + "width": 612, |
475 | + "height": 612 |
476 | + } |
477 | + }, |
478 | + "caption": { |
479 | + "created_time": "1365651465", |
480 | + "text": "National siblings day @piratemaddi ? You mustn't've known. #nofilter", |
481 | + "from": { |
482 | + "username": "what_a_handsome_boy", |
483 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_5891266_75sq_1345460082.jpg", |
484 | + "id": "5891266", |
485 | + "full_name": "Fence!" |
486 | + }, |
487 | + "id": "431438224520630210" |
488 | + }, |
489 | + "user_has_liked": false, |
490 | + "id": "431438012683111856_5891266", |
491 | + "user": { |
492 | + "username": "what_a_handsome_boy", |
493 | + "website": "", |
494 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_5891266_75sq_1345460082.jpg", |
495 | + "full_name": "Fence!", |
496 | + "bio": "", |
497 | + "id": "5891266" |
498 | + } |
499 | + }, |
500 | + { |
501 | + "attribution": null, |
502 | + "tags": [ |
503 | + "canada", |
504 | + "iphone", |
505 | + "punklife", |
506 | + "acap", |
507 | + "doom", |
508 | + "crust" |
509 | + ], |
510 | + "type": "image", |
511 | + "location": null, |
512 | + "comments": { |
513 | + "count": 7, |
514 | + "data": [ |
515 | + { |
516 | + "created_time": "1365651434", |
517 | + "text": "I have not seen him for a minute, he is a great guy!", |
518 | + "from": { |
519 | + "username": "rocktoberblood", |
520 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_210506849_75sq_1355190976.jpg", |
521 | + "id": "210506849", |
522 | + "full_name": "Nate Phillips" |
523 | + }, |
524 | + "id": "431437959929388541" |
525 | + }, |
526 | + { |
527 | + "created_time": "1365652422", |
528 | + "text": "Saw him puke in the elevator at the Hilton when Doom played chaos.", |
529 | + "from": { |
530 | + "username": "occult_obsession", |
531 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_6080835_75sq_1364154159.jpg", |
532 | + "id": "6080835", |
533 | + "full_name": "Evan Vellela" |
534 | + }, |
535 | + "id": "431446251162427175" |
536 | + }, |
537 | + { |
538 | + "created_time": "1365652900", |
539 | + "text": "Lol ^ who DIDN'T do this at chaos", |
540 | + "from": { |
541 | + "username": "laurababbili", |
542 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_1238692_75sq_1361065720.jpg", |
543 | + "id": "1238692", |
544 | + "full_name": "Laura Babbili" |
545 | + }, |
546 | + "id": "431450261906907041" |
547 | + }, |
548 | + { |
549 | + "created_time": "1365655310", |
550 | + "text": "I tried to kiss the singer of cocksparrer in that elevator.", |
551 | + "from": { |
552 | + "username": "gregdaly", |
553 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_31133446_75sq_1333488205.jpg", |
554 | + "id": "31133446", |
555 | + "full_name": "greg daly" |
556 | + }, |
557 | + "id": "431470473175752176" |
558 | + }, |
559 | + { |
560 | + "created_time": "1365655687", |
561 | + "text": "Leant that dude a 900 Marshall a time ago! Love that dude!!", |
562 | + "from": { |
563 | + "username": "nickatnightengale", |
564 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_29366372_75sq_1354241971.jpg", |
565 | + "id": "29366372", |
566 | + "full_name": "Nick Poulos" |
567 | + }, |
568 | + "id": "431473639539727956" |
569 | + }, |
570 | + { |
571 | + "created_time": "1365658654", |
572 | + "text": "Hahaha!!", |
573 | + "from": { |
574 | + "username": "jeremyhush", |
575 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_43750226_75sq_1335024423.jpg", |
576 | + "id": "43750226", |
577 | + "full_name": "Jeremy Hush" |
578 | + }, |
579 | + "id": "431498525729480083" |
580 | + }, |
581 | + { |
582 | + "created_time": "1365685226", |
583 | + "text": "There's an app for that", |
584 | + "from": { |
585 | + "username": "liam_wilson", |
586 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_4357248_75sq_1327009309.jpg", |
587 | + "id": "4357248", |
588 | + "full_name": "Liam Wilson" |
589 | + }, |
590 | + "id": "431721432141388885" |
591 | + } |
592 | + ] |
593 | + }, |
594 | + "filter": "X-Pro II", |
595 | + "created_time": "1365650801", |
596 | + "link": "http://instagram.com/p/X8wbTQtd3Y/", |
597 | + "likes": { |
598 | + "count": 36, |
599 | + "data": [ |
600 | + { |
601 | + "username": "fatbenatar", |
602 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_280136224_75sq_1357073366.jpg", |
603 | + "id": "280136224", |
604 | + "full_name": "China Oxendine" |
605 | + }, |
606 | + { |
607 | + "username": "tejleo", |
608 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_10403332_75sq_1354058520.jpg", |
609 | + "id": "10403332", |
610 | + "full_name": "tejleo" |
611 | + }, |
612 | + { |
613 | + "username": "tonyromeo13", |
614 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_255933637_75sq_1360272947.jpg", |
615 | + "id": "255933637", |
616 | + "full_name": "Tony Romeo" |
617 | + }, |
618 | + { |
619 | + "username": "libraryrat4", |
620 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_17715046_75sq_1325381597.jpg", |
621 | + "id": "17715046", |
622 | + "full_name": "libraryrat4" |
623 | + }, |
624 | + { |
625 | + "username": "boredwithapathy", |
626 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_174679368_75sq_1338500455.jpg", |
627 | + "id": "174679368", |
628 | + "full_name": "Lisa McCarthy" |
629 | + }, |
630 | + { |
631 | + "username": "justinpittney", |
632 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_10976940_75sq_1361306615.jpg", |
633 | + "id": "10976940", |
634 | + "full_name": "Justin Pittney" |
635 | + }, |
636 | + { |
637 | + "username": "nuclearhell", |
638 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_212702314_75sq_1361642319.jpg", |
639 | + "id": "212702314", |
640 | + "full_name": "Rachel Whittaker" |
641 | + }, |
642 | + { |
643 | + "username": "gregelk", |
644 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_240084204_75sq_1359223473.jpg", |
645 | + "id": "240084204", |
646 | + "full_name": "Greg Elk" |
647 | + }, |
648 | + { |
649 | + "username": "bradhasher", |
650 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_283278325_75sq_1357514041.jpg", |
651 | + "id": "283278325", |
652 | + "full_name": "Brett Ellingson" |
653 | + }, |
654 | + { |
655 | + "username": "zaimlmzim", |
656 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_180060277_75sq_1363118292.jpg", |
657 | + "id": "180060277", |
658 | + "full_name": "zaimlmzim" |
659 | + } |
660 | + ] |
661 | + }, |
662 | + "images": { |
663 | + "low_resolution": { |
664 | + "url": "http://distilleryimage4.s3.amazonaws.com/a051f882a25711e282e122000a1f9aae_6.jpg", |
665 | + "width": 306, |
666 | + "height": 306 |
667 | + }, |
668 | + "thumbnail": { |
669 | + "url": "http://distilleryimage4.s3.amazonaws.com/a051f882a25711e282e122000a1f9aae_5.jpg", |
670 | + "width": 150, |
671 | + "height": 150 |
672 | + }, |
673 | + "standard_resolution": { |
674 | + "url": "http://distilleryimage4.s3.amazonaws.com/a051f882a25711e282e122000a1f9aae_7.jpg", |
675 | + "width": 612, |
676 | + "height": 612 |
677 | + } |
678 | + }, |
679 | + "caption": { |
680 | + "created_time": "1365650915", |
681 | + "text": "This is my friend Scoot. He plays bass in #doom and is a mega rock star in newfoundland. Here you can see him signing an autograph on an iphone. #crust #punklife #iphone #canada #acap", |
682 | + "from": { |
683 | + "username": "gregdaly", |
684 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_31133446_75sq_1333488205.jpg", |
685 | + "id": "31133446", |
686 | + "full_name": "greg daly" |
687 | + }, |
688 | + "id": "431433605218426202" |
689 | + }, |
690 | + "user_has_liked": false, |
691 | + "id": "431432646660578776_31133446", |
692 | + "user": { |
693 | + "username": "gregdaly", |
694 | + "website": "", |
695 | + "profile_picture": "http://images.ak.instagram.com/profiles/profile_31133446_75sq_1333488205.jpg", |
696 | + "full_name": "greg daly", |
697 | + "bio": "", |
698 | + "id": "31133446" |
699 | + } |
700 | + } |
701 | + ] |
702 | +} |
703 | |
704 | === added file 'friends/tests/data/instagram-login.dat' |
705 | --- friends/tests/data/instagram-login.dat 1970-01-01 00:00:00 +0000 |
706 | +++ friends/tests/data/instagram-login.dat 2013-07-05 20:42:17 +0000 |
707 | @@ -0,0 +1,18 @@ |
708 | +{ |
709 | + "meta": { |
710 | + "code": 200 |
711 | + }, |
712 | + "data": { |
713 | + "username": "bpersons", |
714 | + "bio": "", |
715 | + "website": "", |
716 | + "profile_picture": "http://images.ak.instagram.com/profiles/anonymousUser.jpg", |
717 | + "full_name": "Bart Person", |
718 | + "counts": { |
719 | + "media": 174, |
720 | + "followed_by": 100, |
721 | + "follows": 503 |
722 | + }, |
723 | + "id": "801" |
724 | + } |
725 | +} |
726 | |
727 | === added file 'friends/tests/test_instagram.py' |
728 | --- friends/tests/test_instagram.py 1970-01-01 00:00:00 +0000 |
729 | +++ friends/tests/test_instagram.py 2013-07-05 20:42:17 +0000 |
730 | @@ -0,0 +1,229 @@ |
731 | +# friends-dispatcher -- send & receive messages from any social network |
732 | +# Copyright (C) 2012 Canonical Ltd |
733 | +# |
734 | +# This program is free software: you can redistribute it and/or modify |
735 | +# it under the terms of the GNU General Public License as published by |
736 | +# the Free Software Foundation, version 3 of the License. |
737 | +# |
738 | +# This program is distributed in the hope that it will be useful, |
739 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
740 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
741 | +# GNU General Public License for more details. |
742 | +# |
743 | +# You should have received a copy of the GNU General Public License |
744 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
745 | + |
746 | +"""Test the Instagram plugin.""" |
747 | + |
748 | + |
749 | +__all__ = [ |
750 | + 'TestInstagram', |
751 | + ] |
752 | + |
753 | + |
754 | +import os |
755 | +import tempfile |
756 | +import unittest |
757 | +import shutil |
758 | + |
759 | +from friends.protocols.instagram import Instagram |
760 | +from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock |
761 | +from friends.tests.mocks import TestModel, mock |
762 | +from friends.tests.mocks import EDSRegistry |
763 | +from friends.errors import FriendsError, AuthorizationError |
764 | +from friends.utils.cache import JsonCache |
765 | + |
766 | + |
767 | +@mock.patch('friends.utils.http._soup', mock.Mock()) |
768 | +@mock.patch('friends.utils.base.notify', mock.Mock()) |
769 | +class TestInstagram(unittest.TestCase): |
770 | + """Test the Instagram API.""" |
771 | + |
772 | + def setUp(self): |
773 | + self._temp_cache = tempfile.mkdtemp() |
774 | + self._root = JsonCache._root = os.path.join( |
775 | + self._temp_cache, '{}.json') |
776 | + self.account = FakeAccount() |
777 | + self.protocol = Instagram(self.account) |
778 | + self.protocol.source_registry = EDSRegistry() |
779 | + |
780 | + def tearDown(self): |
781 | + TestModel.clear() |
782 | + shutil.rmtree(self._temp_cache) |
783 | + |
784 | + def test_features(self): |
785 | + # The set of public features. |
786 | + self.assertEqual(Instagram.get_features(), |
787 | + ['home', 'like', 'receive', 'send_thread', 'unlike']) |
788 | + |
789 | + @mock.patch('friends.utils.authentication.manager') |
790 | + @mock.patch('friends.utils.authentication.Accounts') |
791 | + @mock.patch('friends.utils.authentication.Authentication.__init__', |
792 | + return_value=None) |
793 | + @mock.patch('friends.utils.authentication.Authentication.login', |
794 | + return_value=dict(AccessToken='abc')) |
795 | + @mock.patch('friends.utils.http.Soup.Message', |
796 | + FakeSoupMessage('friends.tests.data', 'instagram-login.dat')) |
797 | + def test_successful_login(self, *mock): |
798 | + # Test that a successful response from instagram.com returning |
799 | + # the user's data, sets up the account dict correctly. |
800 | + self.protocol._login() |
801 | + self.assertEqual(self.account.access_token, 'abc') |
802 | + self.assertEqual(self.account.user_name, 'bpersons') |
803 | + self.assertEqual(self.account.user_id, '801') |
804 | + |
805 | + @mock.patch('friends.utils.authentication.manager') |
806 | + @mock.patch('friends.utils.authentication.Accounts') |
807 | + @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1) |
808 | + @mock.patch('friends.utils.authentication.Signon.AuthSession.new') |
809 | + def test_login_unsuccessful_authentication(self, *mock): |
810 | + # The user is not already logged in, but the act of logging in fails. |
811 | + self.assertRaises(AuthorizationError, self.protocol._login) |
812 | + self.assertIsNone(self.account.access_token) |
813 | + self.assertIsNone(self.account.user_name) |
814 | + |
815 | + @mock.patch('friends.utils.authentication.manager') |
816 | + @mock.patch('friends.utils.authentication.Accounts') |
817 | + @mock.patch('friends.utils.authentication.Authentication.login', |
818 | + return_value=dict(AccessToken='abc')) |
819 | + @mock.patch('friends.protocols.instagram.Downloader.get_json', |
820 | + return_value=dict( |
821 | + error=dict(message='Bad access token', |
822 | + type='OAuthException', |
823 | + code=190))) |
824 | + def test_error_response(self, *mocks): |
825 | + with LogMock('friends.utils.base', |
826 | + 'friends.protocols.instagram') as log_mock: |
827 | + self.assertRaises( |
828 | + FriendsError, |
829 | + self.protocol.home, |
830 | + ) |
831 | + contents = log_mock.empty(trim=False) |
832 | + self.assertEqual(contents, 'Logging in to Instagram\n') |
833 | + |
834 | + @mock.patch('friends.utils.http.Soup.Message', |
835 | + FakeSoupMessage('friends.tests.data', 'instagram-full.dat')) |
836 | + @mock.patch('friends.utils.base.Model', TestModel) |
837 | + @mock.patch('friends.protocols.instagram.Instagram._login', |
838 | + return_value=True) |
839 | + def test_receive(self, *mocks): |
840 | + # Receive the feed for a user. |
841 | + self.maxDiff = None |
842 | + self.account.access_token = 'abc' |
843 | + self.assertEqual(self.protocol.receive(), 14) |
844 | + self.assertEqual(TestModel.get_n_rows(), 14) |
845 | + self.assertEqual(list(TestModel.get_row(0)), [ |
846 | + 'instagram', |
847 | + 88, |
848 | + '431474591469914097_223207800', |
849 | + 'messages', |
850 | + 'Josh', |
851 | + '223207800', |
852 | + 'joshwolp', |
853 | + False, |
854 | + '2013-04-11T04:50:01Z', |
855 | + 'joshwolp shared a picture on Instagram.', |
856 | + '/tmp/friends-avatars/ca55b643e7b440762c7c6292399eed6542a84b90', |
857 | + 'http://instagram.com/joshwolp', |
858 | + 8, |
859 | + False, |
860 | + 'http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_5.jpg', |
861 | + '', |
862 | + 'http://instagram.com/p/X859raK8fx/', |
863 | + '', |
864 | + '', |
865 | + '', |
866 | + '', |
867 | + 0.0, |
868 | + 0.0, |
869 | + ]) |
870 | + self.assertEqual(list(TestModel.get_row(3)), [ |
871 | + 'instagram', |
872 | + 88, |
873 | + '431462132263145102', |
874 | + 'reply_to/431438012683111856_5891266', |
875 | + 'Syd', |
876 | + '5917696', |
877 | + 'squidneylol', |
878 | + False, |
879 | + '2013-04-11T04:25:15Z', |
880 | + 'I remember pushing that little guy of the swings a few times....', |
881 | + '/tmp/friends-avatars/e61c8d91e37fec3e1dec9325fa4edc52ebeb96bb', |
882 | + '', |
883 | + 0, |
884 | + False, |
885 | + '', |
886 | + '', |
887 | + '', |
888 | + '', |
889 | + '', |
890 | + '', |
891 | + '', |
892 | + 0.0, |
893 | + 0.0, |
894 | + ]) |
895 | + |
896 | + @mock.patch('friends.protocols.instagram.Downloader') |
897 | + def test_send_thread(self, dload): |
898 | + dload().get_json.return_value = dict(id='comment_id') |
899 | + token = self.protocol._get_access_token = mock.Mock( |
900 | + return_value='abc') |
901 | + publish = self.protocol._publish_entry = mock.Mock( |
902 | + return_value='http://instagram.com/p/post_id') |
903 | + |
904 | + self.assertEqual( |
905 | + self.protocol.send_thread('post_id', 'Some witty response!'), |
906 | + 'http://instagram.com/p/post_id') |
907 | + token.assert_called_once_with() |
908 | + publish.assert_called_with(entry={'id': 'comment_id'}, |
909 | + stream='reply_to/post_id') |
910 | + self.assertEqual( |
911 | + dload.mock_calls, |
912 | + [mock.call(), |
913 | + mock.call( |
914 | + 'https://api.instagram.com/v1/media/post_id/comments?access_token=abc', |
915 | + method='POST', |
916 | + params=dict( |
917 | + access_token='abc', |
918 | + text='Some witty response!')), |
919 | + mock.call().get_json(), |
920 | + mock.call('https://api.instagram.com/v1/media/post_id/comments?access_token=abc', |
921 | + params=dict(access_token='abc')), |
922 | + mock.call().get_json(), |
923 | + ]) |
924 | + |
925 | + @mock.patch('friends.protocols.instagram.Downloader') |
926 | + def test_like(self, dload): |
927 | + dload().get_json.return_value = True |
928 | + token = self.protocol._get_access_token = mock.Mock( |
929 | + return_value='insta') |
930 | + inc_cell = self.protocol._inc_cell = mock.Mock() |
931 | + set_cell = self.protocol._set_cell = mock.Mock() |
932 | + |
933 | + self.assertEqual(self.protocol.like('post_id'), 'post_id') |
934 | + |
935 | + inc_cell.assert_called_once_with('post_id', 'likes') |
936 | + set_cell.assert_called_once_with('post_id', 'liked', True) |
937 | + token.assert_called_once_with() |
938 | + dload.assert_called_with( |
939 | + 'https://api.instagram.com/v1/media/post_id/likes?access_token=insta', |
940 | + method='POST', |
941 | + params=dict(access_token='insta')) |
942 | + |
943 | + @mock.patch('friends.protocols.instagram.Downloader') |
944 | + def test_unlike(self, dload): |
945 | + dload.get_json.return_value = True |
946 | + token = self.protocol._get_access_token = mock.Mock( |
947 | + return_value='insta') |
948 | + dec_cell = self.protocol._dec_cell = mock.Mock() |
949 | + set_cell = self.protocol._set_cell = mock.Mock() |
950 | + |
951 | + self.assertEqual(self.protocol.unlike('post_id'), 'post_id') |
952 | + |
953 | + dec_cell.assert_called_once_with('post_id', 'likes') |
954 | + set_cell.assert_called_once_with('post_id', 'liked', False) |
955 | + token.assert_called_once_with() |
956 | + dload.assert_called_once_with( |
957 | + 'https://api.instagram.com/v1/media/post_id/likes?access_token=insta', |
958 | + method='DELETE', |
959 | + params=dict(access_token='insta')) |
PASSED: Continuous integration, rev:201 jenkins. qa.ubuntu. com/job/ friends- ci/48/ jenkins. qa.ubuntu. com/job/ friends- saucy-amd64- ci/5
http://
Executed test runs:
SUCCESS: http://
Click here to trigger a rebuild: s-jenkins: 8080/job/ friends- ci/48/rebuild
http://