Beispiel #1
0
  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))
Beispiel #2
0
  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)
Beispiel #3
0
  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)
Beispiel #4
0
  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)
Beispiel #5
0
  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)
Beispiel #6
0
  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
Beispiel #7
0
  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 />' + ' &nbsp; '.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)
Beispiel #9
0
  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)
Beispiel #10
0
  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
Beispiel #11
0
  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 />' + ' &nbsp; '.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)
Beispiel #12
0
    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)
Beispiel #14
0
  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.')
Beispiel #15
0
  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)
Beispiel #16
0
    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)
Beispiel #17
0
  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.')
Beispiel #18
0
  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)