Merge lp:~kai-mast/friends/links into lp:friends

Proposed by Kai Mast
Status: Merged
Merged at revision: 249
Proposed branch: lp:~kai-mast/friends/links
Merge into: lp:friends
Diff against target: 437 lines (+361/-17)
4 files modified
friends/protocols/twitter.py (+48/-15)
friends/tests/data/twitter-hashtags.dat (+94/-0)
friends/tests/data/twitter-multiple-links.dat (+144/-0)
friends/tests/test_twitter.py (+75/-2)
To merge this branch: bzr merge lp:~kai-mast/friends/links
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Needs Fixing
Robert Bruce Park Approve
Review via email: mp+202954@code.launchpad.net

Description of the change

This branch likifies mentions and hashtags. Hashtags open a link with the search in a browser. Friends has support for searches but afaik there is no way to add links to them. So this is the best we can do right now. Imo that makes tweets (especially retweets) much more interakctive.

I also fixed a problem when there are multiple links in a tweet and also added a test so this stays fixed.

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

Wow, congrats on the test coverage, that's really impressive ;-)

One thing that I'm a little bit confused about. You named a variable "urls_sorted" but I don't actually see any sorting going on there. Then you go on to use a concept of 'offset' in order to keep track of where each link should go, and I find it a bit sloppy.

What I'd like to see instead, is just iterate over urls_sorted in a sorted way, with descending values of 'begin' (eg, linkify the last URL first and progress backwards through the string). That way there's never an offset to have to worry about as the change in string length does not affect links that appear prior to the change being made.

Another (small) problem is that you used iter() in a useless way. Friends is python3-only, so dict.items() always returns an iter and your extra call to iter() doesn't do anything.

So instead it should probably look like this:

    for key, url in sorted(urls_sorted.items(), reverse=True):

And then...

    if content:
        message = ''.join([message[:begin], content, message[end:]])

Also, I'm slightly annoyed by the inconsistency between the _linkify_mention function which is nearly identical to other linkification techniques that are inlined in the larger function. I'm sure there can be a general purpose _linkify method that can be used to linkify all kinds of links, in a much more consistent and space-efficient manner (ie, fewer lines of code).

review: Needs Fixing
Revision history for this message
Kai Mast (kai-mast) wrote :

Oh okay I thought dictionaries where sorted by default (I am so used to C++ ;) )

I haven't even thought of going through the message in reverse to keep track off the offset. Good point.

Revision history for this message
Kai Mast (kai-mast) wrote :

Better?

I am not sure how to use just one linkify function because some have absolute addresses and some don't, so I now have linkify and linkify_mention, where the latter calls the first.

I also made the links use https which should be used whenever possible in my opinion.

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

This is a ton better, thank you!

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Autolanding.
No commit message was specified in the merge proposal. Hit 'Add commit message' on the merge proposal web page or follow the link below. You can approve the merge proposal yourself to rerun.
https://code.launchpad.net/~kai-mast/friends/links/+merge/202954/+edit-commit-message

review: Needs Fixing (continuous-integration)
Revision history for this message
Robert Bruce Park (robru) wrote :

One thing to watch out for is that you have a ton of trailing whitespace all over. I don't like to see that in my code ;-)

Don't worry, I'm cleaning it up for this merge, but if you could start stripping trailing whitespace before committing I'd appreciate it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'friends/protocols/twitter.py'
--- friends/protocols/twitter.py 2013-12-10 07:04:05 +0000
+++ friends/protocols/twitter.py 2014-01-24 10:56:16 +0000
@@ -127,29 +127,51 @@
127127
128 #Resolve t.co128 #Resolve t.co
129 #TODO support more than one url and/or media file129 #TODO support more than one url and/or media file
130 for url in (entities.get('urls', []) + entities.get('media', [])):130 urls = {}
131 begin, end = url.get('indices', (None, None))131
132132 for url in (entities.get('urls', []) + entities.get('media', []) + entities.get('user_mentions', []) + entities.get('hashtags', [])):
133 expanded_url = url.get('expanded_url', '')133 begin, end = url.get('indices', (None, None))
134 display_url = url.get('display_url', '')134
135 other_url = url.get('url', '')135 #Drop invalid entities (just to be safe)
136 if None not in (begin, end):
137 urls[begin] = url
138
139 for key, url in sorted(urls.items(), reverse = True):
140 begin, end = url.get('indices', (None, None))
141
142 expanded_url = url.get('expanded_url')
143 display_url = url.get('display_url')
144 other_url = url.get('url')
145
146 mention_name = url.get('screen_name')
136147
137 picture_url = url.get('media_url', picture_url)148 picture_url = url.get('media_url', picture_url)
149
150 hashtag = url.get('text')
151
152 content = None
138153
139 # Friends has no notion of display URLs, so this is handled at the protocol level154 # Friends has no notion of display URLs, so this is handled at the protocol level
140 if None not in (begin, end):155 if (other_url or expanded_url):
156 content = self._linkify(expanded_url or other_url, display_url or other_url)
157
158 # Linkify hashtags until supported by friends
159 if hashtag:
160 content = self._linkify('https://twitter.com/search?q=%23' + hashtag + '&src=hash', '#' + hashtag)
161
162 # Linkify a mention until they are supported natively by friends
163 if mention_name:
164 content = self._linkify_mention(mention_name)
165
166 if content:
141 message = ''.join([167 message = ''.join([
142 message[:begin],168 message[:begin],
143 '<a href="',169 content,
144 (expanded_url or other_url),
145 '">',
146 (display_url or other_url),
147 '</a>',
148 message[end:]])170 message[end:]])
149171
150 if retweet:172 if retweet:
151 message = 'RT @{}: {}'.format(173 message = 'RT {}: {}'.format(
152 retweet.get('user', {}).get('screen_name', ''),174 self._linkify_mention(retweet.get('user', {}).get('screen_name', '')),
153 message175 message
154 )176 )
155177
@@ -168,6 +190,17 @@
168 link_picture=picture_url,190 link_picture=picture_url,
169 )191 )
170 return permalink192 return permalink
193
194 def _linkify_mention(self, name):
195 return self._linkify('https://twitter.com/' + name, '@' + name)
196
197 def _linkify(self, address, name):
198 return ''.join([
199 '<a href="',
200 address,
201 '">',
202 name,
203 '</a>'])
171204
172 def _append_since(self, url, stream='messages'):205 def _append_since(self, url, stream='messages'):
173 since = self._tweet_ids.get(stream)206 since = self._tweet_ids.get(stream)
174207
=== added file 'friends/tests/data/twitter-hashtags.dat'
--- friends/tests/data/twitter-hashtags.dat 1970-01-01 00:00:00 +0000
+++ friends/tests/data/twitter-hashtags.dat 2014-01-24 10:56:16 +0000
@@ -0,0 +1,94 @@
1{
2 "created_at": "Fri Jan 17 14:23:41 +0000 2014",
3 "id": 424185261375766500,
4 "id_str": "424185261375766530",
5 "text": "A service that filters food pictures from Instagram. #MillionDollarIdea",
6 "source": "web",
7 "truncated": false,
8 "in_reply_to_status_id": null,
9 "in_reply_to_status_id_str": null,
10 "in_reply_to_user_id": null,
11 "in_reply_to_user_id_str": null,
12 "in_reply_to_screen_name": null,
13 "user": {
14 "id": 17339829,
15 "id_str": "17339829",
16 "name": "Kai Mast",
17 "screen_name": "Kai_Mast",
18 "location": "Bamberg",
19 "description": "Computer Science Student, Grad School Applicant, C++ Fanboy",
20 "url": "http://t.co/1myW31Mlhl",
21 "entities": {
22 "url": {
23 "urls": [
24 {
25 "url": "http://t.co/1myW31Mlhl",
26 "expanded_url": "http://kai-mast.de",
27 "display_url": "kai-mast.de",
28 "indices": [
29 0,
30 22
31 ]
32 }
33 ]
34 },
35 "description": {
36 "urls": []
37 }
38 },
39 "protected": false,
40 "followers_count": 415,
41 "friends_count": 904,
42 "listed_count": 36,
43 "created_at": "Wed Nov 12 14:23:09 +0000 2008",
44 "favourites_count": 939,
45 "utc_offset": 3600,
46 "time_zone": "Berlin",
47 "geo_enabled": true,
48 "verified": false,
49 "statuses_count": 7886,
50 "lang": "en",
51 "contributors_enabled": false,
52 "is_translator": false,
53 "profile_background_color": "8B542B",
54 "profile_background_image_url": "http://abs.twimg.com/images/themes/theme8/bg.gif",
55 "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme8/bg.gif",
56 "profile_background_tile": false,
57 "profile_image_url": "http://pbs.twimg.com/profile_images/424181800701673473/Q6Ggqg7P_normal.png",
58 "profile_image_url_https": "https://pbs.twimg.com/profile_images/424181800701673473/Q6Ggqg7P_normal.png",
59 "profile_banner_url": "https://pbs.twimg.com/profile_banners/17339829/1360006443",
60 "profile_link_color": "9D582E",
61 "profile_sidebar_border_color": "D9B17E",
62 "profile_sidebar_fill_color": "EADEAA",
63 "profile_text_color": "333333",
64 "profile_use_background_image": true,
65 "default_profile": false,
66 "default_profile_image": false,
67 "following": false,
68 "follow_request_sent": false,
69 "notifications": false
70 },
71 "geo": null,
72 "coordinates": null,
73 "place": null,
74 "contributors": null,
75 "retweet_count": 0,
76 "favorite_count": 0,
77 "entities": {
78 "hashtags": [
79 {
80 "text": "MillionDollarIdea",
81 "indices": [
82 53,
83 71
84 ]
85 }
86 ],
87 "symbols": [],
88 "urls": [],
89 "user_mentions": []
90 },
91 "favorited": false,
92 "retweeted": false,
93 "lang": "en"
94}
095
=== added file 'friends/tests/data/twitter-multiple-links.dat'
--- friends/tests/data/twitter-multiple-links.dat 1970-01-01 00:00:00 +0000
+++ friends/tests/data/twitter-multiple-links.dat 2014-01-24 10:56:16 +0000
@@ -0,0 +1,144 @@
1{
2 "created_at": "Thu Jan 23 11:40:35 +0000 2014",
3 "id": 426318539796930560,
4 "id_str": "426318539796930560",
5 "text": "An old people's home has recreated famous movie scenes for a wonderful calendar http://t.co/jjqteYzur0 http://t.co/JxQTPG7WLL",
6 "source": "<a href=\"https://about.twitter.com/products/tweetdeck\" rel=\"nofollow\">TweetDeck</a>",
7 "truncated": false,
8 "in_reply_to_status_id": null,
9 "in_reply_to_status_id_str": null,
10 "in_reply_to_user_id": null,
11 "in_reply_to_user_id_str": null,
12 "in_reply_to_screen_name": null,
13 "user": {
14 "id": 16973333,
15 "id_str": "16973333",
16 "name": "The Independent",
17 "screen_name": "Independent",
18 "location": "London, United Kingdom",
19 "description": "News, comment and features from The Independent. Also follow: @IndyVoices, \r\n@IndyPolitics, @IndyWorld and our journalists at http://t.co/YjS7NcXK4A",
20 "url": "http://t.co/wDS5ly0QoO",
21 "entities": {
22 "url": {
23 "urls": [
24 {
25 "url": "http://t.co/wDS5ly0QoO",
26 "expanded_url": "http://www.independent.co.uk",
27 "display_url": "independent.co.uk",
28 "indices": [
29 0,
30 22
31 ]
32 }
33 ]
34 },
35 "description": {
36 "urls": [
37 {
38 "url": "http://t.co/YjS7NcXK4A",
39 "expanded_url": "http://ind.pn/Wdlm9a",
40 "display_url": "ind.pn/Wdlm9a",
41 "indices": [
42 126,
43 148
44 ]
45 }
46 ]
47 }
48 },
49 "protected": false,
50 "followers_count": 505759,
51 "friends_count": 1747,
52 "listed_count": 7890,
53 "created_at": "Sun Oct 26 00:00:29 +0000 2008",
54 "favourites_count": 29,
55 "utc_offset": 0,
56 "time_zone": "London",
57 "geo_enabled": false,
58 "verified": true,
59 "statuses_count": 58003,
60 "lang": "en",
61 "contributors_enabled": false,
62 "is_translator": false,
63 "profile_background_color": "EBEBEB",
64 "profile_background_image_url": "http://a0.twimg.com/profile_background_images/378800000119288704/4ac964c83462c88837dc1e735aa1a45e.png",
65 "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/378800000119288704/4ac964c83462c88837dc1e735aa1a45e.png",
66 "profile_background_tile": true,
67 "profile_image_url": "http://pbs.twimg.com/profile_images/378800000706113664/d1a957578723e496c025be1e2577d06d_normal.jpeg",
68 "profile_image_url_https": "https://pbs.twimg.com/profile_images/378800000706113664/d1a957578723e496c025be1e2577d06d_normal.jpeg",
69 "profile_link_color": "FC051A",
70 "profile_sidebar_border_color": "FFFFFF",
71 "profile_sidebar_fill_color": "FFFFFF",
72 "profile_text_color": "333333",
73 "profile_use_background_image": true,
74 "default_profile": false,
75 "default_profile_image": false,
76 "following": false,
77 "follow_request_sent": false,
78 "notifications": false
79 },
80 "geo": null,
81 "coordinates": null,
82 "place": null,
83 "contributors": null,
84 "retweet_count": 131,
85 "favorite_count": 83,
86 "entities": {
87 "hashtags": [],
88 "symbols": [],
89 "urls": [
90 {
91 "url": "http://t.co/jjqteYzur0",
92 "expanded_url": "http://ind.pn/1g3wX9q",
93 "display_url": "ind.pn/1g3wX9q",
94 "indices": [
95 80,
96 102
97 ]
98 }
99 ],
100 "user_mentions": [],
101 "media": [
102 {
103 "id": 426318539692056600,
104 "id_str": "426318539692056576",
105 "indices": [
106 103,
107 125
108 ],
109 "media_url": "http://pbs.twimg.com/media/BeqWc_-CIAAhmdc.jpg",
110 "media_url_https": "https://pbs.twimg.com/media/BeqWc_-CIAAhmdc.jpg",
111 "url": "http://t.co/JxQTPG7WLL",
112 "display_url": "pic.twitter.com/JxQTPG7WLL",
113 "expanded_url": "http://twitter.com/Independent/status/426318539796930560/photo/1",
114 "type": "photo",
115 "sizes": {
116 "thumb": {
117 "w": 150,
118 "h": 150,
119 "resize": "crop"
120 },
121 "large": {
122 "w": 1024,
123 "h": 682,
124 "resize": "fit"
125 },
126 "small": {
127 "w": 340,
128 "h": 226,
129 "resize": "fit"
130 },
131 "medium": {
132 "w": 600,
133 "h": 400,
134 "resize": "fit"
135 }
136 }
137 }
138 ]
139 },
140 "favorited": false,
141 "retweeted": false,
142 "possibly_sensitive": false,
143 "lang": "en"
144}
0145
=== modified file 'friends/tests/test_twitter.py'
--- friends/tests/test_twitter.py 2013-12-05 20:45:19 +0000
+++ friends/tests/test_twitter.py 2014-01-24 10:56:16 +0000
@@ -145,7 +145,8 @@
145 ['twitter', 88, '240556426106372096',145 ['twitter', 88, '240556426106372096',
146 'messages', 'Raffi Krikorian', '8285392', 'raffi', False,146 'messages', 'Raffi Krikorian', '8285392', 'raffi', False,
147 '2012-08-28T21:08:15Z', 'lecturing at the "analyzing big data '147 '2012-08-28T21:08:15Z', 'lecturing at the "analyzing big data '
148 'with twitter" class at @cal with @othman '148 'with twitter" class at <a href="https://twitter.com/Cal">@Cal</a>'
149 ' with <a href="https://twitter.com/othman">@othman</a> '
149 '<a href="http://twitter.com/yunorno/status/114080493036773378/photo/1">'150 '<a href="http://twitter.com/yunorno/status/114080493036773378/photo/1">'
150 'pic.twitter.com/rJC5Pxsu</a>', 151 'pic.twitter.com/rJC5Pxsu</a>',
151 'https://si0.twimg.com/profile_images/1270234259/'152 'https://si0.twimg.com/profile_images/1270234259/'
@@ -468,7 +469,7 @@
468 expected_row = [469 expected_row = [
469 'twitter', 88, '324220250889543682',470 'twitter', 88, '324220250889543682',
470 'messages', 'Robert Bruce', '836242932', 'therealrobru', True,471 'messages', 'Robert Bruce', '836242932', 'therealrobru', True,
471 '2013-04-16T17:58:26Z', 'RT @tarek_ziade: Just found a "Notification '472 '2013-04-16T17:58:26Z', 'RT <a href="https://twitter.com/tarek_ziade">@tarek_ziade</a>: Just found a "Notification '
472 'of Inspection" card in the bottom of my bag. looks like they were '473 'of Inspection" card in the bottom of my bag. looks like they were '
473 'curious about those raspberry-pi :O',474 'curious about those raspberry-pi :O',
474 'https://si0.twimg.com/profile_images/2631306428/'475 'https://si0.twimg.com/profile_images/2631306428/'
@@ -478,6 +479,78 @@
478 ]479 ]
479 self.assertEqual(list(TestModel.get_row(0)), expected_row)480 self.assertEqual(list(TestModel.get_row(0)), expected_row)
480481
482
483 @mock.patch('friends.utils.base.Model', TestModel)
484 @mock.patch('friends.utils.http.Soup.Message',
485 FakeSoupMessage('friends.tests.data', 'twitter-multiple-links.dat'))
486 @mock.patch('friends.protocols.twitter.Twitter._login',
487 return_value=True)
488 @mock.patch('friends.utils.base._seen_ids', {})
489 def test_multiple_links(self, *mocks):
490 self.account.access_token = 'access'
491 self.account.secret_token = 'secret'
492 self.account.user_name = 'Independent'
493 self.account.auth.parameters = dict(
494 ConsumerKey='key',
495 ConsumerSecret='secret')
496
497 self.assertEqual(0, TestModel.get_n_rows())
498 self.assertEqual(
499 self.protocol.send('some message'),
500 'https://twitter.com/Independent/status/426318539796930560')
501 self.assertEqual(1, TestModel.get_n_rows())
502
503 self.maxDiff = None
504 expected_row = [
505 'twitter', 88, '426318539796930560',
506 'messages', 'The Independent', '16973333', 'Independent', True,
507 '2014-01-23T11:40:35Z',
508 'An old people\'s home has recreated famous movie scenes for a wonderful calendar '
509 '<a href="http://ind.pn/1g3wX9q">ind.pn/1g3wX9q</a> '
510 '<a href="http://twitter.com/Independent/status/426318539796930560/photo/1">pic.twitter.com/JxQTPG7WLL</a>',
511 'https://pbs.twimg.com/profile_images/378800000706113664/d1a957578723e496c025be1e2577d06d.jpeg',
512 'https://twitter.com/Independent/status/426318539796930560',
513 0, False,
514 'http://pbs.twimg.com/media/BeqWc_-CIAAhmdc.jpg',
515 '', '', '', '', '', '', 0.0, 0.0,
516 ]
517 self.assertEqual(list(TestModel.get_row(0)), expected_row)
518
519 @mock.patch('friends.utils.base.Model', TestModel)
520 @mock.patch('friends.utils.http.Soup.Message',
521 FakeSoupMessage('friends.tests.data', 'twitter-hashtags.dat'))
522 @mock.patch('friends.protocols.twitter.Twitter._login',
523 return_value=True)
524 @mock.patch('friends.utils.base._seen_ids', {})
525 def test_multiple_links(self, *mocks):
526 self.account.access_token = 'access'
527 self.account.secret_token = 'secret'
528 self.account.user_name = 'Independent'
529 self.account.auth.parameters = dict(
530 ConsumerKey='key',
531 ConsumerSecret='secret')
532
533 self.assertEqual(0, TestModel.get_n_rows())
534 self.assertEqual(
535 self.protocol.send('some message'),
536 'https://twitter.com/Kai_Mast/status/424185261375766530')
537 self.assertEqual(1, TestModel.get_n_rows())
538
539 self.maxDiff = None
540 expected_row = [
541 'twitter', 88, '424185261375766530',
542 'messages', 'Kai Mast', '17339829', 'Kai_Mast', False,
543 '2014-01-17T14:23:41Z',
544 'A service that filters food pictures from Instagram. '
545 '<a href="https://twitter.com/search?q=%23MillionDollarIdea&src=hash">#MillionDollarIdea</a>',
546 'https://pbs.twimg.com/profile_images/424181800701673473/Q6Ggqg7P.png',
547 'https://twitter.com/Kai_Mast/status/424185261375766530',
548 0, False,
549 '',
550 '', '', '', '', '', '', 0.0, 0.0,
551 ]
552 self.assertEqual(list(TestModel.get_row(0)), expected_row)
553
481 def test_unfollow(self):554 def test_unfollow(self):
482 get_url = self.protocol._get_url = mock.Mock()555 get_url = self.protocol._get_url = mock.Mock()
483556

Subscribers

People subscribed via source and target branches

to all changes: