mirror of
https://github.com/zulip/zulip.git
synced 2025-11-06 15:03:34 +00:00
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:
@@ -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',
|
||||||
|
|||||||
63
zephyr/lib/bugdown/testing_mocks.py
Normal file
63
zephyr/lib/bugdown/testing_mocks.py
Normal 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
|
||||||
|
}""")
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user