Merge lp:~andrewsomething/friends/instagram into lp:~super-friends/friends/trunk-next

Proposed by Andrew Starr-Bochicchio
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
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.

Description of the change

Comment posting isn't completely tested yet. Instagram returns:

{'meta': {'error_message': 'Please visit http://bit.ly/instacomments for commenting access', 'error_type': 'APIError', 'code': 400}}

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.

To post a comment you must log in.
Revision history for this message
Robert Bruce Park (robru) wrote : Posted in a previous version of this proposal

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.

Revision history for this message
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).

Revision history for this message
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.

review: Needs Resubmitting
Revision history for this message
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://bugs.launchpad.net/ubuntu/+source/friends-app/+bug/1169679

Revision history for this message
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

Revision history for this message
Robert Bruce Park (robru) wrote :

I've done the work of rebasing this on trunk, and I've resubmitted it myself here: https://code.launchpad.net/~robru/friends/instagram/+merge/173277

Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'debian/changelog'
--- debian/changelog 2013-04-16 03:04:48 +0000
+++ debian/changelog 2013-04-16 19:33:26 +0000
@@ -1,7 +1,11 @@
1friends (0.2.0-0ubuntu1) UNRELEASED; urgency=low1friends (0.2.0-0ubuntu1) UNRELEASED; urgency=low
22
3 [ Robert Bruce Park ]
3 * Version bump for the next development series.4 * Version bump for the next development series.
45
6 [Andrew Starr-Bochicchio]
7 * Add support for Instagram (LP: #1167449).
8
5 -- Robert Bruce Park <robert.park@canonical.com> Tue, 19 Mar 2013 15:03:58 -07009 -- Robert Bruce Park <robert.park@canonical.com> Tue, 19 Mar 2013 15:03:58 -0700
610
7friends (0.1.3daily13.04.10.1-0ubuntu1) raring; urgency=low11friends (0.1.3daily13.04.10.1-0ubuntu1) raring; urgency=low
812
=== modified file 'debian/control'
--- debian/control 2013-02-22 00:06:42 +0000
+++ debian/control 2013-04-16 19:33:26 +0000
@@ -103,3 +103,9 @@
103Depends: friends, ${misc:Depends}, ${python3:Depends}103Depends: friends, ${misc:Depends}, ${python3:Depends}
104Description: Social integration with the desktop - Flickr104Description: Social integration with the desktop - Flickr
105 Provides social networking integration with the desktop105 Provides social networking integration with the desktop
106
107Package: friends-instagram
108Architecture: all
109Depends: friends, ${misc:Depends}, ${python3:Depends}
110Description: Social integration with the desktop - Instagram
111 Provides social networking integration with the desktop
106112
=== added file 'debian/friends-instagram.install'
--- debian/friends-instagram.install 1970-01-01 00:00:00 +0000
+++ debian/friends-instagram.install 2013-04-16 19:33:26 +0000
@@ -0,0 +1,1 @@
1usr/lib/python3/dist-packages/friends/protocols/instagram*
02
=== added file 'friends/protocols/instagram.py'
--- friends/protocols/instagram.py 1970-01-01 00:00:00 +0000
+++ friends/protocols/instagram.py 2013-04-16 19:33:26 +0000
@@ -0,0 +1,200 @@
1# friends-dispatcher -- send & receive messages from any social network
2# Copyright (C) 2013 Canonical Ltd
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""The Instagram protocol plugin."""
17
18
19__all__ = [
20 'Instagram',
21 ]
22
23import time
24import logging
25
26from friends.utils.avatar import Avatar
27from friends.utils.base import Base, feature
28from friends.utils.cache import JsonCache
29from friends.utils.http import Downloader, Uploader
30from friends.utils.time import parsetime, iso8601utc
31from friends.errors import FriendsError
32
33log = logging.getLogger(__name__)
34
35class Instagram(Base):
36 _api_base = 'https://api.instagram.com/v1/{endpoint}?access_token={token}'
37 def _whoami(self, authdata):
38 """Identify the authenticating user."""
39 url = self._api_base.format(
40 endpoint='users/self',
41 token=self._get_access_token())
42 result = Downloader(url).get_json()
43 self._account.user_id = result.get('data').get('id')
44 self._account.user_name = result.get('data').get('username')
45
46 def _publish_entry(self, entry, stream='messages'):
47 """Publish a single update into the Dee.SharedModel."""
48 message_id = entry.get('id')
49
50 if message_id is None:
51 # We can't do much with this entry.
52 return
53
54 person = entry.get('user')
55 nick = person.get('username')
56 name = person.get('full_name')
57 person_id = person.get('id')
58 message= '%s shared a picture on Instagram.' % nick
59 person_icon = Avatar.get_image(person.get('profile_picture'))
60 person_url = 'http://instagram.com/' + nick
61 picture = entry.get('images').get('thumbnail').get('url')
62 if entry.get('caption'):
63 desc = entry.get('caption').get('text', '')
64 else:
65 desc = ''
66 url = entry.get('link')
67 timestamp = entry.get('created_time')
68 if timestamp is not None:
69 timestamp = iso8601utc(parsetime(timestamp))
70 likes = entry.get('likes').get('count')
71 liked = entry.get('user_has_liked')
72 location = entry.get('location', {})
73 if location:
74 latitude = location.get('latitude', '')
75 longitude = location.get('longitude', '')
76 else:
77 latitude = 0
78 longitude = 0
79
80 args = dict(
81 message_id=message_id,
82 message=message,
83 stream=stream,
84 likes=likes,
85 sender_id=person_id,
86 sender=name,
87 sender_nick=nick,
88 url=person_url,
89 icon_uri=person_icon,
90 link_url=url,
91 link_picture=picture,
92 link_desc=desc,
93 timestamp=timestamp,
94 liked=liked,
95 latitude=latitude,
96 longitude=longitude
97 )
98
99 self._publish(**args)
100
101 # If there are any replies, publish them as well.
102 parent_id = message_id
103 for comment in entry.get('comments', {}).get('data', []):
104 if comment:
105 self._publish_comment(
106 comment, stream='reply_to/{}'.format(parent_id))
107 return args['url']
108
109 def _publish_comment(self, comment, stream):
110 message_id = comment.get('id')
111 if message_id is None:
112 return
113 message = comment.get('text', '')
114 person = comment.get('from', {})
115 sender_nick = person.get('username')
116 timestamp = comment.get('created_time')
117 if timestamp is not None:
118 timestamp = iso8601utc(parsetime(timestamp))
119 icon_uri = Avatar.get_image(person.get('profile_picture'))
120 sender_id = person.get('id')
121 sender = person.get('full_name')
122
123 args = dict(
124 stream=stream,
125 message_id=message_id,
126 message=message,
127 timestamp=timestamp,
128 sender_nick=sender_nick,
129 icon_uri=icon_uri,
130 sender_id=sender_id,
131 sender=sender,
132 )
133# print(args)
134 self._publish(**args)
135
136 @feature
137 def home(self):
138 """Gather and publish public timeline messages."""
139 url = self._api_base.format(
140 endpoint='users/self/feed',
141 token=self._get_access_token())
142 result = Downloader(url).get_json()
143 values = result.get('data', {})
144 for update in values:
145 self._publish_entry(update)
146
147 @feature
148 def receive(self):
149 """Gather and publish all incoming messages."""
150 self.home()
151 return self._get_n_rows()
152
153 def _send(self, obj_id, message, endpoint, stream='messages'):
154 token = self._get_access_token()
155
156 url = self._api_base.format(endpoint=endpoint, token=token)
157
158 result = Downloader(
159 url,
160 method='POST',
161 params=dict(access_token=token, text=message)).get_json()
162 new_id = result.get('id')
163 if new_id is None:
164 raise FriendsError('Failed sending to Instagram: {!r}'.format(result))
165 enpoint = 'media/{}/comments'.format(new_id)
166 url = self._api_base.format(endpoint=endpoint, token=token)
167 comment = Downloader(url, params=dict(access_token=token)).get_json()
168 return self._publish_entry(entry=comment, stream=stream)
169
170 @feature
171 def send_thread(self, obj_id, message):
172 """Write a comment on some existing picture.
173 """
174 return self._send(obj_id, message, 'media/{}/comments'.format(obj_id),
175 stream='reply_to/{}'.format(obj_id))
176
177 def _like(self, obj_id, endpoint, method):
178 token = self._get_access_token()
179 url = self._api_base.format(endpoint=endpoint, token=token)
180
181 if not Downloader(url, method=method,
182 params=dict(access_token=token)).get_json():
183 raise FriendsError('Failed to {} like {} on Instagram'.format(
184 method, obj_id))
185
186 @feature
187 def like(self, obj_id):
188 endpoint = 'media/{}/likes'.format(obj_id)
189 self._like(obj_id, endpoint, 'POST')
190 self._inc_cell(obj_id, 'likes')
191 self._set_cell(obj_id, 'liked', True)
192 return obj_id
193
194 @feature
195 def unlike(self, obj_id):
196 endpoint = 'media/{}/likes'.format(obj_id)
197 self._like(obj_id, endpoint, 'DELETE')
198 self._dec_cell(obj_id, 'likes')
199 self._set_cell(obj_id, 'liked', False)
200 return obj_id
0201
=== added file 'friends/tests/data/instagram-full.dat'
--- friends/tests/data/instagram-full.dat 1970-01-01 00:00:00 +0000
+++ friends/tests/data/instagram-full.dat 2013-04-16 19:33:26 +0000
@@ -0,0 +1,473 @@
1{
2 "pagination": {
3 "next_url": "https://api.instagram.com/v1/users/self/feed?access_token=4abc&max_id=431429392441037546_46931811",
4 "next_max_id": "431429392441037546_46931811"
5 },
6 "meta": {
7 "code": 200
8 },
9 "data": [
10 {
11 "attribution": null,
12 "tags": [],
13 "type": "image",
14 "location": null,
15 "comments": {
16 "count": 1,
17 "data": [
18 {
19 "created_time": "1365680633",
20 "text": "Wtf is that from?",
21 "from": {
22 "username": "ellllliottttt",
23 "profile_picture": "http://images.ak.instagram.com/profiles/profile_13811917_75sq_1322106636.jpg",
24 "id": "13811917",
25 "full_name": "Elliott Markowitz"
26 },
27 "id": "431682899841631793"
28 }
29 ]
30 },
31 "filter": "Normal",
32 "created_time": "1365655801",
33 "link": "http://instagram.com/p/X859raK8fx/",
34 "likes": {
35 "count": 8,
36 "data": [
37 {
38 "username": "noahvargass",
39 "profile_picture": "http://images.ak.instagram.com/profiles/profile_1037856_75sq_1354178959.jpg",
40 "id": "1037856",
41 "full_name": "Noah Vargas"
42 },
43 {
44 "username": "deniboring",
45 "profile_picture": "http://images.ak.instagram.com/profiles/profile_16828740_75sq_1345748703.jpg",
46 "id": "16828740",
47 "full_name": "deniboring"
48 },
49 {
50 "username": "gabriel_cordero",
51 "profile_picture": "http://images.ak.instagram.com/profiles/profile_185127499_75sq_1340358021.jpg",
52 "id": "185127499",
53 "full_name": "cheeseburger pocket"
54 },
55 {
56 "username": "inmyheadache",
57 "profile_picture": "http://images.ak.instagram.com/profiles/profile_22645548_75sq_1343529900.jpg",
58 "id": "22645548",
59 "full_name": "Tim F"
60 },
61 {
62 "username": "ellllliottttt",
63 "profile_picture": "http://images.ak.instagram.com/profiles/profile_13811917_75sq_1322106636.jpg",
64 "id": "13811917",
65 "full_name": "Elliott Markowitz"
66 },
67 {
68 "username": "eyesofsatan",
69 "profile_picture": "http://images.ak.instagram.com/profiles/profile_2526196_75sq_1352254863.jpg",
70 "id": "2526196",
71 "full_name": "Christopher Hansell"
72 },
73 {
74 "username": "lakeswimming",
75 "profile_picture": "http://images.ak.instagram.com/profiles/profile_3610143_75sq_1363648525.jpg",
76 "id": "3610143",
77 "full_name": "lakeswimming"
78 },
79 {
80 "username": "jtesnakis",
81 "profile_picture": "http://images.ak.instagram.com/profiles/profile_214679263_75sq_1346185198.jpg",
82 "id": "214679263",
83 "full_name": "Jonathan"
84 }
85 ]
86 },
87 "images": {
88 "low_resolution": {
89 "url": "http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_6.jpg",
90 "width": 306,
91 "height": 306
92 },
93 "thumbnail": {
94 "url": "http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_5.jpg",
95 "width": 150,
96 "height": 150
97 },
98 "standard_resolution": {
99 "url": "http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_7.jpg",
100 "width": 612,
101 "height": 612
102 }
103 },
104 "caption": null,
105 "user_has_liked": false,
106 "id": "431474591469914097_223207800",
107 "user": {
108 "username": "joshwolp",
109 "website": "",
110 "profile_picture": "http://images.ak.instagram.com/profiles/profile_223207800_75sq_1347753109.jpg",
111 "full_name": "Josh",
112 "bio": "",
113 "id": "223207800"
114 }
115 },
116 {
117 "attribution": null,
118 "tags": [
119 "nofilter"
120 ],
121 "type": "image",
122 "location": {
123 "latitude": 40.702485554,
124 "longitude": -73.929230548
125 },
126 "comments": {
127 "count": 3,
128 "data": [
129 {
130 "created_time": "1365654315",
131 "text": "I remember pushing that little guy of the swings a few times....",
132 "from": {
133 "username": "squidneylol",
134 "profile_picture": "http://images.ak.instagram.com/profiles/profile_5917696_75sq_1336705905.jpg",
135 "id": "5917696",
136 "full_name": "Syd"
137 },
138 "id": "431462132263145102"
139 },
140 {
141 "created_time": "1365665741",
142 "text": "Stop it!!!",
143 "from": {
144 "username": "nightruiner",
145 "profile_picture": "http://images.ak.instagram.com/profiles/profile_31239607_75sq_1358968326.jpg",
146 "id": "31239607",
147 "full_name": "meredith gaydosh"
148 },
149 "id": "431557973837598336"
150 },
151 {
152 "created_time": "1365691283",
153 "text": "You know I hate being held.",
154 "from": {
155 "username": "piratemaddi",
156 "profile_picture": "http://images.ak.instagram.com/profiles/profile_191388805_75sq_1341897870.jpg",
157 "id": "191388805",
158 "full_name": "piratemaddi"
159 },
160 "id": "431772240369143639"
161 }
162 ]
163 },
164 "filter": "Normal",
165 "created_time": "1365651440",
166 "link": "http://instagram.com/p/X8xpYwk82w/",
167 "likes": {
168 "count": 17,
169 "data": [
170 {
171 "username": "meaghanlou",
172 "profile_picture": "http://images.ak.instagram.com/profiles/profile_41007792_75sq_1334443633.jpg",
173 "id": "41007792",
174 "full_name": "meaghanlou"
175 },
176 {
177 "username": "shahwiththat",
178 "profile_picture": "http://images.ak.instagram.com/profiles/profile_40353923_75sq_1361330343.jpg",
179 "id": "40353923",
180 "full_name": "Soraya Shah"
181 },
182 {
183 "username": "marsyred",
184 "profile_picture": "http://images.ak.instagram.com/profiles/profile_216361031_75sq_1346524176.jpg",
185 "id": "216361031",
186 "full_name": "marsyred"
187 },
188 {
189 "username": "thewhitewizzard",
190 "profile_picture": "http://images.ak.instagram.com/profiles/profile_189027880_75sq_1341183134.jpg",
191 "id": "189027880",
192 "full_name": "Drew Mack"
193 },
194 {
195 "username": "juddymagee",
196 "profile_picture": "http://images.ak.instagram.com/profiles/profile_8993099_75sq_1315066258.jpg",
197 "id": "8993099",
198 "full_name": "juddymagee"
199 },
200 {
201 "username": "pixxamonster",
202 "profile_picture": "http://images.ak.instagram.com/profiles/profile_1672624_75sq_1364673157.jpg",
203 "id": "1672624",
204 "full_name": "Sonia 👓 + Dita 🐈"
205 },
206 {
207 "username": "tongue_in_cheek",
208 "profile_picture": "http://images.ak.instagram.com/profiles/profile_5710964_75sq_1345359909.jpg",
209 "id": "5710964",
210 "full_name": "merrily."
211 },
212 {
213 "username": "nightruiner",
214 "profile_picture": "http://images.ak.instagram.com/profiles/profile_31239607_75sq_1358968326.jpg",
215 "id": "31239607",
216 "full_name": "meredith gaydosh"
217 },
218 {
219 "username": "eliz_mortals",
220 "profile_picture": "http://images.ak.instagram.com/profiles/profile_36417303_75sq_1364179217.jpg",
221 "id": "36417303",
222 "full_name": "eliz_mortals"
223 },
224 {
225 "username": "caranicoletti",
226 "profile_picture": "http://images.ak.instagram.com/profiles/profile_14162814_75sq_1359848230.jpg",
227 "id": "14162814",
228 "full_name": "Cara Nicoletti"
229 }
230 ]
231 },
232 "images": {
233 "low_resolution": {
234 "url": "http://distilleryimage8.s3.amazonaws.com/1d99da5ca25911e2822f22000a9f09ca_6.jpg",
235 "width": 306,
236 "height": 306
237 },
238 "thumbnail": {
239 "url": "http://distilleryimage8.s3.amazonaws.com/1d99da5ca25911e2822f22000a9f09ca_5.jpg",
240 "width": 150,
241 "height": 150
242 },
243 "standard_resolution": {
244 "url": "http://distilleryimage8.s3.amazonaws.com/1d99da5ca25911e2822f22000a9f09ca_7.jpg",
245 "width": 612,
246 "height": 612
247 }
248 },
249 "caption": {
250 "created_time": "1365651465",
251 "text": "National siblings day @piratemaddi ? You mustn't've known. #nofilter",
252 "from": {
253 "username": "what_a_handsome_boy",
254 "profile_picture": "http://images.ak.instagram.com/profiles/profile_5891266_75sq_1345460082.jpg",
255 "id": "5891266",
256 "full_name": "Fence!"
257 },
258 "id": "431438224520630210"
259 },
260 "user_has_liked": false,
261 "id": "431438012683111856_5891266",
262 "user": {
263 "username": "what_a_handsome_boy",
264 "website": "",
265 "profile_picture": "http://images.ak.instagram.com/profiles/profile_5891266_75sq_1345460082.jpg",
266 "full_name": "Fence!",
267 "bio": "",
268 "id": "5891266"
269 }
270 },
271 {
272 "attribution": null,
273 "tags": [
274 "canada",
275 "iphone",
276 "punklife",
277 "acap",
278 "doom",
279 "crust"
280 ],
281 "type": "image",
282 "location": null,
283 "comments": {
284 "count": 7,
285 "data": [
286 {
287 "created_time": "1365651434",
288 "text": "I have not seen him for a minute, he is a great guy!",
289 "from": {
290 "username": "rocktoberblood",
291 "profile_picture": "http://images.ak.instagram.com/profiles/profile_210506849_75sq_1355190976.jpg",
292 "id": "210506849",
293 "full_name": "Nate Phillips"
294 },
295 "id": "431437959929388541"
296 },
297 {
298 "created_time": "1365652422",
299 "text": "Saw him puke in the elevator at the Hilton when Doom played chaos.",
300 "from": {
301 "username": "occult_obsession",
302 "profile_picture": "http://images.ak.instagram.com/profiles/profile_6080835_75sq_1364154159.jpg",
303 "id": "6080835",
304 "full_name": "Evan Vellela"
305 },
306 "id": "431446251162427175"
307 },
308 {
309 "created_time": "1365652900",
310 "text": "Lol ^ who DIDN'T do this at chaos",
311 "from": {
312 "username": "laurababbili",
313 "profile_picture": "http://images.ak.instagram.com/profiles/profile_1238692_75sq_1361065720.jpg",
314 "id": "1238692",
315 "full_name": "Laura Babbili"
316 },
317 "id": "431450261906907041"
318 },
319 {
320 "created_time": "1365655310",
321 "text": "I tried to kiss the singer of cocksparrer in that elevator.",
322 "from": {
323 "username": "gregdaly",
324 "profile_picture": "http://images.ak.instagram.com/profiles/profile_31133446_75sq_1333488205.jpg",
325 "id": "31133446",
326 "full_name": "greg daly"
327 },
328 "id": "431470473175752176"
329 },
330 {
331 "created_time": "1365655687",
332 "text": "Leant that dude a 900 Marshall a time ago! Love that dude!!",
333 "from": {
334 "username": "nickatnightengale",
335 "profile_picture": "http://images.ak.instagram.com/profiles/profile_29366372_75sq_1354241971.jpg",
336 "id": "29366372",
337 "full_name": "Nick Poulos"
338 },
339 "id": "431473639539727956"
340 },
341 {
342 "created_time": "1365658654",
343 "text": "Hahaha!!",
344 "from": {
345 "username": "jeremyhush",
346 "profile_picture": "http://images.ak.instagram.com/profiles/profile_43750226_75sq_1335024423.jpg",
347 "id": "43750226",
348 "full_name": "Jeremy Hush"
349 },
350 "id": "431498525729480083"
351 },
352 {
353 "created_time": "1365685226",
354 "text": "There's an app for that",
355 "from": {
356 "username": "liam_wilson",
357 "profile_picture": "http://images.ak.instagram.com/profiles/profile_4357248_75sq_1327009309.jpg",
358 "id": "4357248",
359 "full_name": "Liam Wilson"
360 },
361 "id": "431721432141388885"
362 }
363 ]
364 },
365 "filter": "X-Pro II",
366 "created_time": "1365650801",
367 "link": "http://instagram.com/p/X8wbTQtd3Y/",
368 "likes": {
369 "count": 36,
370 "data": [
371 {
372 "username": "fatbenatar",
373 "profile_picture": "http://images.ak.instagram.com/profiles/profile_280136224_75sq_1357073366.jpg",
374 "id": "280136224",
375 "full_name": "China Oxendine"
376 },
377 {
378 "username": "tejleo",
379 "profile_picture": "http://images.ak.instagram.com/profiles/profile_10403332_75sq_1354058520.jpg",
380 "id": "10403332",
381 "full_name": "tejleo"
382 },
383 {
384 "username": "tonyromeo13",
385 "profile_picture": "http://images.ak.instagram.com/profiles/profile_255933637_75sq_1360272947.jpg",
386 "id": "255933637",
387 "full_name": "Tony Romeo"
388 },
389 {
390 "username": "libraryrat4",
391 "profile_picture": "http://images.ak.instagram.com/profiles/profile_17715046_75sq_1325381597.jpg",
392 "id": "17715046",
393 "full_name": "libraryrat4"
394 },
395 {
396 "username": "boredwithapathy",
397 "profile_picture": "http://images.ak.instagram.com/profiles/profile_174679368_75sq_1338500455.jpg",
398 "id": "174679368",
399 "full_name": "Lisa McCarthy"
400 },
401 {
402 "username": "justinpittney",
403 "profile_picture": "http://images.ak.instagram.com/profiles/profile_10976940_75sq_1361306615.jpg",
404 "id": "10976940",
405 "full_name": "Justin Pittney"
406 },
407 {
408 "username": "nuclearhell",
409 "profile_picture": "http://images.ak.instagram.com/profiles/profile_212702314_75sq_1361642319.jpg",
410 "id": "212702314",
411 "full_name": "Rachel Whittaker"
412 },
413 {
414 "username": "gregelk",
415 "profile_picture": "http://images.ak.instagram.com/profiles/profile_240084204_75sq_1359223473.jpg",
416 "id": "240084204",
417 "full_name": "Greg Elk"
418 },
419 {
420 "username": "bradhasher",
421 "profile_picture": "http://images.ak.instagram.com/profiles/profile_283278325_75sq_1357514041.jpg",
422 "id": "283278325",
423 "full_name": "Brett Ellingson"
424 },
425 {
426 "username": "zaimlmzim",
427 "profile_picture": "http://images.ak.instagram.com/profiles/profile_180060277_75sq_1363118292.jpg",
428 "id": "180060277",
429 "full_name": "zaimlmzim"
430 }
431 ]
432 },
433 "images": {
434 "low_resolution": {
435 "url": "http://distilleryimage4.s3.amazonaws.com/a051f882a25711e282e122000a1f9aae_6.jpg",
436 "width": 306,
437 "height": 306
438 },
439 "thumbnail": {
440 "url": "http://distilleryimage4.s3.amazonaws.com/a051f882a25711e282e122000a1f9aae_5.jpg",
441 "width": 150,
442 "height": 150
443 },
444 "standard_resolution": {
445 "url": "http://distilleryimage4.s3.amazonaws.com/a051f882a25711e282e122000a1f9aae_7.jpg",
446 "width": 612,
447 "height": 612
448 }
449 },
450 "caption": {
451 "created_time": "1365650915",
452 "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",
453 "from": {
454 "username": "gregdaly",
455 "profile_picture": "http://images.ak.instagram.com/profiles/profile_31133446_75sq_1333488205.jpg",
456 "id": "31133446",
457 "full_name": "greg daly"
458 },
459 "id": "431433605218426202"
460 },
461 "user_has_liked": false,
462 "id": "431432646660578776_31133446",
463 "user": {
464 "username": "gregdaly",
465 "website": "",
466 "profile_picture": "http://images.ak.instagram.com/profiles/profile_31133446_75sq_1333488205.jpg",
467 "full_name": "greg daly",
468 "bio": "",
469 "id": "31133446"
470 }
471 }
472 ]
473}
0474
=== added file 'friends/tests/data/instagram-login.dat'
--- friends/tests/data/instagram-login.dat 1970-01-01 00:00:00 +0000
+++ friends/tests/data/instagram-login.dat 2013-04-16 19:33:26 +0000
@@ -0,0 +1,18 @@
1{
2 "meta": {
3 "code": 200
4 },
5 "data": {
6 "username": "bpersons",
7 "bio": "",
8 "website": "",
9 "profile_picture": "http://images.ak.instagram.com/profiles/anonymousUser.jpg",
10 "full_name": "Bart Person",
11 "counts": {
12 "media": 174,
13 "followed_by": 100,
14 "follows": 503
15 },
16 "id": "801"
17 }
18}
019
=== added file 'friends/tests/test_instagram.py'
--- friends/tests/test_instagram.py 1970-01-01 00:00:00 +0000
+++ friends/tests/test_instagram.py 2013-04-16 19:33:26 +0000
@@ -0,0 +1,234 @@
1# friends-dispatcher -- send & receive messages from any social network
2# Copyright (C) 2012 Canonical Ltd
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""Test the Instagram plugin."""
17
18
19__all__ = [
20 'TestInstagram',
21 ]
22
23
24import os
25import tempfile
26import unittest
27import shutil
28
29from gi.repository import GLib
30from pkg_resources import resource_filename
31
32from friends.protocols.instagram import Instagram
33from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock
34from friends.tests.mocks import TestModel, mock
35from friends.tests.mocks import EDSBookClientMock, EDSSource, EDSRegistry
36from friends.errors import ContactsError, FriendsError, AuthorizationError
37from friends.utils.cache import JsonCache
38
39
40@mock.patch('friends.utils.http._soup', mock.Mock())
41@mock.patch('friends.utils.base.notify', mock.Mock())
42class TestInstagram(unittest.TestCase):
43 """Test the Instagram API."""
44
45 def setUp(self):
46 self._temp_cache = tempfile.mkdtemp()
47 self._root = JsonCache._root = os.path.join(
48 self._temp_cache, '{}.json')
49 self.account = FakeAccount()
50 self.protocol = Instagram(self.account)
51 self.protocol.source_registry = EDSRegistry()
52
53 def tearDown(self):
54 TestModel.clear()
55 shutil.rmtree(self._temp_cache)
56
57 def test_features(self):
58 # The set of public features.
59 self.assertEqual(Instagram.get_features(),
60 ['home', 'like', 'receive', 'send_thread', 'unlike'])
61
62 @mock.patch('friends.utils.authentication.manager')
63 @mock.patch('friends.utils.authentication.Accounts')
64 @mock.patch('friends.utils.authentication.Authentication.__init__',
65 return_value=None)
66 @mock.patch('friends.utils.authentication.Authentication.login',
67 return_value=dict(AccessToken='abc'))
68 @mock.patch('friends.utils.http.Soup.Message',
69 FakeSoupMessage('friends.tests.data', 'instagram-login.dat'))
70 def test_successful_login(self, *mock):
71 # Test that a successful response from instagram.com returning
72 # the user's data, sets up the account dict correctly.
73 self.protocol._login()
74 self.assertEqual(self.account.access_token, 'abc')
75 self.assertEqual(self.account.user_name, 'bpersons')
76 self.assertEqual(self.account.user_id, '801')
77
78 @mock.patch('friends.utils.authentication.manager')
79 @mock.patch('friends.utils.authentication.Accounts')
80 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
81 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')
82 def test_login_unsuccessful_authentication(self, *mock):
83 # The user is not already logged in, but the act of logging in fails.
84 self.assertRaises(AuthorizationError, self.protocol._login)
85 self.assertIsNone(self.account.access_token)
86 self.assertIsNone(self.account.user_name)
87
88 @mock.patch('friends.utils.authentication.manager')
89 @mock.patch('friends.utils.authentication.Accounts')
90 @mock.patch('friends.utils.authentication.Authentication.login',
91 return_value=dict(AccessToken='abc'))
92 @mock.patch('friends.protocols.instagram.Downloader.get_json',
93 return_value=dict(
94 error=dict(message='Bad access token',
95 type='OAuthException',
96 code=190)))
97 def test_error_response(self, *mocks):
98 with LogMock('friends.utils.base',
99 'friends.protocols.instagram') as log_mock:
100 self.assertRaises(
101 FriendsError,
102 self.protocol.home,
103 )
104 contents = log_mock.empty(trim=False)
105 self.assertEqual(contents, 'Logging in to Instagram\n')
106
107 @mock.patch('friends.utils.http.Soup.Message',
108 FakeSoupMessage('friends.tests.data', 'instagram-full.dat'))
109 @mock.patch('friends.utils.base.Model', TestModel)
110 @mock.patch('friends.protocols.instagram.Instagram._login',
111 return_value=True)
112 def test_receive(self, *mocks):
113 # Receive the feed for a user.
114 self.maxDiff = None
115 self.account.access_token = 'abc'
116 self.assertEqual(self.protocol.receive(), 14)
117 self.assertEqual(TestModel.get_n_rows(), 14)
118 self.assertEqual(list(TestModel.get_row(0)), [
119 'instagram',
120 88,
121 '431474591469914097_223207800',
122 'messages',
123 'Josh',
124 '223207800',
125 'joshwolp',
126 False,
127 '2013-04-11T04:50:01Z',
128 'joshwolp shared a picture on Instagram.',
129 GLib.get_user_cache_dir() +
130 '/friends/avatars/ca55b643e7b440762c7c6292399eed6542a84b90',
131 'http://instagram.com/joshwolp',
132 8,
133 False,
134 'http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_5.jpg',
135 '',
136 'http://instagram.com/p/X859raK8fx/',
137 '',
138 '',
139 '',
140 '',
141 0.0,
142 0.0,
143 ])
144 self.assertEqual(list(TestModel.get_row(3)), [
145 'instagram',
146 88,
147 '431462132263145102',
148 'reply_to/431438012683111856_5891266',
149 'Syd',
150 '5917696',
151 'squidneylol',
152 False,
153 '2013-04-11T04:25:15Z',
154 'I remember pushing that little guy of the swings a few times....',
155 GLib.get_user_cache_dir() +
156 '/friends/avatars/e61c8d91e37fec3e1dec9325fa4edc52ebeb96bb',
157 '',
158 0,
159 False,
160 '',
161 '',
162 '',
163 '',
164 '',
165 '',
166 '',
167 0.0,
168 0.0,
169 ])
170
171 @mock.patch('friends.protocols.instagram.Downloader')
172 def test_send_thread(self, dload):
173 dload().get_json.return_value = dict(id='comment_id')
174 token = self.protocol._get_access_token = mock.Mock(
175 return_value='abc')
176 publish = self.protocol._publish_entry = mock.Mock(
177 return_value='http://instagram.com/p/post_id')
178
179 self.assertEqual(
180 self.protocol.send_thread('post_id', 'Some witty response!'),
181 'http://instagram.com/p/post_id')
182 token.assert_called_once_with()
183 publish.assert_called_with(entry={'id': 'comment_id'},
184 stream='reply_to/post_id')
185 self.assertEqual(
186 dload.mock_calls,
187 [mock.call(),
188 mock.call(
189 'https://api.instagram.com/v1/media/post_id/comments?access_token=abc',
190 method='POST',
191 params=dict(
192 access_token='abc',
193 text='Some witty response!')),
194 mock.call().get_json(),
195 mock.call('https://api.instagram.com/v1/media/post_id/comments?access_token=abc',
196 params=dict(access_token='abc')),
197 mock.call().get_json(),
198 ])
199
200 @mock.patch('friends.protocols.instagram.Downloader')
201 def test_like(self, dload):
202 dload().get_json.return_value = True
203 token = self.protocol._get_access_token = mock.Mock(
204 return_value='insta')
205 inc_cell = self.protocol._inc_cell = mock.Mock()
206 set_cell = self.protocol._set_cell = mock.Mock()
207
208 self.assertEqual(self.protocol.like('post_id'), 'post_id')
209
210 inc_cell.assert_called_once_with('post_id', 'likes')
211 set_cell.assert_called_once_with('post_id', 'liked', True)
212 token.assert_called_once_with()
213 dload.assert_called_with(
214 'https://api.instagram.com/v1/media/post_id/likes?access_token=insta',
215 method='POST',
216 params=dict(access_token='insta'))
217
218 @mock.patch('friends.protocols.instagram.Downloader')
219 def test_unlike(self, dload):
220 dload.get_json.return_value = True
221 token = self.protocol._get_access_token = mock.Mock(
222 return_value='insta')
223 dec_cell = self.protocol._dec_cell = mock.Mock()
224 set_cell = self.protocol._set_cell = mock.Mock()
225
226 self.assertEqual(self.protocol.unlike('post_id'), 'post_id')
227
228 dec_cell.assert_called_once_with('post_id', 'likes')
229 set_cell.assert_called_once_with('post_id', 'liked', False)
230 token.assert_called_once_with()
231 dload.assert_called_once_with(
232 'https://api.instagram.com/v1/media/post_id/likes?access_token=insta',
233 method='DELETE',
234 params=dict(access_token='insta'))

Subscribers

People subscribed via source and target branches