Example #1
0
  def comment_to_object(self, comment, photo_id):
    """Convert a Flickr comment json object to an ActivityStreams comment.

    Args:
      comment: dict, the comment object from Flickr
      photo_id: string, the Flickr ID of the photo that this comment belongs to

    Returns:
      dict, an ActivityStreams object
    """
    obj = {
      'objectType': 'comment',
      'url': comment.get('permalink'),
      'id': self.tag_uri(comment.get('id')),
      'inReplyTo': [{'id': self.tag_uri(photo_id)}],
      'content': comment.get('_content', ''),
      'published': util.maybe_timestamp_to_rfc3339(comment.get('datecreate')),
      'updated': util.maybe_timestamp_to_rfc3339(comment.get('datecreate')),
      'author': {
        'objectType': 'person',
        'displayName': comment.get('realname') or comment.get('authorname'),
        'username': comment.get('authorname'),
        'id': self.tag_uri(comment.get('author')),
        'url': self.user_url(comment.get('path_alias') or comment.get('author')),
        'image': {
          'url': self.get_user_image(comment.get('iconfarm'),
                                     comment.get('iconserver'),
                                     comment.get('author')),
        },
      }
    }
    self.postprocess_object(obj)
    return obj
Example #2
0
  def comment_to_object(self, comment, photo_id):
    """Convert a Flickr comment json object to an ActivityStreams comment.

    Args:
      comment: dict, the comment object from Flickr
      photo_id: string, the Flickr ID of the photo that this comment belongs to

    Returns:
      dict, an ActivityStreams object
    """
    obj = {
      'objectType': 'comment',
      'url': comment.get('permalink'),
      'id': self.tag_uri(comment.get('id')),
      'inReplyTo': [{'id': self.tag_uri(photo_id)}],
      'content': comment.get('_content', ''),
      'published': util.maybe_timestamp_to_rfc3339(comment.get('datecreate')),
      'updated': util.maybe_timestamp_to_rfc3339(comment.get('datecreate')),
      'author': {
        'objectType': 'person',
        'displayName': comment.get('realname') or comment.get('authorname'),
        'username': comment.get('authorname'),
        'id': self.tag_uri(comment.get('author')),
        'url': self.user_url(comment.get('path_alias') or comment.get('author')),
        'image': {
          'url': self.get_user_image(comment.get('iconfarm'),
                                     comment.get('iconserver'),
                                     comment.get('author')),
        },
      }
    }
    self.postprocess_object(obj)
    return obj
Example #3
0
  def like_to_object(self, person, photo_activity):
    """Convert a Flickr favorite into an ActivityStreams like tag.

    Args:
      person: dict, the person object from Flickr
      photo_activity: dict, the ActivityStreams object representing
        the photo this like belongs to

    Returns:
      dict, an ActivityStreams object
    """
    return {
      'author': {
        'objectType': 'person',
        'displayName': person.get('realname') or person.get('username'),
        'username': person.get('username'),
        'id': self.tag_uri(person.get('nsid')),
        'image': {
          'url': self.get_user_image(person.get('iconfarm'),
                                     person.get('iconserver'),
                                     person.get('nsid')),
        },
      },
      'created': util.maybe_timestamp_to_rfc3339(photo_activity.get('favedate')),
      'url': u'{}#liked-by-{}'.format(
        photo_activity.get('url'), person.get('nsid')),
      'object': {'url': photo_activity.get('url')},
      'id': self.tag_uri(u'{}_liked_by_{}'.format(
        photo_activity.get('flickr_id'), person.get('nsid'))),
      'objectType': 'activity',
      'verb': 'like',
    }
Example #4
0
    def comment_to_object(self, comment, media_id, media_url):
        """Converts a comment to an object.

    Args:
      comment: JSON object retrieved from the Instagram API
      media_id: string
      media_url: string

    Returns:
      an ActivityStreams object dict, ready to be JSON-encoded
    """
        return self.postprocess_object({
            'objectType':
            'comment',
            'id':
            self.tag_uri(comment.get('id')),
            'inReplyTo': [{
                'id': self.tag_uri(media_id)
            }],
            'url':
            '%s#comment-%s' %
            (media_url, comment.get('id')) if media_url else None,
            # TODO: add PST time zone
            'published':
            util.maybe_timestamp_to_rfc3339(comment.get('created_time')),
            'content':
            comment.get('text'),
            'author':
            self.user_to_actor(comment.get('from')),
            'to': [{
                'objectType': 'group',
                'alias': '@public'
            }],
        })
Example #5
0
  def like_to_object(self, person, photo_activity):
    """Convert a Flickr favorite into an ActivityStreams like tag.

    Args:
      person: dict, the person object from Flickr
      photo_activity: dict, the ActivityStreams object representing
        the photo this like belongs to

    Returns:
      dict, an ActivityStreams object
    """
    return {
      'author': {
        'objectType': 'person',
        'displayName': person.get('realname') or person.get('username'),
        'username': person.get('username'),
        'id': self.tag_uri(person.get('nsid')),
        'image': {
          'url': self.get_user_image(person.get('iconfarm'),
                                     person.get('iconserver'),
                                     person.get('nsid')),
        },
      },
      'created': util.maybe_timestamp_to_rfc3339(photo_activity.get('favedate')),
      'url': u'{}#liked-by-{}'.format(
        photo_activity.get('url'), person.get('nsid')),
      'object': {'url': photo_activity.get('url')},
      'id': self.tag_uri(u'{}_liked_by_{}'.format(
        photo_activity.get('flickr_id'), person.get('nsid'))),
      'objectType': 'activity',
      'verb': 'like',
    }
Example #6
0
  def comment_to_object(self, comment, media_id, media_url):
    """Converts a comment to an object.

    Args:
      comment: JSON object retrieved from the Instagram API
      media_id: string
      media_url: string

    Returns:
      an ActivityStreams object dict, ready to be JSON-encoded
    """
    return self.postprocess_object({
      'objectType': 'comment',
      'id': self.tag_uri(comment.get('id')),
      'inReplyTo': [{'id': self.tag_uri(media_id)}],
      'url': '%s#comment-%s' % (media_url, comment.get('id')) if media_url else None,
      # TODO: add PST time zone
      'published': util.maybe_timestamp_to_rfc3339(comment.get('created_time')),
      'content': comment.get('text'),
      'author': self.user_to_actor(comment.get('from')),
      'to': [{'objectType': 'group', 'alias': '@public'}],
    })
Example #7
0
    def media_to_object(self, media):
        """Converts a media to an object.

    Args:
      media: JSON object retrieved from the Instagram API

    Returns:
      an ActivityStreams object dict, ready to be JSON-encoded
    """
        id = media.get('id')

        user = media.get('user', {})
        content = xml.sax.saxutils.escape(
            media.get('caption', {}).get('text', ''))
        object = {
            'id':
            self.tag_uri(id),
            # TODO: detect videos. (the type field is in the JSON respose but not
            # propagated into the Media object.)
            'objectType':
            OBJECT_TYPES.get(media.get('type', 'image'), 'photo'),
            'published':
            util.maybe_timestamp_to_rfc3339(media.get('created_time')),
            'author':
            self.user_to_actor(user),
            'content':
            content,
            'url':
            media.get('link'),
            'to': [{
                'objectType':
                'group',
                'alias':
                '@private' if user.get('is_private') else '@public',
            }],
            'attachments': [{
                'objectType':
                'video' if 'videos' in media else 'image',
                'url':
                media.get('link'),
                # ActivityStreams 2.0 allows image to be a JSON array.
                # http://jasnell.github.io/w3c-socialwg-activitystreams/activitystreams2.html#link
                'image':
                sorted(
                    media.get('images', {}).values(),
                    # sort by size, descending, since atom.py
                    # uses the first image in the list.
                    key=operator.itemgetter('width'),
                    reverse=True),
                # video object defined in
                # http://activitystrea.ms/head/activity-schema.html#rfc.section.4.18
                'stream':
                sorted(media.get('videos', {}).values(),
                       key=operator.itemgetter('width'),
                       reverse=True),
            }],
            # comments go in the replies field, according to the "Responses for
            # Activity Streams" extension spec:
            # http://activitystrea.ms/specs/json/replies/1.0/
            'replies': {
                'items': [
                    self.comment_to_object(c, id, media.get('link'))
                    for c in media.get('comments', {}).get('data', [])
                ],
                'totalItems':
                media.get('comments', {}).get('count'),
            },
            'tags': [
                {
                    'objectType': 'hashtag',
                    'id': self.tag_uri(tag),
                    'displayName': tag,
                    # TODO: url
                } for tag in media.get('tags', [])
            ] + [
                self.user_to_actor(u.get('user'))
                for u in media.get('users_in_photo', [])
            ] + [
                self.like_to_object(u, id, media.get('link'))
                for u in media.get('likes', {}).get('data', [])
            ] + self._mention_tags_from_content(content)
        }

        # alt text
        # https://instagram-press.com/blog/2018/11/28/creating-a-more-accessible-instagram/
        alt = media.get('accessibility_caption')
        if alt and not any(
                alt.startswith(prefix) for prefix in AUTO_ALT_TEXT_PREFIXES):
            for att in object['attachments']:
                for img in att.get('image', []):
                    img['displayName'] = alt

        for version in ('standard_resolution', 'low_resolution', 'thumbnail'):
            image = media.get('images', {}).get(version)
            if image:
                object['image'] = {'url': image.get('url')}
                break

        for version in ('standard_resolution', 'low_resolution',
                        'low_bandwidth'):
            video = media.get('videos', {}).get(version)
            if video:
                object['stream'] = {'url': video.get('url')}
                break

        # http://instagram.com/developer/endpoints/locations/
        if 'location' in media:
            media_loc = media.get('location', {})
            object['location'] = {
                'id':
                self.tag_uri(media_loc.get('id')),
                'displayName':
                media_loc.get('name'),
                'latitude':
                media_loc.get('point', {}).get('latitude'),
                'longitude':
                media_loc.get('point', {}).get('longitude'),
                'address': {
                    'formatted': media_loc.get('street_address')
                },
                'url': (media_loc.get('id')
                        and 'https://instagram.com/explore/locations/%s/' %
                        media_loc.get('id')),
            }

        return self.postprocess_object(object)
Example #8
0
  def photo_to_activity(self, photo):
    """Convert a Flickr photo to an ActivityStreams object. Takes either
    data in the expanded form returned by flickr.photos.getInfo or the
    abbreviated form returned by flickr.people.getPhotos.

    Args:
      photo: dict response from Flickr

    Returns:
      dict, an ActivityStreams object
    """
    owner = photo.get('owner')
    if isinstance(owner, dict):
      owner_id = owner.get('nsid')
      path_alias = owner.get('path_alias')
    else:
      owner_id = owner
      path_alias = photo.get('pathalias')

    created = photo.get('dates', {}).get('taken') or photo.get('datetaken')
    published = util.maybe_timestamp_to_rfc3339(
      photo.get('dates', {}).get('posted') or photo.get('dateupload'))

    # TODO replace owner_id with path_alias?
    photo_permalink = self.photo_url(path_alias or owner_id, photo.get('id'))

    title = photo.get('title')
    if isinstance(title, dict):
      title = title.get('_content', '')

    public = (photo.get('visibility') or photo).get('ispublic')

    activity = {
      'id': self.tag_uri(photo.get('id')),
      'flickr_id': photo.get('id'),
      'url': photo_permalink,
      'actor': {
        'numeric_id': owner_id,
      },
      'object': {
        'displayName': title,
        'url': photo_permalink,
        'id': self.tag_uri(photo.get('id')),
        'image': {
          'url': u'https://farm{}.staticflickr.com/{}/{}_{}_{}.jpg'.format(
            photo.get('farm'), photo.get('server'),
            photo.get('id'), photo.get('secret'), 'b'),
        },
        'content': '\n'.join((
          title, photo.get('description', {}).get('_content', ''))),
        'objectType': 'photo',
        'created': created,
        'published': published,
        'to': [{'objectType': 'group',
                'alias': '@public' if public else '@private'}],
      },
      'verb': 'post',
      'created': created,
      'published': published,
    }

    if isinstance(owner, dict):
      activity['object']['author'] = {
        'objectType': 'person',
        'displayName': owner.get('realname') or owner.get('username'),
        'username': owner.get('username'),
        'id': self.tag_uri(owner.get('username')),
        'image': {
          'url': self.get_user_image(owner.get('iconfarm'),
                                     owner.get('iconserver'),
                                     owner.get('nsid')),
        },
      }

    if isinstance(photo.get('tags'), dict):
      activity['object']['tags'] = [{
          'objectType': 'hashtag',
          'id': self.tag_uri(tag.get('id')),
          'url': u'https://www.flickr.com/search?tags={}'.format(
            tag.get('_content')),
          'displayName': tag.get('raw'),
        } for tag in photo.get('tags', {}).get('tag', [])]
    elif isinstance(photo.get('tags'), basestring):
      activity['object']['tags'] = [{
        'objectType': 'hashtag',
        'url': u'https://www.flickr.com/search?tags={}'.format(
          tag.strip()),
        'displayName': tag.strip(),
      } for tag in photo.get('tags').split(' ') if tag.strip()]

    self.postprocess_activity(activity)
    return activity
Example #9
0
    def photo_to_activity(self, photo):
        """Convert a Flickr photo to an ActivityStreams object. Takes either
    data in the expanded form returned by flickr.photos.getInfo or the
    abbreviated form returned by flickr.people.getPhotos.

    Args:
      photo: dict response from Flickr

    Returns:
      dict, an ActivityStreams object
    """
        owner = photo.get('owner')
        if isinstance(owner, dict):
            owner_id = owner.get('nsid')
            path_alias = owner.get('path_alias')
        else:
            owner_id = owner
            path_alias = photo.get('pathalias')

        created = photo.get('dates', {}).get('taken') or photo.get('datetaken')
        published = util.maybe_timestamp_to_rfc3339(
            photo.get('dates', {}).get('posted') or photo.get('dateupload'))

        # TODO replace owner_id with path_alias?
        photo_permalink = self.photo_url(path_alias or owner_id,
                                         photo.get('id'))

        title = photo.get('title')
        if isinstance(title, dict):
            title = title.get('_content', '')

        public = (photo.get('visibility') or photo).get('ispublic')

        activity = {
            'id': self.tag_uri(photo.get('id')),
            'flickr_id': photo.get('id'),
            'url': photo_permalink,
            'actor': {
                'numeric_id': owner_id,
            },
            'object': {
                'displayName':
                title,
                'url':
                photo_permalink,
                'id':
                self.tag_uri(photo.get('id')),
                'image': {
                    'url':
                    u'https://farm{}.staticflickr.com/{}/{}_{}_{}.jpg'.format(
                        photo.get('farm'), photo.get('server'),
                        photo.get('id'), photo.get('secret'), 'b'),
                },
                'content':
                '\n'.join((title, photo.get('description',
                                            {}).get('_content', ''))),
                'objectType':
                'photo',
                'created':
                created,
                'published':
                published,
                'to': [{
                    'objectType': 'group',
                    'alias': '@public' if public else '@private'
                }],
            },
            'verb': 'post',
            'created': created,
            'published': published,
        }

        if isinstance(owner, dict):
            username = owner.get('username')
            # Flickr API is evidently inconsistent in what it puts in realname vs
            # username vs path_alias. if username has spaces, it's probably actually
            # real name, so use path alias instead.
            # https://github.com/snarfed/bridgy/issues/687
            # https://www.flickr.com/groups/51035612836@N01/discuss/72157625945600254
            if not username or ' ' in username:
                username = owner.get('path_alias')

            activity['object']['author'] = {
                'objectType': 'person',
                'displayName': owner.get('realname') or username,
                'username': username,
                'id': self.tag_uri(username),
                'image': {
                    'url':
                    self.get_user_image(owner.get('iconfarm'),
                                        owner.get('iconserver'),
                                        owner.get('nsid')),
                },
            }

        if isinstance(photo.get('tags'), dict):
            activity['object']['tags'] = [{
                'objectType':
                'hashtag',
                'id':
                self.tag_uri(tag.get('id')),
                'url':
                u'https://www.flickr.com/search?tags={}'.format(
                    tag.get('_content')),
                'displayName':
                tag.get('raw'),
            } for tag in photo.get('tags', {}).get('tag', [])]
        elif isinstance(photo.get('tags'), basestring):
            activity['object']['tags'] = [{
                'objectType':
                'hashtag',
                'url':
                u'https://www.flickr.com/search?tags={}'.format(tag.strip()),
                'displayName':
                tag.strip(),
            } for tag in photo.get('tags').split(' ') if tag.strip()]

        # location is represented differently in a list of photos vs a
        # single photo info
        lat = photo.get('latitude') or photo.get('location',
                                                 {}).get('latitude')
        lng = photo.get('longitude') or photo.get('location',
                                                  {}).get('longitude')
        if lat and lng and float(lat) != 0 and float(lng) != 0:
            activity['object']['location'] = {
                'objectType': 'place',
                'latitude': float(lat),
                'longitude': float(lng),
            }

        self.postprocess_object(activity['object'])
        self.postprocess_activity(activity)
        return activity
Example #10
0
  def media_to_object(self, media):
    """Converts a media to an object.

    Args:
      media: JSON object retrieved from the Instagram API

    Returns:
      an ActivityStreams object dict, ready to be JSON-encoded
    """
    id = media.get('id')

    object = {
      'id': self.tag_uri(id),
      # TODO: detect videos. (the type field is in the JSON respose but not
      # propagated into the Media object.)
      'objectType': OBJECT_TYPES.get(media.get('type', 'image'), 'photo'),
      'published': util.maybe_timestamp_to_rfc3339(media.get('created_time')),
      'author': self.user_to_actor(media.get('user')),
      'content': xml.sax.saxutils.escape(
        media.get('caption', {}).get('text', '')),
      'url': media.get('link'),
      'to': [{'objectType': 'group', 'alias': '@public'}],
      'attachments': [{
        'objectType': 'video' if 'videos' in media else 'image',
        # ActivityStreams 2.0 allows image to be a JSON array.
        # http://jasnell.github.io/w3c-socialwg-activitystreams/activitystreams2.html#link
        'image': sorted(
          media.get('images', {}).values(),
          # sort by size, descending, since atom.py
          # uses the first image in the list.
          key=operator.itemgetter('width'), reverse=True),
        # video object defined in
        # http://activitystrea.ms/head/activity-schema.html#rfc.section.4.18
        'stream': sorted(
          media.get('videos', {}).values(),
          key=operator.itemgetter('width'), reverse=True),
      }],
      # comments go in the replies field, according to the "Responses for
      # Activity Streams" extension spec:
      # http://activitystrea.ms/specs/json/replies/1.0/
      'replies': {
        'items': [self.comment_to_object(c, id, media.get('link'))
                  for c in media.get('comments', {}).get('data', [])],
        'totalItems': media.get('comments', {}).get('count'),
      },
      'tags': [{
        'objectType': 'hashtag',
        'id': self.tag_uri(tag),
        'displayName': tag,
        # TODO: url
      } for tag in media.get('tags', [])] +
      [self.user_to_actor(user.get('user'))
       for user in media.get('users_in_photo', [])] +
      [self.like_to_object(user, id, media.get('link'))
       for user in media.get('likes', {}).get('data', [])],
    }

    for version in ('standard_resolution', 'low_resolution', 'thumbnail'):
      image = media.get('images').get(version)
      if image:
        object['image'] = {'url': image.get('url')}
        break

    for version in ('standard_resolution', 'low_resolution', 'low_bandwidth'):
      video = media.get('videos', {}).get(version)
      if video:
        object['stream'] = {'url': video.get('url')}
        break

    # http://instagram.com/developer/endpoints/locations/
    if 'location' in media:
      media_loc = media.get('location', {})
      object['location'] = {
        'id': media_loc.get('id'),
        'displayName': media_loc.get('name'),
        'latitude': media_loc.get('point', {}).get('latitude'),
        'longitude': media_loc.get('point', {}).get('longitude'),
        'address': {'formatted': media_loc.get('street_address')},
        'url': (media_loc.get('id')
                and 'https://instagram.com/explore/locations/%s/'
                % media_loc.get('id')),
      }

    return self.postprocess_object(object)
Example #11
0
    def photo_to_activity(self, photo):
        """Convert a Flickr photo to an ActivityStreams object. Takes either
    data in the expanded form returned by flickr.photos.getInfo or the
    abbreviated form returned by flickr.people.getPhotos.

    Args:
      photo: dict response from Flickr

    Returns:
      dict, an ActivityStreams object
    """
        owner = photo.get('owner')
        owner_id = owner.get('nsid') if isinstance(owner, dict) else owner

        created = photo.get('dates', {}).get('taken') or photo.get('datetaken')
        published = util.maybe_timestamp_to_rfc3339(
            photo.get('dates', {}).get('posted') or photo.get('dateupload'))

        photo_permalink = 'https://www.flickr.com/photos/{}/{}/'.format(
            owner_id, photo.get('id'))

        title = photo.get('title')
        if isinstance(title, dict):
            title = title.get('_content', '')

        public = (photo.get('visibility') or photo).get('ispublic')

        activity = {
            'id': self.tag_uri(photo.get('id')),
            'flickr_id': photo.get('id'),
            'url': photo_permalink,
            'actor': {
                'numeric_id': owner_id,
            },
            'object': {
                'displayName':
                title,
                'url':
                photo_permalink,
                'id':
                self.tag_uri(photo.get('id')),
                'image': {
                    'url':
                    'https://farm{}.staticflickr.com/{}/{}_{}_{}.jpg'.format(
                        photo.get('farm'), photo.get('server'),
                        photo.get('id'), photo.get('secret'), 'b'),
                },
                'content':
                '\n'.join((title, photo.get('description',
                                            {}).get('_content', ''))),
                'objectType':
                'photo',
                'created':
                created,
                'published':
                published,
                'to': [{
                    'objectType': 'group',
                    'alias': '@public' if public else '@private'
                }],
            },
            'verb': 'post',
            'created': created,
            'published': published,
        }

        if isinstance(owner, dict):
            activity['object']['author'] = {
                'objectType': 'person',
                'displayName': owner.get('realname') or owner.get('username'),
                'username': owner.get('username'),
                'id': self.tag_uri(owner.get('username')),
                'image': {
                    'url':
                    self.get_user_image(owner.get('iconfarm'),
                                        owner.get('iconserver'),
                                        owner.get('nsid')),
                },
            }

        if isinstance(photo.get('tags'), dict):
            activity['object']['tags'] = [{
                'objectType':
                'hashtag',
                'id':
                self.tag_uri(tag.get('id')),
                'url':
                'https://www.flickr.com/search?tags={}'.format(
                    tag.get('_content')),
                'displayName':
                tag.get('raw'),
            } for tag in photo.get('tags', {}).get('tag', [])]
        elif isinstance(photo.get('tags'), basestring):
            activity['object']['tags'] = [{
                'objectType':
                'hashtag',
                'url':
                'https://www.flickr.com/search?tags={}'.format(tag.strip()),
                'displayName':
                tag.strip(),
            } for tag in photo.get('tags').split(' ') if tag.strip()]

        self.postprocess_activity(activity)
        return activity
Example #12
0
  def photo_to_activity(self, photo):
    """Convert a Flickr photo to an ActivityStreams object. Takes either
    data in the expanded form returned by flickr.photos.getInfo or the
    abbreviated form returned by flickr.people.getPhotos.

    Args:
      photo: dict response from Flickr

    Returns:
      dict, an ActivityStreams object
    """
    owner = photo.get('owner')
    if isinstance(owner, dict):
      owner_id = owner.get('nsid')
      path_alias = owner.get('path_alias')
    else:
      owner_id = owner
      path_alias = photo.get('pathalias')

    created = photo.get('dates', {}).get('taken') or photo.get('datetaken')
    published = util.maybe_timestamp_to_rfc3339(
      photo.get('dates', {}).get('posted') or photo.get('dateupload'))

    # TODO replace owner_id with path_alias?
    photo_permalink = self.photo_url(path_alias or owner_id, photo.get('id'))

    title = photo.get('title')
    if isinstance(title, dict):
      title = title.get('_content', '')

    public = (photo.get('visibility') or photo).get('ispublic')

    activity = {
      'id': self.tag_uri(photo.get('id')),
      'flickr_id': photo.get('id'),
      'url': photo_permalink,
      'actor': {
        'numeric_id': owner_id,
      },
      'object': {
        'displayName': title,
        'url': photo_permalink,
        'id': self.tag_uri(photo.get('id')),
        'image': {
          'url': 'https://farm{}.staticflickr.com/{}/{}_{}_{}.jpg'.format(
            photo.get('farm'), photo.get('server'),
            photo.get('id'), photo.get('secret'), 'b'),
        },
        'content': '\n'.join((
          title, photo.get('description', {}).get('_content', ''))),
        'objectType': 'photo',
        'created': created,
        'published': published,
        'to': [{'objectType': 'group',
                'alias': '@public' if public else '@private'}],
      },
      'verb': 'post',
      'created': created,
      'published': published,
    }

    if isinstance(owner, dict):
      username = owner.get('username')
      # Flickr API is evidently inconsistent in what it puts in realname vs
      # username vs path_alias. if username has spaces, it's probably actually
      # real name, so use path alias instead.
      # https://github.com/snarfed/bridgy/issues/687
      # https://www.flickr.com/groups/51035612836@N01/discuss/72157625945600254
      if not username or ' ' in username:
        username = owner.get('path_alias')

      activity['object']['author'] = {
        'objectType': 'person',
        'displayName': owner.get('realname') or username,
        'username': username,
        'id': self.tag_uri(username),
        'image': {
          'url': self.get_user_image(owner.get('iconfarm'),
                                     owner.get('iconserver'),
                                     owner.get('nsid')),
        },
      }

    if isinstance(photo.get('tags'), dict):
      activity['object']['tags'] = [{
          'objectType': 'hashtag',
          'id': self.tag_uri(tag.get('id')),
          'url': 'https://www.flickr.com/search?tags={}'.format(
            tag.get('_content')),
          'displayName': tag.get('raw'),
        } for tag in photo.get('tags', {}).get('tag', [])]
    elif isinstance(photo.get('tags'), basestring):
      activity['object']['tags'] = [{
        'objectType': 'hashtag',
        'url': 'https://www.flickr.com/search?tags={}'.format(
          tag.strip()),
        'displayName': tag.strip(),
      } for tag in photo.get('tags').split(' ') if tag.strip()]

    # location is represented differently in a list of photos vs a
    # single photo info
    lat = photo.get('latitude') or photo.get('location', {}).get('latitude')
    lng = photo.get('longitude') or photo.get('location', {}).get('longitude')
    if lat and lng and float(lat) != 0 and float(lng) != 0:
      activity['object']['location'] = {
        'objectType': 'place',
        'latitude': float(lat),
        'longitude': float(lng),
      }

    self.postprocess_object(activity['object'])
    self.postprocess_activity(activity)
    return activity