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