Nothing Special   »   [go: up one dir, main page]

Skip to content

Commit

Permalink
twitter create/preview: support alt text for images, via AS1 displayName
Browse files Browse the repository at this point in the history
  • Loading branch information
snarfed committed Jul 26, 2018
1 parent 07e516d commit 263c1d1
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 19 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,8 @@ Changelog
### 1.13 - unreleased
* Twitter:
* Support ISO 8601 formatted created_at timestamps, which the [archive download uses](https://help.twitter.com/en/managing-your-account/how-to-download-your-twitter-archive), as well as RFC 2822 from the API.
* `create()` and `preview_create()`: support RSVPs. Tweet them as normal tweets with the RSVP content. ([#818](https://github.com/snarfed/bridgy/issues/818))
* `create()` and `preview_create()`: support RSVPs. Tweet them as normal tweets with the RSVP content. ([snarfed/bridgy#818](https://github.com/snarfed/bridgy/issues/818))
* `create()` and `preview_create()`: support alt text for images, via AS1 `displayName`. ([snarfed/bridgy#756](https://github.com/snarfed/bridgy/issues/756)).
* Instagram:
* Add global rate limiting lock for scraping. If a scraping HTTP request gets a 429 or 503 response, we refuse to make more requests for 5m, and instead short circuit and return the same error. This can be overridden with a new `ignore_rate_limit` kwarg to `get_activities()`.
* GitHub:
Expand Down
42 changes: 39 additions & 3 deletions granary/test/test_twitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2360,7 +2360,7 @@ def test_create_with_multiple_photos(self):
preview = self.twitter.preview_create(obj)
self.assertEqual('<span class="verb">tweet</span>:', preview.description)
self.assertEqual(ellipsized + '<br /><br />' +
' &nbsp; '.join('<img src="%s" />' % url
' &nbsp; '.join('<img src="%s" alt="" />' % url
for url in image_urls[:-1]),
preview.content)

Expand Down Expand Up @@ -2393,7 +2393,7 @@ def test_create_reply_with_photo(self):
# test preview
preview = self.twitter.preview_create(obj)
self.assertIn('<span class="verb">@-reply</span> to <a href="http://twitter.com/you/status/100">this tweet</a>:', preview.description)
self.assertEqual('my content<br /><br /><img src="http://my/picture" />',
self.assertEqual('my content<br /><br /><img src="http://my/picture" alt="" />',
preview.content)

# test create
Expand Down Expand Up @@ -2423,7 +2423,8 @@ def test_create_with_photo_no_content(self):
# test preview
preview = self.twitter.preview_create(obj)
self.assertEqual('<span class="verb">tweet</span>:', preview.description)
self.assertEqual('<br /><br /><img src="http://my/picture" />', preview.content)
self.assertEqual('<br /><br /><img src="http://my/picture" alt="" />',
preview.content)

# test create
self.expect_urlopen('http://my/picture', 'picture response')
Expand Down Expand Up @@ -2476,6 +2477,41 @@ def test_create_with_photo_wrong_type(self):
self.assertEqual('Twitter only supports JPG, PNG, GIF, and WEBP images; '
'http://my/picture.tiff looks like image/tiff', msg)

def test_create_with_photo_with_alt(self):
obj = {
'objectType': 'note',
'image': {
'url': 'http://my/picture.png',
'displayName': 'some alt text',
},
}

# test preview
preview = self.twitter.preview_create(obj)
self.assertEqual('<span class="verb">tweet</span>:', preview.description)
self.assertEqual('<br /><br /><img src="http://my/picture.png" alt="some alt text" />',
preview.content)

# test create
self.expect_urlopen('http://my/picture.png', 'picture response')
self.expect_requests_post(twitter.API_UPLOAD_MEDIA,
json.dumps({'media_id_string': '123'}),
files={'media': 'picture response'},
headers=mox.IgnoreArg())
self.expect_requests_post(twitter.API_MEDIA_METADATA,
json={'media_id': '123',
'alt_text': {'text': 'some alt text'}},
headers=mox.IgnoreArg())
self.expect_urlopen(twitter.API_POST_TWEET, {'url': 'http://posted/picture'},
params=(
# sorted; order matters.
('media_ids', '123'),
('status', ''),
))
self.mox.ReplayAll()
self.assert_equals({'url': 'http://posted/picture', 'type': 'post'},
self.twitter.create(obj).content)

def test_create_with_video(self):
self.mox.StubOutWithMock(twitter, 'MAX_TWEET_LENGTH')
twitter.MAX_TWEET_LENGTH = 140
Expand Down
52 changes: 37 additions & 15 deletions granary/twitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@

import collections
import datetime
import itertools
import http.client
import itertools
import json
import logging
import mimetypes
import re
Expand Down Expand Up @@ -50,6 +51,7 @@
API_STATUS = 'statuses/show.json?id=%s&include_entities=true&tweet_mode=extended'
API_TIMELINE = 'statuses/home_timeline.json?include_entities=true&tweet_mode=extended&count=%d'
API_UPLOAD_MEDIA = 'https://upload.twitter.com/1.1/media/upload.json'
API_MEDIA_METADATA = 'https://upload.twitter.com/1.1/media/metadata/create.json'
API_USER = 'users/show.json?screen_name=%s'
API_USER_TIMELINE = 'statuses/user_timeline.json?include_entities=true&tweet_mode=extended&count=%(count)d&screen_name=%(screen_name)s'
HTML_FAVORITES = 'https://twitter.com/i/activity/favorited_popup?id=%s'
Expand Down Expand Up @@ -666,9 +668,9 @@ def _create(self, obj, preview=None, include_link=source.OMIT_LINK,

is_reply = type == 'comment' or 'inReplyTo' in obj
is_rsvp = (verb and verb.startswith('rsvp-')) or verb == 'invite'
image_urls = [image.get('url') for image in util.get_list(obj, 'image')]
images = util.get_list(obj, 'image')
video_url = util.get_first(obj, 'stream', {}).get('url')
has_media = (image_urls or video_url) and (type in ('note', 'article') or is_reply)
has_media = (images or video_url) and (type in ('note', 'article') or is_reply)
lat = obj.get('location', {}).get('latitude')
lng = obj.get('location', {}).get('longitude')

Expand Down Expand Up @@ -816,16 +818,17 @@ def _create(self, obj, preview=None, include_link=source.OMIT_LINK,
return ret
data.append(('media_ids', ret))

elif image_urls:
num_urls = len(image_urls)
if num_urls > MAX_MEDIA:
image_urls = image_urls[:MAX_MEDIA]
elif images:
num = len(images)
if num > MAX_MEDIA:
images = images[:MAX_MEDIA]
logging.warning('Found %d photos! Only using the first %d: %r',
num_urls, MAX_MEDIA, image_urls)
num, MAX_MEDIA, images)
preview_content += '<br /><br />' + ' &nbsp; '.join(
'<img src="%s" />' % url for url in image_urls)
'<img src="%s" alt="%s" />' % (img.get('url'), img.get('displayName', ''))
for img in images)
if not preview:
ret = self.upload_images(image_urls)
ret = self.upload_images(images)
if isinstance(ret, source.CreationResult):
return ret
data.append(('media_ids', ','.join(ret)))
Expand Down Expand Up @@ -895,19 +898,28 @@ def _truncate(self, content, url, include_link, type, quote_tweet=None):

return truncated

def upload_images(self, urls):
def upload_images(self, images):
"""Uploads one or more images from web URLs.
https://dev.twitter.com/rest/reference/post/media/upload
Note that files and JSON bodies in media POST API requests are *not*
included in OAuth signatures.
https://developer.twitter.com/en/docs/media/upload-media/uploading-media/media-best-practices
Args:
urls: sequence of string URLs of images
images: sequence of AS image objects, eg:
[{'url': 'http://picture', 'displayName': 'a thing'}, ...]
Returns:
list of string media ids
list of string media ids or :class:`CreationResult` on error
"""
ids = []
for url in urls:
for image in images:
url = image.get('url')
if not url:
continue

image_resp = util.urlopen(url)
bad_type = self._check_mime_type(url, image_resp, IMAGE_MIME_TYPES,
'JPG, PNG, GIF, and WEBP images')
Expand All @@ -921,7 +933,17 @@ def upload_images(self, urls):
headers=headers)
resp.raise_for_status()
logging.info('Got: %s', resp.text)
ids.append(source.load_json(resp.text, API_UPLOAD_MEDIA)['media_id_string'])
media_id = source.load_json(resp.text, API_UPLOAD_MEDIA)['media_id_string']
ids.append(media_id)

alt = image.get('displayName')
if alt:
headers = twitter_auth.auth_header(
API_MEDIA_METADATA, self.access_token_key, self.access_token_secret, 'POST')
resp = util.requests_post(API_MEDIA_METADATA,
json={'media_id': media_id,'alt_text': {'text': alt}},
headers=headers)
logging.info('Got: %s', resp)

return ids

Expand Down

0 comments on commit 263c1d1

Please sign in to comment.