Merge lp:~paulliu/friends/add-twitter-contacts into lp:friends

Proposed by Ying-Chun Liu
Status: Merged
Merged at revision: 79
Proposed branch: lp:~paulliu/friends/add-twitter-contacts
Merge into: lp:friends
Diff against target: 314 lines (+174/-53)
6 files modified
friends/protocols/facebook.py (+13/-40)
friends/protocols/twitter.py (+58/-0)
friends/tests/test_dispatcher.py (+6/-6)
friends/tests/test_facebook.py (+22/-7)
friends/tests/test_twitter.py (+48/-0)
friends/utils/base.py (+27/-0)
To merge this branch: bzr merge lp:~paulliu/friends/add-twitter-contacts
Reviewer Review Type Date Requested Status
Robert Bruce Park Approve
Review via email: mp+135890@code.launchpad.net
To post a comment you must log in.
81. By Conor Curran on 2012-11-23

populate web-service-addresses with appropriate key name and put the id in there also

82. By Conor Curran on 2012-11-23

extend and fix tests

83. By Conor Curran on 2012-11-23

fix twitter tests

84. By Conor Curran on 2012-11-23

fix typos

Robert Bruce Park (robru) wrote :

This looks really good! I'm so glad to see the EBook import dropped from facebook.py, thank you *soooo* much! I'll just do a few minor whitespace cleanups and then this looks pretty much good to go here.

I just have one question: Is this expected to work with identica as well? I see in the get_feature tests you've changed it to say that you are expecting identica to have the 'contacts' feature, but in one of the other methods I saw you reference the 'id_str' attribute, which as far as I'm aware is only supplied by twitter, not by identica.

If that's the *only* issue, then "userdata['id_str']" can be replaced with "str(userdata['id'])" and you've bought yourself Identica support, but if that *doesn't* work, then it's better to override Identica.contacts to raise NotImplementedError so that we don't erroneously advertise that we support a feature that doesn't work ;-)

I'll play with this a bit and see if I can't figure that out on my own. I'd like to merge it today ;-)

Thanks again!

review: Needs Information
Robert Bruce Park (robru) wrote :

Turns out there were a couple other gotchas standing in the way of Identica support, but I fixed them all without breaking Twitter, so it's all looking very good now ;-)

Thanks again!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'friends/protocols/facebook.py'
2--- friends/protocols/facebook.py 2012-11-16 02:41:14 +0000
3+++ friends/protocols/facebook.py 2012-11-23 15:37:19 +0000
4@@ -25,7 +25,6 @@
5 import logging
6
7 from datetime import datetime, timedelta
8-from gi.repository import EBook
9
10 from friends.utils.avatar import Avatar
11 from friends.utils.base import Base, feature
12@@ -320,52 +319,26 @@
13
14 def _create_contact(self, contact_json):
15 """Build a VCard based on a dict representation of a contact."""
16+
17 user_id = contact_json.get('id')
18 user_fullname = contact_json.get('name')
19 user_nickname = contact_json.get('username')
20 user_link = contact_json.get('link')
21 gender = contact_json.get('gender')
22
23- vcard = EBook.VCard.new()
24-
25- vcafid = EBook.VCardAttribute.new(
26- 'social-networking-attributes', 'facebook-id')
27- vcafid.add_value(user_id)
28- vcafn = EBook.VCardAttribute.new(
29- 'social-networking-attributes', 'facebook-name')
30- vcafn.add_value(user_fullname)
31- vcauri = EBook.VCardAttribute.new(
32- 'social-networking-attributes', 'X-URIS')
33- vcauri.add_value(user_link)
34-
35- vcaws = EBook.VCardAttribute.new(
36- 'social-networking-attributes', 'X-FOLKS-WEB-SERVICES-IDS')
37- vcaws_param = EBook.VCardAttributeParam.new('jabber')
38- vcaws_param.add_value('-{}@chat.facebook.com'.format(user_id))
39- vcaws.add_param(vcaws_param)
40-
41- vcaws_param_2 = EBook.VCardAttributeParam.new('alias')
42- vcaws_param_2.add_value(user_fullname)
43- vcaws.add_param(vcaws_param_2)
44-
45- vcard.add_attribute(vcaws)
46-
47+ attrs = {}
48+ attrs['facebook-id'] = user_id
49+ attrs['facebook-name'] = user_fullname
50+ attrs['X-URIS'] = user_link
51+ attrs['X-FOLKS-WEB-SERVICES-IDS'] = {
52+ 'jabber':'-{}@chat.facebook.com'.format(user_id),
53+ 'remote-full-name':user_fullname,
54+ 'facebook-id': user_id}
55 if gender is not None:
56- vcag = EBook.VCardAttribute.new(
57- 'social-networking-attributes', 'X-GENDER')
58- vcag.add_value(gender)
59- vcard.add_attribute(vcag)
60-
61- vcard.add_attribute(vcafn)
62- vcard.add_attribute(vcauri)
63- vcard.add_attribute(vcafid)
64-
65- contact = EBook.Contact.new_from_vcard(
66- vcard.to_string(EBook.VCardFormat(1)))
67- contact.set_property('full-name', user_fullname)
68-
69- if user_nickname is not None:
70- contact.set_property('nickname', user_nickname)
71+ attrs['X-GENDER'] = gender
72+
73+ contact = Base._create_contact(self, user_fullname,
74+ user_nickname, attrs)
75
76 log.debug('Creating new contact for {}'.format(user_fullname))
77 return contact
78
79=== modified file 'friends/protocols/twitter.py'
80--- friends/protocols/twitter.py 2012-11-16 02:06:40 +0000
81+++ friends/protocols/twitter.py 2012-11-23 15:37:19 +0000
82@@ -34,6 +34,7 @@
83 from friends.utils.http import BaseRateLimiter, Downloader
84 from friends.utils.time import parsetime, iso8601utc
85
86+TWITTER_ADDRESS_BOOK = 'friends-twitter-contacts'
87
88 log = logging.getLogger(__name__)
89
90@@ -297,6 +298,63 @@
91 for tweet in response.get(self._search_result_key, []):
92 self._publish_tweet(tweet, stream='search/{}'.format(query))
93
94+# https://dev.twitter.com/docs/api/1.1/get/friends/ids
95+ def getfriendsids(self):
96+ """List friend ID."""
97+ url = self._api_base.format(endpoint="friends/ids")
98+ response = self._get_url(url)
99+
100+ return response["ids"]
101+
102+# https://dev.twitter.com/docs/api/1.1/get/users/show
103+ def showuser(self, uid):
104+ """Show users Data."""
105+ url = self._api_base.format(endpoint="users/show") + "?user_id={}".format(uid)
106+ response = self._get_url(url)
107+ return response
108+
109+ def _create_contact(self, userdata):
110+ """Build a VCard based on a dict representation of a contact."""
111+
112+ user_fullname = userdata['name']
113+ user_nickname = userdata['screen_name']
114+
115+ attrs = {}
116+ attrs['twitter-id'] = userdata['id_str']
117+ attrs['twitter-name'] = user_fullname
118+ attrs['X-URIS'] = 'https://twitter.com/{}'.format(user_nickname)
119+ attrs['X-FOLKS-WEB-SERVICES-IDS'] = {
120+ 'remote-full-name':user_fullname, 'twitter-id': userdata['id_str'] }
121+
122+ contact = Base._create_contact(self,
123+ user_fullname, user_nickname, attrs)
124+
125+ log.debug('Creating new contact for {}'.format(user_fullname))
126+ return contact
127+
128+ @feature
129+ def contacts(self):
130+ contacts = self.getfriendsids()
131+ log.debug('Size of the contacts returned {}'.format(len(contacts)))
132+ source = self._get_eds_source(TWITTER_ADDRESS_BOOK)
133+ if source is None:
134+ source = self._create_eds_source(TWITTER_ADDRESS_BOOK)
135+
136+ for contact in contacts:
137+ twitterid = str(contact)
138+ if self._previously_stored_contact(source,
139+ 'twitter-id', twitterid):
140+ continue
141+ full_contact = self.showuser(twitterid)
142+ eds_contact = self._create_contact(full_contact)
143+ if not self._push_to_eds(TWITTER_ADDRESS_BOOK, eds_contact):
144+ log.error(
145+ 'Unable to save twitter contact {}'.format(
146+ contact['name']))
147+
148+ def delete_contacts(self):
149+ source = self._get_eds_source(TWITTER_ADDRESS_BOOK)
150+ return self._delete_service_contacts(source)
151
152 class RateLimiter(BaseRateLimiter):
153 """Twitter rate limiter."""
154
155=== modified file 'friends/tests/test_dispatcher.py'
156--- friends/tests/test_dispatcher.py 2012-11-16 18:42:13 +0000
157+++ friends/tests/test_dispatcher.py 2012-11-23 15:37:19 +0000
158@@ -169,13 +169,13 @@
159 'search', 'send', 'send_thread', 'unlike', 'upload',
160 'wall'])
161 self.assertEqual(json.loads(self.dispatcher.GetFeatures('twitter')),
162- ['delete', 'follow', 'home', 'like', 'list', 'lists',
163- 'mentions', 'private', 'receive', 'retweet',
164- 'search', 'send', 'send_private', 'send_thread',
165- 'tag', 'unfollow', 'unlike', 'user'])
166+ ['contacts', 'delete', 'follow', 'home', 'like',
167+ 'list', 'lists', 'mentions', 'private', 'receive',
168+ 'retweet', 'search', 'send', 'send_private',
169+ 'send_thread', 'tag', 'unfollow', 'unlike', 'user'])
170 self.assertEqual(json.loads(self.dispatcher.GetFeatures('identica')),
171- ['delete', 'follow', 'home', 'mentions', 'private',
172- 'receive', 'retweet', 'search', 'send',
173+ ['contacts', 'delete', 'follow', 'home', 'mentions',
174+ 'private', 'receive', 'retweet', 'search', 'send',
175 'send_private', 'send_thread', 'unfollow', 'user'])
176 self.assertEqual(json.loads(self.dispatcher.GetFeatures('flickr')),
177 ['receive'])
178
179=== modified file 'friends/tests/test_facebook.py'
180--- friends/tests/test_facebook.py 2012-11-16 02:41:14 +0000
181+++ friends/tests/test_facebook.py 2012-11-23 15:37:19 +0000
182@@ -451,13 +451,28 @@
183 self.assertEqual(facebook_name_attr.get_value(), 'Lucy Baron')
184 web_service_addrs = eds_contact.get_attribute('X-FOLKS-WEB-SERVICES-IDS')
185 params= web_service_addrs.get_params()
186- self.assertEqual(len(params), 2)
187- self.assertEqual(params[0].get_name(), 'alias')
188- self.assertEqual(len(params[0].get_values()), 1)
189- self.assertEqual(params[0].get_values()[0], 'Lucy Baron')
190- self.assertEqual(params[1].get_name(), 'jabber')
191- self.assertEqual(len(params[1].get_values()), 1)
192- self.assertEqual(params[1].get_values()[0], '-555555555@chat.facebook.com')
193+
194+ self.assertEqual(len(params), 3)
195+
196+ test_jabber = False
197+ test_remote_name = False
198+ test_facebook_id = False
199+
200+ for p in params:
201+ if p.get_name() == 'jabber':
202+ self.assertEqual(len(p.get_values()), 1)
203+ self.assertEqual(p.get_values()[0], '-555555555@chat.facebook.com')
204+ test_jabber = True
205+ if p.get_name() == 'remote-full-name':
206+ self.assertEqual(len(p.get_values()), 1)
207+ self.assertEqual(p.get_values()[0], 'Lucy Baron')
208+ test_remote_name = True
209+ if p.get_name() == 'facebook-id':
210+ self.assertEqual(len(p.get_values()), 1)
211+ self.assertEqual(p.get_values()[0], '555555555')
212+ test_facebook_id = True
213+ # Finally test to ensure all key value pairs were tested
214+ self.assertTrue(test_jabber and test_remote_name and test_facebook_id)
215
216 @mock.patch('friends.utils.base.Base._get_eds_source',
217 return_value=True)
218
219=== modified file 'friends/tests/test_twitter.py'
220--- friends/tests/test_twitter.py 2012-11-16 02:06:40 +0000
221+++ friends/tests/test_twitter.py 2012-11-23 15:37:19 +0000
222@@ -416,6 +416,54 @@
223 get_url.assert_called_with(
224 'https://api.twitter.com/1.1/search/tweets.json?q=hello')
225
226+ def test_getfriendsids(self):
227+ get_url = self.protocol._get_url = mock.Mock(return_value={"ids":[1,2,3]})
228+ ids = self.protocol.getfriendsids()
229+
230+ get_url.assert_called_with(
231+ 'https://api.twitter.com/1.1/friends/ids.json'
232+ )
233+ self.assertEqual(ids, [1,2,3])
234+
235+ def test_showuser(self):
236+ get_url = self.protocol._get_url = mock.Mock(return_value={"name":"Alice"})
237+ userdata = self.protocol.showuser(1)
238+
239+ get_url.assert_called_with(
240+ 'https://api.twitter.com/1.1/users/show.json?user_id=1'
241+ )
242+ self.assertEqual(userdata, {"name":"Alice"})
243+
244+ def test_create_contact(self, *mocks):
245+ # Receive the users friends.
246+ bare_contact = {'name': 'Alice Bob',
247+ 'screen_name': 'alice_bob',
248+ 'id_str': '13579'}
249+
250+ eds_contact = self.protocol._create_contact(bare_contact)
251+ twitter_id_attr = eds_contact.get_attribute('twitter-id')
252+ self.assertEqual(twitter_id_attr.get_value(), '13579')
253+ twitter_name_attr = eds_contact.get_attribute('twitter-name')
254+ self.assertEqual(twitter_name_attr.get_value(), 'Alice Bob')
255+ web_service_addrs = eds_contact.get_attribute('X-FOLKS-WEB-SERVICES-IDS')
256+ params= web_service_addrs.get_params()
257+ self.assertEqual(len(params), 2)
258+
259+ test_remote_name = False
260+ test_twitter_id = False
261+
262+ for p in params:
263+ if p.get_name() == 'remote-full-name':
264+ self.assertEqual(len(p.get_values()), 1)
265+ self.assertEqual(p.get_values()[0], 'Alice Bob')
266+ test_remote_name = True
267+ if p.get_name() == 'twitter-id':
268+ self.assertEqual(len(p.get_values()), 1)
269+ self.assertEqual(p.get_values()[0], '13579')
270+ test_twitter_id = True
271+
272+ self.assertTrue(test_remote_name and test_twitter_id)
273+
274 @mock.patch('friends.protocols.twitter.time.sleep')
275 def test_rate_limiter_first_time(self, sleep):
276 # The first time we see a URL, there is no rate limiting.
277
278=== modified file 'friends/utils/base.py'
279--- friends/utils/base.py 2012-11-17 16:34:48 +0000
280+++ friends/utils/base.py 2012-11-23 15:37:19 +0000
281@@ -457,6 +457,33 @@
282 client.remove_contact_sync(contact, None)
283 return True
284
285+ def _create_contact(self, user_fullname, user_nickname,
286+ social_network_attrs):
287+ """Build a VCard based on a dict representation of a contact."""
288+
289+ vcard = EBook.VCard.new()
290+ info = social_network_attrs
291+
292+ for i in info:
293+ attr = EBook.VCardAttribute.new('social-networking-attributes', i)
294+ if type(info[i]) == type(dict()):
295+ for j in info[i]:
296+ param = EBook.VCardAttributeParam.new(j)
297+ param.add_value(info[i][j])
298+ attr.add_param(param);
299+ else:
300+ attr.add_value(info[i])
301+ vcard.add_attribute(attr)
302+
303+ contact = EBook.Contact.new_from_vcard(
304+ vcard.to_string(EBook.VCardFormat(1)))
305+ contact.set_property('full-name', user_fullname)
306+ if user_nickname is not None:
307+ contact.set_property('nickname', user_nickname)
308+
309+ log.debug('Creating new contact for {}'.format(user_fullname))
310+ return contact
311+
312 @classmethod
313 def get_features(cls):
314 """Report what public operations we expose over DBus."""

Subscribers

People subscribed via source and target branches

to all changes: