def test_create_like(self): self.expect_urlopen( 'https://api.instagram.com/v1/media/shortcode/ABC123', json.dumps({'data': MEDIA})) self.expect_urlopen( 'https://api.instagram.com/v1/media/123_456/likes', '{"meta":{"status":200}}', data='access_token=None') self.expect_urlopen( 'https://api.instagram.com/v1/users/self', json.dumps({'data': { 'id': '8', 'username': '******', 'full_name': 'Alice', 'profile_picture': 'http://alice/picture', }})) # like obj doesn't have url or id prior to publishing to_publish = copy.deepcopy(LIKE_OBJS[0]) del to_publish['id'] del to_publish['url'] self.mox.ReplayAll() self.assert_equals(source.creation_result(LIKE_OBJS[0]), self.instagram.create(to_publish))
def _check_mime_type(url, resp, allowed, label): """Checks that a URL is in a set of allowed MIME type(s). Args: url: string resp: urlopen result object allowed: sequence of allowed string MIME types label: human-readable description of the allowed MIME types, to be used in an error message Returns: None if the url's MIME type is in the set, CreationResult with abort=True if it isn't """ type = resp.headers.get('Content-Type') if not type: type, _ = mimetypes.guess_type(url) if type and type not in allowed: msg = 'Twitter only supports %s; %s looks like %s' % (label, url, type) return source.creation_result(abort=True, error_plain=msg, error_html=msg)
def _check_mime_type(url, resp, allowed, label): """Checks that a URL is in a set of allowed MIME type(s). Args: url: string resp: urlopen result object allowed: sequence of allowed string MIME types label: human-readable description of the allowed MIME types, to be used in an error message Returns: None if the url's MIME type is in the set, :class:`CreationResult` with abort=True if it isn't """ type = resp.headers.get('Content-Type') if not type: type, _ = mimetypes.guess_type(url) if type and type not in allowed: msg = 'Twitter only supports %s; %s looks like %s' % (label, url, type) return source.creation_result(abort=True, error_plain=msg, error_html=msg)
def _create(self, obj, preview=None, include_link=False, ignore_formatting=False): """Creates or previews creating a tweet, reply tweet, retweet, or favorite. https://dev.twitter.com/docs/api/1.1/post/statuses/update https://dev.twitter.com/docs/api/1.1/post/statuses/retweet/:id https://dev.twitter.com/docs/api/1.1/post/favorites/create Args: obj: ActivityStreams object preview: boolean include_link: boolean Returns: a CreationResult If preview is True, the content will be a unicode string HTML snippet. If False, it will be a dict with 'id' and 'url' keys for the newly created Twitter object. """ assert preview in (False, True) type = obj.get('objectType') verb = obj.get('verb') base_obj = self.base_object(obj) base_id = base_obj.get('id') base_url = base_obj.get('url') is_reply = type == 'comment' or 'inReplyTo' in obj has_picture = obj.get('image') and (type in ('note', 'article') or is_reply) content = self._content_for_create(obj, ignore_formatting=ignore_formatting) if not content: if type == 'activity': content = verb elif has_picture: content = '' else: return source.creation_result( abort=False, # keep looking for things to publish, error_plain='No content text found.', error_html='No content text found.') if is_reply and base_url: # extract username from in-reply-to URL so we can @-mention it, if it's # not already @-mentioned, since Twitter requires that to make our new # tweet a reply. # https://dev.twitter.com/docs/api/1.1/post/statuses/update#api-param-in_reply_to_status_id # TODO: this doesn't handle an in-reply-to username that's a prefix of # another username already mentioned, e.g. in reply to @foo when content # includes @foobar. parsed = urlparse.urlparse(base_url) parts = parsed.path.split('/') if len(parts) < 2 or not parts[1]: raise ValueError('Could not determine author of in-reply-to URL %s' % base_url) mention = '@' + parts[1] if mention.lower() not in content.lower(): content = mention + ' ' + content # the embed URL in the preview can't start with mobile. or www., so just # hard-code it to twitter.com. index #1 is netloc. parsed = list(parsed) parsed[1] = self.DOMAIN base_url = urlparse.urlunparse(parsed) # need a base_url with the tweet id for the embed HTML below. do this # *after* checking the real base_url for in-reply-to author username. if base_id and not base_url: base_url = 'https://twitter.com/-/statuses/' + base_id if is_reply and not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to reply to.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/reply">reply to</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/comment">in-reply-to</a> ' 'link a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') # truncate and ellipsize content if it's over the character # count. URLs will be t.co-wrapped, so include that when counting. include_url = obj.get('url') if include_link else None content = self._truncate(content, include_url, has_picture) # linkify defaults to Twitter's link shortening behavior preview_content = util.linkify(content, pretty=True, skip_bare_cc_tlds=True) if has_picture: image_url = obj.get('image').get('url') if preview: if is_reply: desc = ('<span class="verb">@-reply</span> to <a href="%s">this tweet' '</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: desc = '<span class="verb">tweet</span>:' if preview_content: preview_content += '<br /><br />' return source.creation_result( content='%s<img src="%s" />' % (preview_content, image_url), description=desc) else: content = unicode(content).encode('utf-8') data = {'status': content} if is_reply: data['in_reply_to_status_id'] = base_id files = {'media[]': urllib2.urlopen(image_url)} url = API_BASE + API_POST_MEDIA_URL headers = twitter_auth.auth_header(url, self.access_token_key, self.access_token_secret, 'POST') resp = requests.post(url, data=data, files=files, headers=headers, timeout=HTTP_TIMEOUT) resp.raise_for_status() resp = json.loads(resp.text) resp['type'] = 'comment' if is_reply else 'post' elif is_reply: if preview: return source.creation_result( content=preview_content, description='<span class="verb">@-reply</span> to <a href="%s">this tweet' '</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: content = unicode(content).encode('utf-8') data = urllib.urlencode({'status': content, 'in_reply_to_status_id': base_id}) resp = self.urlopen(API_POST_TWEET_URL, data=data) resp['type'] = 'comment' elif type == 'activity' and verb == 'like': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to like.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/favorite">favorite</a>. ' 'Check that your post has a like-of link to a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') if preview: return source.creation_result( description='<span class="verb">favorite</span> <a href="%s">' 'this tweet</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: data = urllib.urlencode({'id': base_id}) self.urlopen(API_POST_FAVORITE_URL, data=data) resp = {'type': 'like'} elif type == 'activity' and verb == 'share': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to retweet.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/repost">retweet</a>. ' 'Check that your post has a repost-of link to a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') if preview: return source.creation_result( description='<span class="verb">retweet</span> <a href="%s">' 'this tweet</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: data = urllib.urlencode({'id': base_id}) resp = self.urlopen(API_POST_RETWEET_URL % base_id, data=data) resp['type'] = 'repost' elif type in ('note', 'article'): if preview: return source.creation_result(content=preview_content, description='<span class="verb">tweet</span>:') else: content = unicode(content).encode('utf-8') data = urllib.urlencode({'status': content}) resp = self.urlopen(API_POST_TWEET_URL, data=data) resp['type'] = 'post' elif (verb and verb.startswith('rsvp-')) or verb == 'invite': return source.creation_result( abort=True, error_plain='Cannot publish RSVPs to Twitter.', error_html='This looks like an <a href="http://indiewebcamp.com/rsvp">RSVP</a>. ' 'Publishing events or RSVPs to Twitter is not supported.') else: return source.creation_result( abort=False, error_plain='Cannot publish type=%s, verb=%s to Twitter' % (type, verb), error_html='Cannot publish type=%s, verb=%s to Twitter' % (type, verb)) id_str = resp.get('id_str') if id_str: resp.update({'id': id_str, 'url': self.tweet_url(resp)}) elif 'url' not in resp: resp['url'] = base_url return source.creation_result(resp)
def _create(self, obj, preview, include_link): """Creates or previews creating for the previous two methods. https://www.flickr.com/services/api/upload.api.html https://www.flickr.com/services/api/flickr.photos.comments.addComment.html https://www.flickr.com/services/api/flickr.favorites.add.html https://www.flickr.com/services/api/flickr.photos.people.add.html Args: obj: ActivityStreams object preview: boolean include_link: boolean Return: a CreationResult """ # photo, comment, or like type = source.object_type(obj) logging.debug('publishing object type %s to Flickr', type) content = self._content_for_create(obj) link_text = '(Originally published at: %s)' % obj.get('url') if obj.get('image') and type in ('note', 'article'): image_url = obj.get('image').get('url') name = obj.get('displayName') people = self._get_person_tags(obj) # if name does not represent an explicit title, then we'll just # use it as the title and wipe out the content if name and content and not mf2util.is_name_a_title(name, content): content = None # add original post link if include_link: content = ((content + '\n\n') if content else '') + link_text if preview: preview_content = '' if name: preview_content += '<h4>%s</h4>' % name if content: preview_content += '<div>%s</div>' % content if people: preview_content += '<div> with %s</div>' % ', '.join( ('<a href="%s">%s</a>' % ( p.get('url'), p.get('displayName') or 'User %s' % p.get('id')) for p in people)) preview_content += '<img src="%s" />' % image_url return source.creation_result( content=preview_content, description='post') params = [] if name: params.append(('title', name)) if content: params.append(('description', content)) resp = self.upload_photo(params, urllib2.urlopen(image_url)) photo_id = resp.get('id') resp.update({ 'type': 'post', 'url': self.photo_url(self.path_alias() or self.user_id(), photo_id), }) # add person tags for person_id in sorted(p.get('id') for p in people): self.call_api_method('flickr.photos.people.add', { 'photo_id': photo_id, 'user_id': person_id, }) return source.creation_result(resp) base_obj = self.base_object(obj) base_id = base_obj.get('id') base_url = base_obj.get('url') # maybe a comment on a flickr photo? if type == 'comment' or obj.get('inReplyTo'): if not base_id: return source.creation_result( abort=True, error_plain='Could not find a photo to comment on.', error_html='Could not find a photo to <a href="http://indiewebcamp.com/reply">comment on</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/comment">in-reply-to</a> ' 'link to a Flickr photo or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Flickr.') if include_link: content += '\n\n' + link_text if preview: return source.creation_result( content=content, description='comment on <a href="%s">this photo</a>.' % base_url) resp = self.call_api_method('flickr.photos.comments.addComment', { 'photo_id': base_id, 'comment_text': content, }) resp = resp.get('comment', {}) resp.update({ 'type': 'comment', 'url': resp.get('permalink'), }) return source.creation_result(resp) if type == 'like': if not base_id: return source.creation_result( abort=True, error_plain='Could not find a photo to favorite.', error_html='Could not find a photo to <a href="http://indiewebcamp.com/like">favorite</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/like">like-of</a> ' 'link to a Flickr photo or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Flickr.') if preview: return source.creation_result( description='favorite <a href="%s">this photo</a>.' % base_url) # this method doesn't return any data self.call_api_method('flickr.favorites.add', { 'photo_id': base_id, }) # TODO should we canonicalize the base_url (e.g. removing trailing path # info like "/in/contacts/") return source.creation_result({ 'type': 'like', 'url': '%s#favorited-by-%s' % (base_url, self.user_id()), }) return source.creation_result( abort=False, error_plain='Cannot publish type=%s to Flickr.' % type, error_html='Cannot publish type=%s to Flickr.' % type)
def upload_video(self, url): """Uploads a video from web URLs using the chunked upload process. Chunked upload consists of multiple API calls: * command=INIT, which allocates the media id * command=APPEND for each 5MB block, up to 15MB total * command=FINALIZE https://dev.twitter.com/rest/reference/post/media/upload-chunked https://dev.twitter.com/rest/public/uploading-media#chunkedupload Args: url: string URL of images Returns: string media id or CreationResult on error """ video_resp = util.urlopen(url) # check format and size type = video_resp.headers.get('Content-Type') if not type: type, _ = mimetypes.guess_type(url) if type and type not in VIDEO_MIME_TYPES: msg = 'Twitter only supports MP4 videos; yours looks like a %s.' % type return source.creation_result(abort=True, error_plain=msg, error_html=msg) length = video_resp.headers.get('Content-Length') if not util.is_int(length): msg = "Couldn't determine your video's size." return source.creation_result(abort=True, error_plain=msg, error_html=msg) length = int(length) if int(length) > MAX_VIDEO_SIZE: msg = "Your %sMB video is larger than Twitter's %dMB limit." % ( length // MB, MAX_VIDEO_SIZE // MB) return source.creation_result(abort=True, error_plain=msg, error_html=msg) # INIT media_id = self.urlopen(API_UPLOAD_MEDIA, data=urllib.urlencode({ 'command': 'INIT', 'media_type': 'video/mp4', 'total_bytes': length, }))['media_id_string'] # APPEND headers = twitter_auth.auth_header( API_UPLOAD_MEDIA, self.access_token_key, self.access_token_secret, 'POST') i = 0 while True: chunk = util.FileLimiter(video_resp, UPLOAD_CHUNK_SIZE) data = { 'command': 'APPEND', 'media_id': media_id, 'segment_index': i, } resp = util.requests_post(API_UPLOAD_MEDIA, data=data, files={'media': chunk}, headers=headers) resp.raise_for_status() if chunk.ateof: break i += 1 # FINALIZE self.urlopen(API_UPLOAD_MEDIA, data=urllib.urlencode({ 'command': 'FINALIZE', 'media_id': media_id, })) return media_id
def _create(self, obj, preview=None, include_link=False, ignore_formatting=False): """Creates or previews creating a tweet, reply tweet, retweet, or favorite. https://dev.twitter.com/docs/api/1.1/post/statuses/update https://dev.twitter.com/docs/api/1.1/post/statuses/retweet/:id https://dev.twitter.com/docs/api/1.1/post/favorites/create Args: obj: ActivityStreams object preview: boolean include_link: boolean Returns: a CreationResult If preview is True, the content will be a unicode string HTML snippet. If False, it will be a dict with 'id' and 'url' keys for the newly created Twitter object. """ assert preview in (False, True) type = obj.get('objectType') verb = obj.get('verb') base_obj = self.base_object(obj) base_id = base_obj.get('id') base_url = base_obj.get('url') is_reply = type == 'comment' or 'inReplyTo' in obj image_urls = [image.get('url') for image in 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) lat = obj.get('location', {}).get('latitude') lng = obj.get('location', {}).get('longitude') # prefer displayName over content for articles type = obj.get('objectType') base_url = self.base_object(obj).get('url') prefer_content = type == 'note' or (base_url and (type == 'comment' or obj.get('inReplyTo'))) content = self._content_for_create(obj, ignore_formatting=ignore_formatting, prefer_name=not prefer_content, strip_first_video_tag=bool(video_url)) if not content: if type == 'activity': content = verb elif has_media: content = '' else: return source.creation_result( abort=False, # keep looking for things to publish, error_plain='No content text found.', error_html='No content text found.') if is_reply and base_url: # extract username from in-reply-to URL so we can @-mention it, if it's # not already @-mentioned, since Twitter requires that to make our new # tweet a reply. # https://dev.twitter.com/docs/api/1.1/post/statuses/update#api-param-in_reply_to_status_id # TODO: this doesn't handle an in-reply-to username that's a prefix of # another username already mentioned, e.g. in reply to @foo when content # includes @foobar. parsed = urlparse.urlparse(base_url) parts = parsed.path.split('/') if len(parts) < 2 or not parts[1]: raise ValueError('Could not determine author of in-reply-to URL %s' % base_url) mention = '@' + parts[1] if mention.lower() not in content.lower(): content = mention + ' ' + content # the embed URL in the preview can't start with mobile. or www., so just # hard-code it to twitter.com. index #1 is netloc. parsed = list(parsed) parsed[1] = self.DOMAIN base_url = urlparse.urlunparse(parsed) # need a base_url with the tweet id for the embed HTML below. do this # *after* checking the real base_url for in-reply-to author username. if base_id and not base_url: base_url = 'https://twitter.com/-/statuses/' + base_id if is_reply and not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to reply to.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/reply">reply to</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/comment">in-reply-to</a> ' 'link a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') # truncate and ellipsize content if it's over the character # count. URLs will be t.co-wrapped, so include that when counting. include_url = obj.get('url') if include_link else None content = self._truncate(content, include_url, has_media) # linkify defaults to Twitter's link shortening behavior preview_content = util.linkify(content, pretty=True, skip_bare_cc_tlds=True) if type == 'activity' and verb == 'like': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to like.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/favorite">favorite</a>. ' 'Check that your post has a like-of link to a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') if preview: return source.creation_result( description='<span class="verb">favorite</span> <a href="%s">' 'this tweet</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: data = urllib.urlencode({'id': base_id}) self.urlopen(API_POST_FAVORITE, data=data) resp = {'type': 'like'} elif type == 'activity' and verb == 'share': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to retweet.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/repost">retweet</a>. ' 'Check that your post has a repost-of link to a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') if preview: return source.creation_result( description='<span class="verb">retweet</span> <a href="%s">' 'this tweet</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: data = urllib.urlencode({'id': base_id}) resp = self.urlopen(API_POST_RETWEET % base_id, data=data) resp['type'] = 'repost' elif type in ('note', 'article') or is_reply: # a tweet content = unicode(content).encode('utf-8') data = {'status': content} if is_reply: description = \ '<span class="verb">@-reply</span> to <a href="%s">this tweet</a>:\n%s' % ( base_url, self.embed_post(base_obj)) data['in_reply_to_status_id'] = base_id else: description = '<span class="verb">tweet</span>:' if video_url: preview_content += ('<br /><br /><video controls src="%s"><a href="%s">' 'this video</a></video>' % (video_url, video_url)) if not preview: ret = self.upload_video(video_url) if isinstance(ret, source.CreationResult): return ret data['media_ids'] = ret elif image_urls: num_urls = len(image_urls) if num_urls > MAX_MEDIA: image_urls = image_urls[:MAX_MEDIA] logging.warning('Found %d photos! Only using the first %d: %r', num_urls, MAX_MEDIA, image_urls) preview_content += '<br /><br />' + ' '.join( '<img src="%s" />' % url for url in image_urls) if not preview: data['media_ids'] = ','.join(self.upload_images(image_urls)) if lat and lng: preview_content += ( '<div>at <a href="https://maps.google.com/maps?q=%s,%s">' '%s, %s</a></div>' % (lat, lng, lat, lng)) data['lat'] = lat data['long'] = lng if preview: return source.creation_result(content=preview_content, description=description) else: resp = self.urlopen(API_POST_TWEET, data=urllib.urlencode(data)) resp['type'] = 'comment' if is_reply else 'post' elif (verb and verb.startswith('rsvp-')) or verb == 'invite': return source.creation_result( abort=True, error_plain='Cannot publish RSVPs to Twitter.', error_html='This looks like an <a href="http://indiewebcamp.com/rsvp">RSVP</a>. ' 'Publishing events or RSVPs to Twitter is not supported.') else: return source.creation_result( abort=False, error_plain='Cannot publish type=%s, verb=%s to Twitter' % (type, verb), error_html='Cannot publish type=%s, verb=%s to Twitter' % (type, verb)) id_str = resp.get('id_str') if id_str: resp.update({'id': id_str, 'url': self.tweet_url(resp)}) elif 'url' not in resp: resp['url'] = base_url return source.creation_result(resp)
def _create(self, obj, preview=None, include_link=False): """Creates a new post, comment, like, or RSVP. https://developers.facebook.com/docs/graph-api/reference/user/feed#publish https://developers.facebook.com/docs/graph-api/reference/object/comments#publish https://developers.facebook.com/docs/graph-api/reference/object/likes#publish https://developers.facebook.com/docs/graph-api/reference/event#attending Args: obj: ActivityStreams object preview: boolean include_link: boolean Returns: a CreationResult If preview is True, the contents will be a unicode string HTML snippet. If False, it will be a dict with 'id' and 'url' keys for the newly created Facebook object. """ # TODO: validation, error handling assert preview in (False, True) type = obj.get('objectType') verb = obj.get('verb') base_id, base_url = self.base_object(obj, verb=verb) if base_id and not base_url: base_url = self.object_url(base_id) content = self._content_for_create(obj) if not content: if type == 'activity': content = verb else: return source.creation_result( abort=False, # keep looking for things to post error_plain='No content text found.', error_html='No content text found.') url = obj.get('url') if include_link and url: content += '\n\n(%s)' % url preview_content = util.linkify(content) msg_data = {'message': content.encode('utf-8')} if appengine_config.DEBUG: msg_data['privacy'] = json.dumps({'value': 'SELF'}) msg_data = urllib.urlencode(msg_data) if type == 'comment': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a Facebook status to reply to.', error_html='Could not find a Facebook status to <a href="http://indiewebcamp.com/comment">reply to</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/comment">in-reply-to</a> ' 'link a Facebook URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Facebook.') if preview: return source.creation_result( 'will <span class="verb">comment</span> <em>%s</em> on ' '<a href="%s">this post</a>:\n%s' % (preview_content, base_url, EMBED_POST % base_url)) else: resp = json.loads(self.urlopen(API_COMMENTS_URL % base_id, data=msg_data).read()) resp.update({'url': self.comment_url(base_id, resp['id']), 'type': 'comment'}) elif type == 'activity' and verb == 'like': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a Facebook status to like.', error_html='Could not find a Facebook status to <a href="http://indiewebcamp.com/favorite">like</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/favorite">like-of</a> ' 'link a Facebook URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Facebook.') if preview: return source.creation_result( 'will <span class="verb">like</span> <a href="%s">this post</a>:\n%s' % (base_url, EMBED_POST % base_url)) else: resp = json.loads(self.urlopen(API_LIKES_URL % base_id, data='').read()) assert resp == True, resp resp = {'type': 'like'} elif type == 'activity' and verb in RSVP_ENDPOINTS: if not base_url: return source.creation_result( abort=True, error_plain="This looks like an RSVP, but it's missing an " "in-reply-to link to the Facebook event.", error_html="This looks like an <a href='http://indiewebcamp.com/rsvp'>RSVP</a>, " "but it's missing an <a href='http://indiewebcamp.com/comment'>in-reply-to</a> " "link to the Facebook event.") # TODO: event invites if preview: assert verb.startswith('rsvp-') return source.creation_result( 'will <span class="verb">RSVP %s</span> to ' '<a href="%s">this event</a>.<br />' % (verb[5:], base_url)) else: resp = json.loads(self.urlopen(RSVP_ENDPOINTS[verb] % base_id, data='').read()) assert resp == True, resp resp = {'type': 'rsvp'} elif type in ('note', 'article'): if preview: return source.creation_result( 'will <span class="verb">post</span>:<br /><br />' '<em>%s</em><br />' % preview_content) else: resp = json.loads(self.urlopen(API_FEED_URL, data=msg_data).read()) resp.update({'url': self.post_url(resp), 'type': 'post'}) elif type == 'activity' and verb == 'share': return source.creation_result( abort=True, error_plain='Cannot publish shares on Facebook.', error_html='Cannot publish <a href="https://www.facebook.com/help/163779957017799">shares</a> ' 'on Facebook. This limitation is imposed by the ' '<a href="https://developers.facebook.com/docs/graph-api/reference/v2.0/object/sharedposts/#publish">Facebook Graph API</a>.') else: return source.creation_result( abort=False, error_plain='Cannot publish type=%s, verb=%s to Facebook' % (type, verb), error_html='Cannot publish type=%s, verb=%s to Facebook' % (type, verb)) if 'url' not in resp: resp['url'] = base_url return source.creation_result(resp)
def _create(self, obj, preview, include_link=False, ignore_formatting=False): """Creates or previews creating for the previous two methods. https://www.flickr.com/services/api/upload.api.html https://www.flickr.com/services/api/flickr.photos.comments.addComment.html https://www.flickr.com/services/api/flickr.favorites.add.html https://www.flickr.com/services/api/flickr.photos.people.add.html Args: obj: ActivityStreams object preview: boolean include_link: boolean Return: a CreationResult """ # photo, comment, or like type = source.object_type(obj) logging.debug('publishing object type %s to Flickr', type) link_text = '(Originally published at: %s)' % obj.get('url') image_url = util.get_first(obj, 'image', {}).get('url') video_url = util.get_first(obj, 'stream', {}).get('url') content = self._content_for_create(obj, ignore_formatting=ignore_formatting, strip_first_video_tag=bool(video_url)) if (video_url or image_url) and type in ('note', 'article'): name = obj.get('displayName') people = self._get_person_tags(obj) hashtags = [t.get('displayName') for t in obj.get('tags', []) if t.get('objectType') == 'hashtag' and t.get('displayName')] lat = obj.get('location', {}).get('latitude') lng = obj.get('location', {}).get('longitude') # if name does not represent an explicit title, then we'll just # use it as the title and wipe out the content if name and content and not mf2util.is_name_a_title(name, content): name = content content = None # add original post link if include_link: content = ((content + '\n\n') if content else '') + link_text if preview: preview_content = '' if name: preview_content += '<h4>%s</h4>' % name if content: preview_content += '<div>%s</div>' % content if hashtags: preview_content += '<div> %s</div>' % ' '.join('#' + t for t in hashtags) if people: preview_content += '<div> with %s</div>' % ', '.join( ('<a href="%s">%s</a>' % ( p.get('url'), p.get('displayName') or 'User %s' % p.get('id')) for p in people)) if lat and lng: preview_content += '<div> at <a href="https://maps.google.com/maps?q=%s,%s">%s, %s</a></div>' % (lat, lng, lat, lng) if video_url: preview_content += ('<video controls src="%s"><a href="%s">this video' '</a></video>' % (video_url, video_url)) else: preview_content += '<img src="%s" />' % image_url return source.creation_result(content=preview_content, description='post') params = [] if name: params.append(('title', name)) if content: params.append(('description', content)) if hashtags: params.append( ('tags', ','.join('"%s"' % t if ' ' in t else t for t in hashtags))) file = util.urlopen(video_url or image_url) resp = self.upload(params, file) photo_id = resp.get('id') resp.update({ 'type': 'post', 'url': self.photo_url(self.path_alias() or self.user_id(), photo_id), }) if video_url: resp['granary_message'] = \ "Note that videos take time to process before they're visible." # add person tags for person_id in sorted(p.get('id') for p in people): self.call_api_method('flickr.photos.people.add', { 'photo_id': photo_id, 'user_id': person_id, }) # add location if lat and lng: self.call_api_method('flickr.photos.geo.setLocation', { 'photo_id': photo_id, 'lat': lat, 'lon': lng, }) return source.creation_result(resp) base_obj = self.base_object(obj) base_id = base_obj.get('id') base_url = base_obj.get('url') # maybe a comment on a flickr photo? if type == 'comment' or obj.get('inReplyTo'): if not base_id: return source.creation_result( abort=True, error_plain='Could not find a photo to comment on.', error_html='Could not find a photo to <a href="http://indiewebcamp.com/reply">comment on</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/comment">in-reply-to</a> ' 'link to a Flickr photo or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Flickr.') if include_link: content += '\n\n' + link_text if preview: return source.creation_result( content=content, description='comment on <a href="%s">this photo</a>.' % base_url) resp = self.call_api_method('flickr.photos.comments.addComment', { 'photo_id': base_id, 'comment_text': content, }) resp = resp.get('comment', {}) resp.update({ 'type': 'comment', 'url': resp.get('permalink'), }) return source.creation_result(resp) if type == 'like': if not base_id: return source.creation_result( abort=True, error_plain='Could not find a photo to favorite.', error_html='Could not find a photo to <a href="http://indiewebcamp.com/like">favorite</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/like">like-of</a> ' 'link to a Flickr photo or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Flickr.') if preview: return source.creation_result( description='favorite <a href="%s">this photo</a>.' % base_url) # this method doesn't return any data self.call_api_method('flickr.favorites.add', { 'photo_id': base_id, }) # TODO should we canonicalize the base_url (e.g. removing trailing path # info like "/in/contacts/") return source.creation_result({ 'type': 'like', 'url': '%s#favorited-by-%s' % (base_url, self.user_id()), }) return source.creation_result( abort=False, error_plain='Cannot publish type=%s to Flickr.' % type, error_html='Cannot publish type=%s to Flickr.' % type)
def upload_video(self, url): """Uploads a video from web URLs using the chunked upload process. Chunked upload consists of multiple API calls: * command=INIT, which allocates the media id * command=APPEND for each 5MB block, up to 15MB total * command=FINALIZE https://dev.twitter.com/rest/reference/post/media/upload-chunked https://dev.twitter.com/rest/public/uploading-media#chunkedupload Args: url: string URL of images Returns: string media id or CreationResult on error """ video_resp = util.urlopen(url) bad_type = self._check_mime_type(url, video_resp, VIDEO_MIME_TYPES, 'MP4 videos') if bad_type: return bad_type length = video_resp.headers.get('Content-Length') if not util.is_int(length): msg = "Couldn't determine your video's size." return source.creation_result(abort=True, error_plain=msg, error_html=msg) length = int(length) if int(length) > MAX_VIDEO_SIZE: msg = "Your %sMB video is larger than Twitter's %dMB limit." % ( length // MB, MAX_VIDEO_SIZE // MB) return source.creation_result(abort=True, error_plain=msg, error_html=msg) # INIT media_id = self.urlopen(API_UPLOAD_MEDIA, data=urllib.urlencode({ 'command': 'INIT', 'media_type': 'video/mp4', 'total_bytes': length, }))['media_id_string'] # APPEND headers = twitter_auth.auth_header( API_UPLOAD_MEDIA, self.access_token_key, self.access_token_secret, 'POST') i = 0 while True: chunk = util.FileLimiter(video_resp, UPLOAD_CHUNK_SIZE) data = { 'command': 'APPEND', 'media_id': media_id, 'segment_index': i, } resp = util.requests_post(API_UPLOAD_MEDIA, data=data, files={'media': chunk}, headers=headers) resp.raise_for_status() if chunk.ateof: break i += 1 # FINALIZE self.urlopen(API_UPLOAD_MEDIA, data=urllib.urlencode({ 'command': 'FINALIZE', 'media_id': media_id, })) return media_id
def _create(self, obj, preview=None, include_link=source.OMIT_LINK, ignore_formatting=False): """Creates or previews creating a tweet, reply tweet, retweet, or favorite. https://dev.twitter.com/docs/api/1.1/post/statuses/update https://dev.twitter.com/docs/api/1.1/post/statuses/retweet/:id https://dev.twitter.com/docs/api/1.1/post/favorites/create Args: obj: ActivityStreams object preview: boolean include_link: string ignore_formatting: boolean Returns: a CreationResult If preview is True, the content will be a unicode string HTML snippet. If False, it will be a dict with 'id' and 'url' keys for the newly created Twitter object. """ assert preview in (False, True) type = obj.get('objectType') verb = obj.get('verb') base_obj = self.base_object(obj) base_id = base_obj.get('id') base_url = base_obj.get('url') is_reply = type == 'comment' or 'inReplyTo' in obj image_urls = [image.get('url') for image in 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) lat = obj.get('location', {}).get('latitude') lng = obj.get('location', {}).get('longitude') # prefer displayName over content for articles type = obj.get('objectType') base_url = self.base_object(obj).get('url') prefer_content = type == 'note' or (base_url and (type == 'comment' or obj.get('inReplyTo'))) content = self._content_for_create(obj, ignore_formatting=ignore_formatting, prefer_name=not prefer_content, strip_first_video_tag=bool(video_url)) if not content: if type == 'activity': content = verb elif has_media: content = '' else: return source.creation_result( abort=False, # keep looking for things to publish, error_plain='No content text found.', error_html='No content text found.') if is_reply and base_url: # Twitter *used* to require replies to include an @-mention of the # original tweet's author # https://dev.twitter.com/docs/api/1.1/post/statuses/update#api-param-in_reply_to_status_id # ...but now we use the auto_populate_reply_metadata query param instead: # https://dev.twitter.com/overview/api/upcoming-changes-to-tweets # the embed URL in the preview can't start with mobile. or www., so just # hard-code it to twitter.com. index #1 is netloc. parsed = urlparse.urlparse(base_url) parts = parsed.path.split('/') if len(parts) < 2 or not parts[1]: raise ValueError('Could not determine author of in-reply-to URL %s' % base_url) reply_to_prefix = '@%s ' % parts[1].lower() if content.lower().startswith(reply_to_prefix): content = content[len(reply_to_prefix):] parsed = list(parsed) parsed[1] = self.DOMAIN base_url = urlparse.urlunparse(parsed) # need a base_url with the tweet id for the embed HTML below. do this # *after* checking the real base_url for in-reply-to author username. if base_id and not base_url: base_url = 'https://twitter.com/-/statuses/' + base_id if is_reply and not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to reply to.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/reply">reply to</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/comment">in-reply-to</a> ' 'link a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') # truncate and ellipsize content if it's over the character # count. URLs will be t.co-wrapped, so include that when counting. content = self._truncate( content, obj.get('url'), include_link, type) # linkify defaults to Twitter's link shortening behavior preview_content = util.linkify(content, pretty=True, skip_bare_cc_tlds=True) if type == 'activity' and verb == 'like': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to like.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/favorite">favorite</a>. ' 'Check that your post has a like-of link to a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') if preview: return source.creation_result( description='<span class="verb">favorite</span> <a href="%s">' 'this tweet</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: data = urllib.urlencode({'id': base_id}) self.urlopen(API_POST_FAVORITE, data=data) resp = {'type': 'like'} elif type == 'activity' and verb == 'share': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to retweet.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/repost">retweet</a>. ' 'Check that your post has a repost-of link to a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') if preview: return source.creation_result( description='<span class="verb">retweet</span> <a href="%s">' 'this tweet</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: data = urllib.urlencode({'id': base_id}) resp = self.urlopen(API_POST_RETWEET % base_id, data=data) resp['type'] = 'repost' elif type in ('note', 'article') or is_reply: # a tweet content = unicode(content).encode('utf-8') data = {'status': content} if is_reply: description = \ '<span class="verb">@-reply</span> to <a href="%s">this tweet</a>:\n%s' % ( base_url, self.embed_post(base_obj)) data.update({ 'in_reply_to_status_id': base_id, 'auto_populate_reply_metadata': 'true', }) else: description = '<span class="verb">tweet</span>:' if video_url: preview_content += ('<br /><br /><video controls src="%s"><a href="%s">' 'this video</a></video>' % (video_url, video_url)) if not preview: ret = self.upload_video(video_url) if isinstance(ret, source.CreationResult): return ret data['media_ids'] = ret elif image_urls: num_urls = len(image_urls) if num_urls > MAX_MEDIA: image_urls = image_urls[:MAX_MEDIA] logging.warning('Found %d photos! Only using the first %d: %r', num_urls, MAX_MEDIA, image_urls) preview_content += '<br /><br />' + ' '.join( '<img src="%s" />' % url for url in image_urls) if not preview: ret = self.upload_images(image_urls) if isinstance(ret, source.CreationResult): return ret data['media_ids'] = ','.join(ret) if lat and lng: preview_content += ( '<div>at <a href="https://maps.google.com/maps?q=%s,%s">' '%s, %s</a></div>' % (lat, lng, lat, lng)) data['lat'] = lat data['long'] = lng if preview: return source.creation_result(content=preview_content, description=description) else: resp = self.urlopen(API_POST_TWEET, data=urllib.urlencode(data)) resp['type'] = 'comment' if is_reply else 'post' elif (verb and verb.startswith('rsvp-')) or verb == 'invite': return source.creation_result( abort=True, error_plain='Cannot publish RSVPs to Twitter.', error_html='This looks like an <a href="http://indiewebcamp.com/rsvp">RSVP</a>. ' 'Publishing events or RSVPs to Twitter is not supported.') else: return source.creation_result( abort=False, error_plain='Cannot publish type=%s, verb=%s to Twitter' % (type, verb), error_html='Cannot publish type=%s, verb=%s to Twitter' % (type, verb)) id_str = resp.get('id_str') if id_str: resp.update({'id': id_str, 'url': self.tweet_url(resp)}) elif 'url' not in resp: resp['url'] = base_url return source.creation_result(resp)
def _create(self, obj, preview, include_link=source.OMIT_LINK, ignore_formatting=False): """Creates or previews creating for the previous two methods. https://www.flickr.com/services/api/upload.api.html https://www.flickr.com/services/api/flickr.photos.comments.addComment.html https://www.flickr.com/services/api/flickr.favorites.add.html https://www.flickr.com/services/api/flickr.photos.people.add.html Args: obj: ActivityStreams object preview: boolean include_link: string ignore_formatting: boolean Return: a CreationResult """ # photo, comment, or like type = source.object_type(obj) logging.debug('publishing object type %s to Flickr', type) link_text = '(Originally published at: %s)' % obj.get('url') image_url = util.get_first(obj, 'image', {}).get('url') video_url = util.get_first(obj, 'stream', {}).get('url') content = self._content_for_create( obj, ignore_formatting=ignore_formatting, strip_first_video_tag=bool(video_url)) if (video_url or image_url) and type in ('note', 'article'): name = obj.get('displayName') people = self._get_person_tags(obj) hashtags = [ t.get('displayName') for t in obj.get('tags', []) if t.get('objectType') == 'hashtag' and t.get('displayName') ] lat = obj.get('location', {}).get('latitude') lng = obj.get('location', {}).get('longitude') # if name does not represent an explicit title, then we'll just # use it as the title and wipe out the content if name and content and not mf2util.is_name_a_title(name, content): name = content content = None # add original post link if include_link == source.INCLUDE_LINK: content = ((content + '\n\n') if content else '') + link_text if preview: preview_content = '' if name: preview_content += '<h4>%s</h4>' % name if content: preview_content += '<div>%s</div>' % content if hashtags: preview_content += '<div> %s</div>' % ' '.join( '#' + t for t in hashtags) if people: preview_content += '<div> with %s</div>' % ', '.join( ('<a href="%s">%s</a>' % (p.get('url'), p.get('displayName') or 'User %s' % p.get('id')) for p in people)) if lat and lng: preview_content += '<div> at <a href="https://maps.google.com/maps?q=%s,%s">%s, %s</a></div>' % ( lat, lng, lat, lng) if video_url: preview_content += ( '<video controls src="%s"><a href="%s">this video' '</a></video>' % (video_url, video_url)) else: preview_content += '<img src="%s" />' % image_url return source.creation_result(content=preview_content, description='post') params = [] if name: params.append(('title', name)) if content: params.append(('description', content.encode('utf-8'))) if hashtags: params.append(('tags', ','.join( ('"%s"' % t if ' ' in t else t).encode('utf-8') for t in hashtags))) file = util.urlopen(video_url or image_url) try: resp = self.upload(params, file) except requests.exceptions.ConnectionError as e: if e.args[0].message.startswith( 'Request exceeds 10 MiB limit'): msg = 'Sorry, photos and videos must be under 10MB.' return source.creation_result(error_plain=msg, error_html=msg) else: raise photo_id = resp.get('id') resp.update({ 'type': 'post', 'url': self.photo_url(self.path_alias() or self.user_id(), photo_id), }) if video_url: resp['granary_message'] = \ "Note that videos take time to process before they're visible." # add person tags for person_id in sorted(p.get('id') for p in people): self.call_api_method('flickr.photos.people.add', { 'photo_id': photo_id, 'user_id': person_id, }) # add location if lat and lng: self.call_api_method('flickr.photos.geo.setLocation', { 'photo_id': photo_id, 'lat': lat, 'lon': lng, }) return source.creation_result(resp) base_obj = self.base_object(obj) base_id = base_obj.get('id') base_url = base_obj.get('url') # maybe a comment on a flickr photo? if type == 'comment' or obj.get('inReplyTo'): if not base_id: return source.creation_result( abort=True, error_plain='Could not find a photo to comment on.', error_html= 'Could not find a photo to <a href="http://indiewebcamp.com/reply">comment on</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/comment">in-reply-to</a> ' 'link to a Flickr photo or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Flickr.' ) if include_link == source.INCLUDE_LINK: content += '\n\n' + link_text if preview: return source.creation_result( content=content, description='comment on <a href="%s">this photo</a>.' % base_url) resp = self.call_api_method( 'flickr.photos.comments.addComment', { 'photo_id': base_id, 'comment_text': content.encode('utf-8'), }) resp = resp.get('comment', {}) resp.update({ 'type': 'comment', 'url': resp.get('permalink'), }) return source.creation_result(resp) if type == 'like': if not base_id: return source.creation_result( abort=True, error_plain='Could not find a photo to favorite.', error_html= 'Could not find a photo to <a href="http://indiewebcamp.com/like">favorite</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/like">like-of</a> ' 'link to a Flickr photo or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Flickr.' ) if preview: return source.creation_result( description='favorite <a href="%s">this photo</a>.' % base_url) # this method doesn't return any data self.call_api_method('flickr.favorites.add', { 'photo_id': base_id, }) # TODO should we canonicalize the base_url (e.g. removing trailing path # info like "/in/contacts/") return source.creation_result({ 'type': 'like', 'url': '%s#favorited-by-%s' % (base_url, self.user_id()), }) return source.creation_result( abort=False, error_plain='Cannot publish type=%s to Flickr.' % type, error_html='Cannot publish type=%s to Flickr.' % type)
def _create(self, obj, preview=None, include_link=False): """Creates or previews creating a tweet, reply tweet, retweet, or favorite. https://dev.twitter.com/docs/api/1.1/post/statuses/update https://dev.twitter.com/docs/api/1.1/post/statuses/retweet/:id https://dev.twitter.com/docs/api/1.1/post/favorites/create Args: obj: ActivityStreams object preview: boolean include_link: boolean Returns: a CreationResult If preview is True, the content will be a unicode string HTML snippet. If False, it will be a dict with 'id' and 'url' keys for the newly created Twitter object. """ # TODO: validation, error handling assert preview in (False, True) type = obj.get('objectType') verb = obj.get('verb') base_id, base_url = self.base_object(obj) content = self._content_for_create(obj) if not content: if type == 'activity': content = verb else: return source.creation_result( abort=False, # keep looking for things to publish, error_plain='No content text found.', error_html='No content text found.') is_reply = type == 'comment' or 'inReplyTo' in obj if is_reply and base_url: # extract username from in-reply-to URL so we can @-mention it, if it's # not already @-mentioned, since Twitter requires that to make our new # tweet a reply. # https://dev.twitter.com/docs/api/1.1/post/statuses/update#api-param-in_reply_to_status_id # TODO: this doesn't handle an in-reply-to username that's a prefix of # another username already mentioned, e.g. in reply to @foo when content # includes @foobar. parsed = urlparse.urlparse(base_url) parts = parsed.path.split('/') if len(parts) < 2 or not parts[1]: raise ValueError('Could not determine author of in-reply-to URL %s' % base_url) mention = '@' + parts[1] if mention not in content: content = mention + ' ' + content # the embed URL in the preview can't start with mobile. or www., so just # hard-code it to twitter.com. index #1 is netloc. parsed = list(parsed) parsed[1] = self.DOMAIN base_url = urlparse.urlunparse(parsed) # need a base_url with the tweet id for the embed HTML below. do this # *after* checking the real base_url for in-reply-to author username. if base_id and not base_url: base_url = 'https://twitter.com/-/statuses/' + base_id # truncate and ellipsize content if it's over the character count. URLs will # be t.co-wrapped, so include that when counting. links = set(util.extract_links(content)) max = MAX_TWEET_LENGTH include_url = obj.get('url') if include_link else None if include_url: max -= TCO_LENGTH + 3 length = 0 tokens = content.split() for i, token in enumerate(tokens): # extract_links() strips trailing slashes from URLs, so do the same here # so we can compare. as_url = token[:-1] if token.endswith('/') else token length += (TCO_LENGTH if as_url in links else len(token)) if i > 0: length += 1 # space between tokens if length > max: break else: i = len(tokens) # normalize whitespace # TODO: user opt in to preserve original whitespace (newlines, etc) content = ' '.join(tokens[:i]) if i < len(tokens): content += u'…' if include_url: content += ' (%s)' % include_url # linkify defaults to Twitter's link shortening behavior preview_content = util.linkify(content, pretty=True) if is_reply: if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to reply to.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/reply">reply to</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/comment">in-reply-to</a> ' 'link a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') if preview: return source.creation_result( 'will <span class="verb">@-reply</span>:<br /><br />\n<em>%s</em>\n' '<br /><br />...to <a href="%s">this tweet</a>:\n%s' % (preview_content, base_url, EMBED_TWEET % base_url)) else: content = unicode(content).encode('utf-8') data = urllib.urlencode({'status': content, 'in_reply_to_status_id': base_id}) resp = json.loads(self.urlopen(API_POST_TWEET_URL, data=data).read()) resp['type'] = 'comment' elif type == 'activity' and verb == 'like': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to like.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/favorite">favorite</a>. ' 'Check that your post has a like-of link to a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') if preview: return source.creation_result( 'will <span class="verb">favorite</span> <a href="%s">this tweet</a>:\n%s' % (base_url, EMBED_TWEET % base_url)) else: data = urllib.urlencode({'id': base_id}) self.urlopen(API_POST_FAVORITE_URL, data=data).read() resp = {'type': 'like'} elif type == 'activity' and verb == 'share': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to retweet.', error_html='Could not find a tweet to <a href="http://indiewebcamp.com/repost">retweet</a>. ' 'Check that your post has a repost-of link to a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.') if preview: return source.creation_result( 'will <span class="verb">retweet</span> <a href="%s">this tweet</a>:\n%s' % (base_url, EMBED_TWEET % base_url)) else: data = urllib.urlencode({'id': base_id}) resp = json.loads(self.urlopen(API_POST_RETWEET_URL % base_id, data=data).read()) resp['type'] = 'repost' elif type in ('note', 'article') and obj.get('image'): image_url = obj.get('image').get('url') if preview: return source.creation_result( 'will <span class="verb">tweet</span> with photo:<br /><br />' '<em>%s</em><br /><img src="%s"/><br />' % (preview_content, image_url)) else: content = unicode(content).encode('utf-8') data = {'status': content} files = {'media[]': urllib2.urlopen(image_url)} headers = twitter_auth.auth_header(API_POST_MEDIA_URL, self.access_token_key, self.access_token_secret, 'POST') resp = json.loads(requests.post(API_POST_MEDIA_URL, data=data, files=files, headers=headers, timeout=HTTP_TIMEOUT).text) resp['type'] = 'post' elif type in ('note', 'article'): if preview: return source.creation_result( 'will <span class="verb">tweet</span>:<br /><br />' '<em>%s</em><br />' % preview_content) else: content = unicode(content).encode('utf-8') data = urllib.urlencode({'status': content}) resp = json.loads(self.urlopen(API_POST_TWEET_URL, data=data).read()) resp['type'] = 'post' elif (verb and verb.startswith('rsvp-')) or verb == 'invite': return source.creation_result( abort=True, error_plain='Cannot publish RSVPs to Twitter.', error_html='This looks like an <a href="http://indiewebcamp.com/rsvp">RSVP</a>. ' 'Publishing events or RSVPs to Twitter is not supported.') else: return source.creation_result( abort=False, error_plain='Cannot publish type=%s, verb=%s to Twitter' % (type, verb), error_html='Cannot publish type=%s, verb=%s to Twitter' % (type, verb)) id_str = resp.get('id_str') if id_str: resp.update({'id': id_str, 'url': self.tweet_url(resp)}) elif 'url' not in resp: resp['url'] = base_url return source.creation_result(resp)
def _create(self, obj, include_link=False, preview=None, ignore_formatting=False): """Creates a new comment or like. The OAuth access token must have been created with scope=comments+likes (or just one, respectively). http://instagram.com/developer/authentication/#scope To comment, you need to apply for access: https://docs.google.com/spreadsheet/viewform?formkey=dFNydmNsUUlEUGdySWFWbGpQczdmWnc6MQ http://instagram.com/developer/endpoints/comments/#post_media_comments http://instagram.com/developer/endpoints/likes/#post_likes Args: obj: ActivityStreams object include_link: boolean preview: boolean Returns: a CreationResult. if successful, content will have and 'id' and 'url' keys for the newly created Instagram object """ # TODO: validation, error handling type = obj.get('objectType') verb = obj.get('verb') base_obj = self.base_object(obj) base_id = base_obj.get('id') base_url = base_obj.get('url') logging.debug( 'instagram create request with type=%s, verb=%s, id=%s, url=%s', type, verb, base_id, base_url) if type == 'comment': # most applications are not approved by instagram to create comments; # better to give a useful error message than try and fail. if not self.allow_comment_creation: return source.creation_result( abort=True, error_plain='Cannot publish comments on Instagram', error_html='<a href="http://instagram.com/developer/endpoints/comments/#post_media_comments">Cannot publish comments</a> on Instagram. The Instagram API technically supports creating comments, but <a href="http://stackoverflow.com/a/26889101/682648">anecdotal</a> <a href="http://stackoverflow.com/a/20229275/682648">evidence</a> suggests they are very selective about which applications they approve to do so.') content = obj.get('content', '').encode('utf-8') content = content if ignore_formatting else self._html_to_text(content) if preview: return source.creation_result( content=content, description='<span class="verb">comment</span> on <a href="%s">' 'this post</a>:\n%s' % (base_url, self.embed_post(base_obj))) self.urlopen(API_COMMENT_URL % base_id, data=urllib.urlencode({ 'access_token': self.access_token, 'text': content, })) # response will be empty even on success, see # http://instagram.com/developer/endpoints/comments/#post_media_comments. # TODO where can we get the comment id? obj = self.comment_to_object({}, base_id, None) return source.creation_result(obj) elif type == 'activity' and verb == 'like': if not base_url: return source.creation_result( abort=True, error_plain='Could not find an Instagram post to like.', error_html='Could not find an Instagram post to <a href="http://indiewebcamp.com/like">like</a>. ' 'Check that your post has a like-of link to an Instagram URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Instagram.') if preview: return source.creation_result( description='<span class="verb">like</span> <a href="%s">' 'this post</a>:\n%s' % (base_url, self.embed_post(base_obj))) if not base_id: shortcode = self.post_id(base_url) logging.debug('looking up media by shortcode %s', shortcode) media_entry = self.urlopen(API_MEDIA_SHORTCODE_URL % shortcode) or {} base_id = media_entry.get('id') base_url = media_entry.get('link') logging.info('posting like for media id id=%s, url=%s', base_id, base_url) # no response other than success/failure self.urlopen(API_MEDIA_LIKES_URL % base_id, data=urllib.urlencode({ 'access_token': self.access_token })) # TODO use the stored user_json rather than looking it up each time. # oauth-dropins auth_entities should have the user_json. me = self.urlopen(API_USER_URL % 'self') return source.creation_result( self.like_to_object(me, base_id, base_url)) return source.creation_result( abort=True, error_plain='Cannot publish this post on Instagram.', error_html='Cannot publish this post on Instagram. Instagram <a href="http://instagram.com/developer/endpoints/media/#get_media_popular">does not support</a> posting photos or videos from 3rd party applications.')
def _create(self, obj, preview=None, include_link=False): """Creates a new post, comment, like, or RSVP. https://developers.facebook.com/docs/graph-api/reference/user/feed#publish https://developers.facebook.com/docs/graph-api/reference/object/comments#publish https://developers.facebook.com/docs/graph-api/reference/object/likes#publish https://developers.facebook.com/docs/graph-api/reference/event#attending Args: obj: ActivityStreams object preview: boolean include_link: boolean Returns: a CreationResult If preview is True, the contents will be a unicode string HTML snippet. If False, it will be a dict with 'id' and 'url' keys for the newly created Facebook object. """ # TODO: validation, error handling assert preview in (False, True) type = obj.get('objectType') verb = obj.get('verb') base_obj = self.base_object(obj, verb=verb) base_id = base_obj.get('id') base_type = base_obj.get('objectType') base_url = base_obj.get('url') if base_id and not base_url: base_url = base_obj['url'] = self.object_url(base_id) content = self._content_for_create(obj) if not content: if type == 'activity': content = verb else: return source.creation_result( abort=False, # keep looking for things to post error_plain='No content text found.', error_html='No content text found.') image_url = obj.get('image', {}).get('url') url = obj.get('url') if include_link and url: content += '\n\n(Originally published at: %s)' % url preview_content = util.linkify(content) if image_url: preview_content += '<br /><br /><img src="%s" />' % image_url msg_data = {'message': content.encode('utf-8')} if appengine_config.DEBUG: msg_data['privacy'] = json.dumps({'value': 'SELF'}) if type == 'comment': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a Facebook status to reply to.', error_html='Could not find a Facebook status to <a href="http://indiewebcamp.com/comment">reply to</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/comment">in-reply-to</a> ' 'link a Facebook URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Facebook.') if preview: desc = """\ <span class="verb">comment</span> on <a href="%s">this post</a>: <br /><br />%s<br />""" % (base_url, self.embed_post(base_obj)) return source.creation_result(content=preview_content, description=desc) else: if image_url: msg_data['attachment_url'] = image_url resp = self.urlopen(API_COMMENTS % base_id, data=urllib.urlencode(msg_data)) url = self.comment_url(base_id, resp['id'], post_author_id=base_obj.get('author', {}).get('id')) resp.update({'url': url, 'type': 'comment'}) elif type == 'activity' and verb == 'like': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a Facebook status to like.', error_html='Could not find a Facebook status to <a href="http://indiewebcamp.com/favorite">like</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/favorite">like-of</a> ' 'link a Facebook URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Facebook.') elif base_type in ('person', 'page'): return source.creation_result( abort=True, error_plain="Sorry, the Facebook API doesn't support liking pages.", error_html='Sorry, <a href="https://developers.facebook.com/docs/graph-api/reference/v2.2/user/likes#publish">' "the Facebook API doesn't support liking pages</a>.") if preview: desc = '<span class="verb">like</span> ' if base_type == 'comment': comment = self.comment_to_object(self.urlopen(base_id)) author = comment.get('author', '') if author: author = self.embed_actor(author) + ':\n' desc += '<a href="%s">this comment</a>:\n<br /><br />%s%s<br />' % ( base_url, author, comment.get('content')) else: desc += '<a href="%s">this post</a>:\n<br /><br />%s<br />' % ( base_url, self.embed_post(base_obj)) return source.creation_result(description=desc) else: resp = self.urlopen(API_LIKES % base_id, data='') assert resp.get('success'), resp resp = {'type': 'like'} elif type == 'activity' and verb in RSVP_ENDPOINTS: if not base_url: return source.creation_result( abort=True, error_plain="This looks like an RSVP, but it's missing an " "in-reply-to link to the Facebook event.", error_html="This looks like an <a href='http://indiewebcamp.com/rsvp'>RSVP</a>, " "but it's missing an <a href='http://indiewebcamp.com/comment'>in-reply-to</a> " "link to the Facebook event.") # TODO: event invites if preview: assert verb.startswith('rsvp-') desc = ('<span class="verb">RSVP %s</span> to <a href="%s">this event</a>.' % (verb[5:], base_url)) return source.creation_result(description=desc) else: resp = self.urlopen(RSVP_ENDPOINTS[verb] % base_id, data='') assert resp.get('success'), resp resp = {'type': 'rsvp'} elif type in ('note', 'article') and image_url: if preview: return source.creation_result(content=preview_content, description='<span class="verb">post</span>:') else: msg_data['url'] = image_url if appengine_config.DEBUG: msg_data['privacy'] = json.dumps({'value': 'SELF'}) resp = self.urlopen(API_PHOTOS, data=urllib.urlencode(msg_data)) resp.update({'url': self.post_url(resp), 'type': 'post'}) elif type in ('note', 'article'): if preview: return source.creation_result(content=preview_content, description='<span class="verb">post</span>:') else: resp = self.urlopen(API_FEED, data=urllib.urlencode(msg_data)) resp.update({'url': self.post_url(resp), 'type': 'post'}) elif type == 'activity' and verb == 'share': return source.creation_result( abort=True, error_plain='Cannot publish shares on Facebook.', error_html='Cannot publish <a href="https://www.facebook.com/help/163779957017799">shares</a> ' 'on Facebook. This limitation is imposed by the ' '<a href="https://developers.facebook.com/docs/graph-api/reference/object/sharedposts/#publish">Facebook Graph API</a>.') else: return source.creation_result( abort=False, error_plain='Cannot publish type=%s, verb=%s to Facebook' % (type, verb), error_html='Cannot publish type=%s, verb=%s to Facebook' % (type, verb)) if 'url' not in resp: resp['url'] = base_url return source.creation_result(resp)
def _create(self, obj, preview=None, include_link=False): """Creates or previews creating a tweet, reply tweet, retweet, or favorite. https://dev.twitter.com/docs/api/1.1/post/statuses/update https://dev.twitter.com/docs/api/1.1/post/statuses/retweet/:id https://dev.twitter.com/docs/api/1.1/post/favorites/create Args: obj: ActivityStreams object preview: boolean include_link: boolean Returns: a CreationResult If preview is True, the content will be a unicode string HTML snippet. If False, it will be a dict with 'id' and 'url' keys for the newly created Twitter object. """ assert preview in (False, True) type = obj.get('objectType') verb = obj.get('verb') base_obj = self.base_object(obj) base_id = base_obj.get('id') base_url = base_obj.get('url') is_reply = type == 'comment' or 'inReplyTo' in obj has_picture = obj.get('image') and (type in ('note', 'article') or is_reply) content = self._content_for_create(obj) if not content: if type == 'activity': content = verb elif has_picture: content = '' else: return source.creation_result( abort=False, # keep looking for things to publish, error_plain='No content text found.', error_html='No content text found.') if is_reply and base_url: # extract username from in-reply-to URL so we can @-mention it, if it's # not already @-mentioned, since Twitter requires that to make our new # tweet a reply. # https://dev.twitter.com/docs/api/1.1/post/statuses/update#api-param-in_reply_to_status_id # TODO: this doesn't handle an in-reply-to username that's a prefix of # another username already mentioned, e.g. in reply to @foo when content # includes @foobar. parsed = urlparse.urlparse(base_url) parts = parsed.path.split('/') if len(parts) < 2 or not parts[1]: raise ValueError( 'Could not determine author of in-reply-to URL %s' % base_url) mention = '@' + parts[1] if mention.lower() not in content.lower(): content = mention + ' ' + content # the embed URL in the preview can't start with mobile. or www., so just # hard-code it to twitter.com. index #1 is netloc. parsed = list(parsed) parsed[1] = self.DOMAIN base_url = urlparse.urlunparse(parsed) # need a base_url with the tweet id for the embed HTML below. do this # *after* checking the real base_url for in-reply-to author username. if base_id and not base_url: base_url = 'https://twitter.com/-/statuses/' + base_id if is_reply and not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to reply to.', error_html= 'Could not find a tweet to <a href="http://indiewebcamp.com/reply">reply to</a>. ' 'Check that your post has an <a href="http://indiewebcamp.com/comment">in-reply-to</a> ' 'link a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.' ) # truncate and ellipsize content if it's over the character # count. URLs will be t.co-wrapped, so include that when counting. include_url = obj.get('url') if include_link else None content = self._truncate(content, include_url, has_picture) # linkify defaults to Twitter's link shortening behavior preview_content = util.linkify(content, pretty=True, skip_bare_cc_tlds=True) if has_picture: image_url = obj.get('image').get('url') if preview: if is_reply: desc = ( '<span class="verb">@-reply</span> to <a href="%s">this tweet' '</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: desc = '<span class="verb">tweet</span>:' if preview_content: preview_content += '<br /><br />' return source.creation_result(content='%s<img src="%s" />' % (preview_content, image_url), description=desc) else: content = unicode(content).encode('utf-8') data = {'status': content} if is_reply: data['in_reply_to_status_id'] = base_id files = {'media[]': urllib2.urlopen(image_url)} headers = twitter_auth.auth_header(API_POST_MEDIA_URL, self.access_token_key, self.access_token_secret, 'POST') resp = requests.post(API_POST_MEDIA_URL, data=data, files=files, headers=headers, timeout=HTTP_TIMEOUT) resp.raise_for_status() resp = json.loads(resp.text) resp['type'] = 'comment' if is_reply else 'post' elif is_reply: if preview: return source.creation_result( content=preview_content, description= '<span class="verb">@-reply</span> to <a href="%s">this tweet' '</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: content = unicode(content).encode('utf-8') data = urllib.urlencode({ 'status': content, 'in_reply_to_status_id': base_id }) resp = self.urlopen(API_POST_TWEET_URL, data=data) resp['type'] = 'comment' elif type == 'activity' and verb == 'like': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to like.', error_html= 'Could not find a tweet to <a href="http://indiewebcamp.com/favorite">favorite</a>. ' 'Check that your post has a like-of link to a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.' ) if preview: return source.creation_result( description= '<span class="verb">favorite</span> <a href="%s">' 'this tweet</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: data = urllib.urlencode({'id': base_id}) self.urlopen(API_POST_FAVORITE_URL, data=data) resp = {'type': 'like'} elif type == 'activity' and verb == 'share': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a tweet to retweet.', error_html= 'Could not find a tweet to <a href="http://indiewebcamp.com/repost">retweet</a>. ' 'Check that your post has a repost-of link to a Twitter URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Twitter.' ) if preview: return source.creation_result( description='<span class="verb">retweet</span> <a href="%s">' 'this tweet</a>:\n%s' % (base_url, self.embed_post(base_obj))) else: data = urllib.urlencode({'id': base_id}) resp = self.urlopen(API_POST_RETWEET_URL % base_id, data=data) resp['type'] = 'repost' elif type in ('note', 'article'): if preview: return source.creation_result( content=preview_content, description='<span class="verb">tweet</span>:') else: content = unicode(content).encode('utf-8') data = urllib.urlencode({'status': content}) resp = self.urlopen(API_POST_TWEET_URL, data=data) resp['type'] = 'post' elif (verb and verb.startswith('rsvp-')) or verb == 'invite': return source.creation_result( abort=True, error_plain='Cannot publish RSVPs to Twitter.', error_html= 'This looks like an <a href="http://indiewebcamp.com/rsvp">RSVP</a>. ' 'Publishing events or RSVPs to Twitter is not supported.') else: return source.creation_result( abort=False, error_plain='Cannot publish type=%s, verb=%s to Twitter' % (type, verb), error_html='Cannot publish type=%s, verb=%s to Twitter' % (type, verb)) id_str = resp.get('id_str') if id_str: resp.update({'id': id_str, 'url': self.tweet_url(resp)}) elif 'url' not in resp: resp['url'] = base_url return source.creation_result(resp)
def _create(self, obj, include_link=source.OMIT_LINK, preview=None, ignore_formatting=False): """Creates a new comment or like. The OAuth access token must have been created with scope=comments+likes (or just one, respectively). http://instagram.com/developer/authentication/#scope To comment, you need to apply for access: https://docs.google.com/spreadsheet/viewform?formkey=dFNydmNsUUlEUGdySWFWbGpQczdmWnc6MQ http://instagram.com/developer/endpoints/comments/#post_media_comments http://instagram.com/developer/endpoints/likes/#post_likes Args: obj: ActivityStreams object include_link: string preview: boolean Returns: a CreationResult. if successful, content will have and 'id' and 'url' keys for the newly created Instagram object """ # TODO: validation, error handling type = obj.get('objectType') verb = obj.get('verb') base_obj = self.base_object(obj) base_id = base_obj.get('id') base_url = base_obj.get('url') logging.debug( 'instagram create request with type=%s, verb=%s, id=%s, url=%s', type, verb, base_id, base_url) if type == 'comment': # most applications are not approved by instagram to create comments; # better to give a useful error message than try and fail. if not self.allow_comment_creation: return source.creation_result( abort=True, error_plain='Cannot publish comments on Instagram', error_html='<a href="http://instagram.com/developer/endpoints/comments/#post_media_comments">Cannot publish comments</a> on Instagram. The Instagram API technically supports creating comments, but <a href="http://stackoverflow.com/a/26889101/682648">anecdotal</a> <a href="http://stackoverflow.com/a/20229275/682648">evidence</a> suggests they are very selective about which applications they approve to do so.') content = obj.get('content', '').encode('utf-8') content = content if ignore_formatting else source.html_to_text(content) if preview: return source.creation_result( content=content, description='<span class="verb">comment</span> on <a href="%s">' 'this post</a>:\n%s' % (base_url, self.embed_post(base_obj))) self.urlopen(API_COMMENT_URL % base_id, data=urllib.urlencode({ 'access_token': self.access_token, 'text': content, })) # response will be empty even on success, see # http://instagram.com/developer/endpoints/comments/#post_media_comments. # TODO where can we get the comment id? obj = self.comment_to_object({}, base_id, None) return source.creation_result(obj) elif type == 'activity' and verb == 'like': if not base_url: return source.creation_result( abort=True, error_plain='Could not find an Instagram post to like.', error_html='Could not find an Instagram post to <a href="http://indiewebcamp.com/like">like</a>. ' 'Check that your post has a like-of link to an Instagram URL or to an original post that publishes a ' '<a href="http://indiewebcamp.com/rel-syndication">rel-syndication</a> link to Instagram.') if preview: return source.creation_result( description='<span class="verb">like</span> <a href="%s">' 'this post</a>:\n%s' % (base_url, self.embed_post(base_obj))) if not base_id: shortcode = self.post_id(base_url) logging.debug('looking up media by shortcode %s', shortcode) media_entry = self.urlopen(API_MEDIA_SHORTCODE_URL % shortcode) or {} base_id = media_entry.get('id') base_url = media_entry.get('link') logging.info('posting like for media id id=%s, url=%s', base_id, base_url) # no response other than success/failure self.urlopen(API_MEDIA_LIKES_URL % base_id, data=urllib.urlencode({ 'access_token': self.access_token })) # TODO use the stored user_json rather than looking it up each time. # oauth-dropins auth_entities should have the user_json. me = self.urlopen(API_USER_URL % 'self') return source.creation_result( self.like_to_object(me, base_id, base_url)) return source.creation_result( abort=True, error_plain='Cannot publish this post on Instagram. Instagram does not support posting photos or videos from 3rd party applications.', error_html='Cannot publish this post on Instagram. Instagram <a href="http://instagram.com/developer/endpoints/media/#get_media_popular">does not support</a> posting photos or videos from 3rd party applications.')