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, preview=None, include_link=source.OMIT_LINK, ignore_formatting=False): """Creates or previews a status (aka toot), reply, boost (aka reblog), or favorite. https://docs.joinmastodon.org/api/rest/statuses/ Based on :meth:`Twitter._create`. Args: obj: ActivityStreams object preview: boolean include_link: string ignore_formatting: boolean Returns: 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 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 obj.get('inReplyTo') is_rsvp = (verb and verb.startswith('rsvp-')) or verb == 'invite' atts = obj.get('attachments', []) images = util.dedupe_urls( util.get_list(obj, 'image') + [a for a in atts if a.get('objectType') == 'image']) videos = util.dedupe_urls( [obj] + [a for a in atts if a.get('objectType') == 'video'], key='stream') has_media = (images or videos) and (type in ('note', 'article') or is_reply) # prefer displayName over content for articles # # TODO: handle activities as well as objects? ie pull out ['object'] here if # necessary? type = obj.get('objectType') prefer_content = type == 'note' or (base_url and is_reply) preview_description = '' content = self._content_for_create(obj, ignore_formatting=ignore_formatting, prefer_name=not prefer_content) if not content: if type == 'activity' and not is_rsvp: 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.') post_label = '%s %s' % (self.NAME, self.TYPE_LABELS['post']) if is_reply and not base_url: return source.creation_result( abort=True, error_plain='Could not find a %s to reply to.' % post_label, error_html= 'Could not find a %s to <a href="http://indiewebcamp.com/reply">reply to</a>. Check that your post has the right <a href="http://indiewebcamp.com/comment">in-reply-to</a> link.' % post_label) # truncate and ellipsize content if necessary # TODO: don't count domains in remote mentions. # https://docs.joinmastodon.org/usage/basics/#text content = self.truncate(content, obj.get('url'), include_link, type) # linkify user mentions def linkify_mention(match): split = match.group(1).split('@') username = split[0] instance = ('https://' + split[1]) if len(split) > 1 else self.instance url = urllib.parse.urljoin(instance, '/@' + username) return '<a href="%s">@%s</a>' % (url, username) preview_content = MENTION_RE.sub(linkify_mention, content) # linkify (defaults to twitter's behavior) preview_content = util.linkify(preview_content, pretty=True, skip_bare_cc_tlds=True) tags_url = urllib.parse.urljoin(self.instance, '/tags') preview_content = HASHTAG_RE.sub( r'\1<a href="%s/\2">#\2</a>' % tags_url, preview_content) # switch on activity type if type == 'activity' and verb == 'like': if not base_url: return source.creation_result( abort=True, error_plain='Could not find a %s to %s.' % (post_label, self.TYPE_LABELS['like']), error_html= 'Could not find a %s to <a href="http://indiewebcamp.com/like">%s</a>. Check that your post has the right <a href="http://indiewebcamp.com/like">u-like-of link</a>.' % (post_label, self.TYPE_LABELS['like'])) if preview: preview_description += '<span class="verb">%s</span> <a href="%s">this %s</a>: %s' % ( self.TYPE_LABELS['like'], base_url, self.TYPE_LABELS['post'], self.embed_post(base_obj)) return source.creation_result(description=preview_description) else: resp = self._post(API_FAVORITE % base_id) 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 %s to %s.' % (post_label, self.TYPE_LABELS['repost']), error_html= 'Could not find a %s to <a href="http://indiewebcamp.com/repost">%s</a>. Check that your post has the right <a href="http://indiewebcamp.com/repost">repost-of</a> link.' % (post_label, self.TYPE_LABELS['repost'])) if preview: preview_description += '<span class="verb">%s</span> <a href="%s">this %s</a>: %s' % ( self.TYPE_LABELS['repost'], base_url, self.TYPE_LABELS['post'], self.embed_post(base_obj)) return source.creation_result(description=preview_description) else: resp = self._post(API_REBLOG % base_id) resp['type'] = 'repost' elif type in ('note', 'article') or is_reply or is_rsvp: # a post data = {'status': content} if is_reply: preview_description += 'add a <span class="verb">%s</span> to <a href="%s">this %s</a>: %s' % ( self.TYPE_LABELS['comment'], base_url, self.TYPE_LABELS['post'], self.embed_post(base_obj)) data['in_reply_to_id'] = base_id else: preview_description += '<span class="verb">%s</span>:' % self.TYPE_LABELS[ 'post'] num_media = len(videos) + len(images) if num_media > MAX_MEDIA: videos = videos[:MAX_MEDIA] images = images[:max(MAX_MEDIA - len(videos), 0)] logging.warning('Found %d media! Only using the first %d: %r', num_media, MAX_MEDIA, videos + images) if preview: media_previews = [ '<video controls src="%s"><a href="%s">%s</a></video>' % (util.get_url(vid, key='stream'), util.get_url(vid, key='stream'), vid.get('displayName') or 'this video') for vid in videos ] + [ '<img src="%s" alt="%s" />' % (util.get_url(img), img.get('displayName') or '') for img in images ] if media_previews: preview_content += '<br /><br />' + ' '.join( media_previews) return source.creation_result(content=preview_content, description=preview_description) else: ids = self.upload_media(videos + images) if ids: data['media_ids'] = ids resp = self._post(API_STATUSES, json=data) else: return source.creation_result( abort=False, error_plain='Cannot publish type=%s, verb=%s to Mastodon' % (type, verb), error_html='Cannot publish type=%s, verb=%s to Mastodon' % (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, 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, 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=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: If preview is True, a string HTML snippet. If False, 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 = obj.get('content', '').strip() is_reply = (type == 'comment' or 'inReplyTo' in obj) and base_url if is_reply: # 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 content = unicode(content).encode('utf-8') # linkify defaults to Twitter's link shortening behavior preview_content = util.linkify(content, pretty=True) if is_reply: if preview: return ('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: 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 preview: return ('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 preview: return ('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', 'comment'): if preview: return ('will <span class="verb">tweet</span>:<br /><br />' '<em>%s</em><br />' % preview_content) else: data = urllib.urlencode({'status': content}) resp = json.loads(self.urlopen(API_POST_TWEET_URL, data=data).read()) resp['type'] = 'post' else: raise NotImplementedError() 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 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=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=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, 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: If preview is True, a string HTML snippet. If False, 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) if base_id and not base_url: base_url = 'http://facebook.com/' + base_id content = obj.get('content', '').strip() preview_content = util.linkify(content) url = obj.get('url') msg_data = { 'message': content.encode('utf-8'), # TODO...or leave it to user's default? # 'privacy': json.dumps({'value': 'SELF'}), } if include_link and url: msg_data['actions'] = json.dumps([{'name': 'See Original', 'link': url}]) msg_data = urllib.urlencode(msg_data) if type == 'comment' and base_url: if preview: return ('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 preview: return ('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: # TODO: event invites if preview: assert verb.startswith('rsvp-') return ('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', 'comment'): if preview: return ('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'}) else: raise NotImplementedError() if 'url' not in resp: resp['url'] = base_url return 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_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 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)