Merge lp:~robru/friends/instagram into lp:friends

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
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 :

PASSED: Continuous integration, rev:201
http://jenkins.qa.ubuntu.com/job/friends-ci/48/
Executed test runs:
    SUCCESS: http://jenkins.qa.ubuntu.com/job/friends-saucy-amd64-ci/5

Click here to trigger a rebuild:
http://s-jenkins:8080/job/friends-ci/48/rebuild

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'))

Subscribers

People subscribed via source and target branches