Add inline preview of Twitter links.

This uses the unauthed v1 of the Twitter API, which is going to go
away soon, but it's fine as an interim measure.

(imported from commit 709a250271321f5479854a363875c9da43e6382d)
This commit is contained in:
Waseem Daher
2013-03-08 00:27:16 -05:00
parent 4ab5cabb80
commit 1df648baa9
4 changed files with 177 additions and 0 deletions

View File

@@ -5,8 +5,11 @@ import urlparse
import re import re
import os.path import os.path
import glob import glob
import urllib2
import simplejson
from django.core import mail from django.core import mail
from django.conf import settings
from zephyr.lib.avatar import gravatar_hash from zephyr.lib.avatar import gravatar_hash
from zephyr.lib.bugdown import codehilite, fenced_code from zephyr.lib.bugdown import codehilite, fenced_code
@@ -87,6 +90,65 @@ class InlineImagePreviewProcessor(markdown.treeprocessors.Treeprocessor):
return root return root
class InlineInterestingLinkProcessor(markdown.treeprocessors.Treeprocessor):
def twitter_link(self, url):
parsed_url = urlparse.urlparse(url)
if not (parsed_url.netloc == 'twitter.com' or parsed_url.netloc.endswith('.twitter.com')):
return None
tweet_id_match = re.match(r'^/.*?/status/(\d{18})$', parsed_url.path)
if not tweet_id_match:
return None
tweet_id = tweet_id_match.groups()[0]
try:
if settings.TEST_SUITE:
import testing_mocks
res = testing_mocks.twitter(tweet_id)
else:
res = simplejson.load(urllib2.urlopen("https://api.twitter.com/1/statuses/show.json?id=%s" % tweet_id))
user = res['user']
tweet = markdown.util.etree.Element("div")
tweet.set("class", "twitter-tweet")
img_a = markdown.util.etree.SubElement(tweet, 'a')
img_a.set("href", url)
img_a.set("target", "_blank")
profile_img = markdown.util.etree.SubElement(img_a, 'img')
profile_img.set('class', 'twitter-avatar')
profile_img.set('src', user['profile_image_url_https'])
p = markdown.util.etree.SubElement(tweet, 'p')
p.text = res['text']
span = markdown.util.etree.SubElement(tweet, 'span')
span.text = "- %s (@%s)" % (user['name'], user['screen_name'])
return ('twitter', tweet)
except:
# We put this in its own try-except because it requires external
# connectivity. If Twitter flakes out, we don't want to not-render
# the entire message; we just want to not show the Twitter preview.
traceback.print_exc()
return None
# Search the tree for <a> tags and read their href values
def find_interesting_links(self, root):
def process_interesting_links(element):
if element.tag != "a":
return None
url = element.get("href")
return self.twitter_link(url)
return walk_tree(root, process_interesting_links)
def run(self, root):
interesting_links = self.find_interesting_links(root)
for (service_name, data) in interesting_links:
div = markdown.util.etree.SubElement(root, "div")
div.set("class", "inline-preview-%s" % service_name)
div.insert(0, data)
return root
class Gravatar(markdown.inlinepatterns.Pattern): class Gravatar(markdown.inlinepatterns.Pattern):
def handleMatch(self, match): def handleMatch(self, match):
img = markdown.util.etree.Element('img') img = markdown.util.etree.Element('img')
@@ -302,6 +364,7 @@ class Bugdown(markdown.Extension):
"_begin") "_begin")
md.treeprocessors.add("inline_images", InlineImagePreviewProcessor(md), "_end") md.treeprocessors.add("inline_images", InlineImagePreviewProcessor(md), "_end")
md.treeprocessors.add("inline_interesting_links", InlineInterestingLinkProcessor(md), "_end")
_md_engine = markdown.Markdown( _md_engine = markdown.Markdown(
safe_mode = 'escape', safe_mode = 'escape',

View File

@@ -0,0 +1,63 @@
import simplejson
def twitter(tweet_id):
return simplejson.loads("""{
"coordinates": null,
"created_at": "Sat Sep 10 22:23:38 +0000 2011",
"truncated": false,
"favorited": false,
"id_str": "112652479837110273",
"in_reply_to_user_id_str": "783214",
"text": "@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM",
"contributors": null,
"id": 112652479837110273,
"retweet_count": 0,
"in_reply_to_status_id_str": null,
"geo": null,
"retweeted": false,
"possibly_sensitive": false,
"in_reply_to_user_id": 783214,
"user": {
"profile_sidebar_border_color": "eeeeee",
"profile_background_tile": true,
"profile_sidebar_fill_color": "efefef",
"name": "Eoin McMillan ",
"profile_image_url": "http://a1.twimg.com/profile_images/1380912173/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png",
"created_at": "Mon May 16 20:07:59 +0000 2011",
"location": "Twitter",
"profile_link_color": "009999",
"follow_request_sent": null,
"is_translator": false,
"id_str": "299862462",
"favourites_count": 0,
"default_profile": false,
"url": "http://www.eoin.me",
"contributors_enabled": false,
"id": 299862462,
"utc_offset": null,
"profile_image_url_https": "https://si0.twimg.com/profile_images/1380912173/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png",
"profile_use_background_image": true,
"listed_count": 0,
"followers_count": 9,
"lang": "en",
"profile_text_color": "333333",
"protected": false,
"profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme14/bg.gif",
"description": "Eoin's photography account. See @mceoin for tweets.",
"geo_enabled": false,
"verified": false,
"profile_background_color": "131516",
"time_zone": null,
"notifications": null,
"statuses_count": 255,
"friends_count": 0,
"default_profile_image": false,
"profile_background_image_url": "http://a1.twimg.com/images/themes/theme14/bg.gif",
"screen_name": "imeoin",
"following": null,
"show_all_inline_media": false
},
"in_reply_to_screen_name": "twitter",
"in_reply_to_status_id": null
}""")

View File

@@ -1040,3 +1040,15 @@ table.floating_recipient {
height: 1em; height: 1em;
vertical-align: middle; vertical-align: middle;
} }
.twitter-tweet {
border: 1px solid #ddd;
padding: .5em .75em;
margin-bottom: 0.25em;
}
.twitter-avatar {
float: left;
height: 48px;
padding-right: .75em;
}

View File

@@ -1941,6 +1941,45 @@ xxxxxxx</strong></p>\n<p>xxxxxxx xxxxx xxxx xxxxx:<br>\n<code>xxxxxx</code>: xxx
self.assertEqual(converted, '<p>Look at the new dropbox logo: <a href="https://www.dropbox.com/static/images/home_logo.png" target="_blank" title="https://www.dropbox.com/static/images/home_logo.png">https://www.dropbox.com/static/images/home_logo.png</a></p>\n<a href="https://www.dropbox.com/static/images/home_logo.png" target="_blank" title="https://www.dropbox.com/static/images/home_logo.png"><img class="message_inline_image" src="https://www.dropbox.com/static/images/home_logo.png"></a>') self.assertEqual(converted, '<p>Look at the new dropbox logo: <a href="https://www.dropbox.com/static/images/home_logo.png" target="_blank" title="https://www.dropbox.com/static/images/home_logo.png">https://www.dropbox.com/static/images/home_logo.png</a></p>\n<a href="https://www.dropbox.com/static/images/home_logo.png" target="_blank" title="https://www.dropbox.com/static/images/home_logo.png"><img class="message_inline_image" src="https://www.dropbox.com/static/images/home_logo.png"></a>')
def test_inline_interesting_links(self):
def make_link(url):
return '<a href="%s" target="_blank" title="%s">%s</a>' % (url, url, url)
def make_inline_twitter_preview(url):
## As of right now, all previews are mocked to be the exact same tweet
return """<div class="inline-preview-twitter"><div class="twitter-tweet"><a href="%s" target="_blank"><img class="twitter-avatar" src="https://si0.twimg.com/profile_images/1380912173/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png"></a><p>@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM</p><span>- Eoin McMillan (@imeoin)</span></div></div>""" % (url, )
msg = 'http://www.twitter.com'
converted = convert(msg)
self.assertEqual(converted, '<p>%s</p>' % make_link('http://www.twitter.com'))
msg = 'http://www.twitter.com/wdaher/'
converted = convert(msg)
self.assertEqual(converted, '<p>%s</p>' % make_link('http://www.twitter.com/wdaher/'))
msg = 'http://www.twitter.com/wdaher/status/3'
converted = convert(msg)
self.assertEqual(converted, '<p>%s</p>' % make_link('http://www.twitter.com/wdaher/status/3'))
# id too long
msg = 'http://www.twitter.com/wdaher/status/2879779692873154569'
converted = convert(msg)
self.assertEqual(converted, '<p>%s</p>' % make_link('http://www.twitter.com/wdaher/status/2879779692873154569'))
msg = 'http://www.twitter.com/wdaher/status/287977969287315456'
converted = convert(msg)
self.assertEqual(converted, '<p>%s</p>\n%s' % (make_link('http://www.twitter.com/wdaher/status/287977969287315456'),
make_inline_twitter_preview('http://www.twitter.com/wdaher/status/287977969287315456')))
msg = 'https://www.twitter.com/wdaher/status/287977969287315456'
converted = convert(msg)
self.assertEqual(converted, '<p>%s</p>\n%s' % (make_link('https://www.twitter.com/wdaher/status/287977969287315456'),
make_inline_twitter_preview('https://www.twitter.com/wdaher/status/287977969287315456')))
msg = 'http://twitter.com/wdaher/status/287977969287315456'
converted = convert(msg)
self.assertEqual(converted, '<p>%s</p>\n%s' % (make_link('http://twitter.com/wdaher/status/287977969287315456'),
make_inline_twitter_preview('http://twitter.com/wdaher/status/287977969287315456')))
def test_emoji(self): def test_emoji(self):
def emoji_img(name, filename=None): def emoji_img(name, filename=None):