Ejemplo n.º 1
0
    def _Account(self):
        """Makes accounting changes:
       1. For unshared photos.
    """
        acc_accum = AccountingAccumulator()

        # Make accounting changes for all unshared photos.
        tasks = []
        for viewpoint_id, ep_dicts in self._unshares_dict.iteritems():
            # Filter out any photos which were already removed.
            if len(self._already_removed_ids) > 0:
                ep_dicts = deepcopy(ep_dicts)
                for episode_id, photo_ids in ep_dicts.iteritems():
                    ep_dicts[episode_id] = [
                        photo_id for photo_id in photo_ids
                        if Post.ConstructPostId(episode_id, photo_id) not in
                        self._already_removed_ids
                    ]

            viewpoint, followers = self._vp_followers_dict[viewpoint_id]
            tasks.append(
                acc_accum.Unshare(self._client, viewpoint, ep_dicts,
                                  [follower for follower in followers]))
        yield tasks

        yield acc_accum.Apply(self._client)
Ejemplo n.º 2
0
 def _Account(self):
   """Makes accounting changes:
      1. For revived followers.
   """
   acc_accum = AccountingAccumulator()
   yield acc_accum.ReviveFollowers(self._client, self._viewpoint_id, self._revive_follower_ids)
   yield acc_accum.Apply(self._client)
Ejemplo n.º 3
0
    def _Account(self):
        """Makes accounting changes:
       1. For revived followers.
       2. For new photos that were shared.
    """
        # Get list of the ids of all photos that were added.
        photo_ids = [
            Post.DeconstructPostId(id)[1] for id in self._new_ids
            if id.startswith(IdPrefix.Post)
        ]

        acc_accum = AccountingAccumulator()

        # Make accounting changes for any revived followers.
        yield acc_accum.ReviveFollowers(self._client, self._viewpoint_id,
                                        self._revive_follower_ids)

        # Make accounting changes for the new photos that were added.
        yield acc_accum.SharePhotos(
            self._client, self._user_id, self._viewpoint_id, photo_ids, [
                follower.user_id
                for follower in self._followers if not follower.IsRemoved()
            ])

        yield acc_accum.Apply(self._client)
Ejemplo n.º 4
0
 def _Account(self):
     """Makes accounting changes:
    1. Decrease user accounting by size of viewpoint.
 """
     acc_accum = AccountingAccumulator()
     yield acc_accum.RemoveViewpoint(self._client, self._user_id,
                                     self._viewpoint_id)
     yield acc_accum.Apply(self._client)
Ejemplo n.º 5
0
    def _Account(self):
        """Makes accounting changes:
       1. For removed followers.
    """
        acc_accum = AccountingAccumulator()

        # Make accounting changes for the removed followers.
        for follower_id in self._remove_id_set:
            yield acc_accum.RemoveViewpoint(self._client, follower_id,
                                            self._viewpoint_id)

        yield acc_accum.Apply(self._client)
Ejemplo n.º 6
0
  def _Account(self):
    """Makes accounting changes:
       1. For new photos that were uploaded.
    """
    # Get list of photos that were added by this operation.
    new_ph_dicts = [ph_dict for ph_dict in self._ph_dicts if ph_dict['photo_id'] in self._new_ids]

    acc_accum = AccountingAccumulator()

    # Make accounting changes for the new photos that were added.
    yield acc_accum.UploadEpisode(self._client, self._user.user_id, self._user.private_vp_id, new_ph_dicts)

    yield acc_accum.Apply(self._client)
Ejemplo n.º 7
0
    def _Account(self):
        """Makes accounting changes:
       1. Decrease user accounting by size of removed photos.
    """
        # Get list of the ids of all photos that were removed.
        photo_ids = [
            Post.DeconstructPostId(post_id)[1] for post_id in self._remove_ids
        ]

        acc_accum = AccountingAccumulator()
        yield acc_accum.RemovePhotos(self._client, self._user.user_id,
                                     self._user.private_vp_id, photo_ids)
        yield acc_accum.Apply(self._client)
Ejemplo n.º 8
0
  def _Account(self):
    """Makes accounting changes:
       1. For new photos that were saved.
    """
    # Get list of the ids of all photos that were added.
    photo_ids = [Post.DeconstructPostId(id)[1] for id in self._new_ids if id.startswith(IdPrefix.Post)]

    acc_accum = AccountingAccumulator()

    # Make accounting changes for the new photos that were added.
    yield acc_accum.SavePhotos(self._client,
                               self._user.user_id,
                               self._user.private_vp_id,
                               photo_ids)

    yield acc_accum.Apply(self._client)
Ejemplo n.º 9
0
    def _Account(self):
        """Makes accounting changes:
       1. For revived followers.
       2. For new followers.
    """
        acc_accum = AccountingAccumulator()

        # Make accounting changes for any revived followers.
        yield acc_accum.ReviveFollowers(self._client, self._viewpoint_id,
                                        self._revive_follower_ids)

        # Make accounting changes for the new followers.
        yield acc_accum.AddFollowers(self._client, self._viewpoint_id,
                                     self._new_follower_ids)

        yield acc_accum.Apply(self._client)
Ejemplo n.º 10
0
    def _Account(self):
        """Makes accounting changes:
       1. For revived followers.
       2. For new followers.
    """
        # Get list of the ids of all photos that were added.
        photo_ids = [
            photo_id for new_ep_dict in self._new_ep_dicts
            for photo_id in new_ep_dict['photo_ids']
        ]

        # Make accounting changes for the new viewpoint.
        acc_accum = AccountingAccumulator()
        yield acc_accum.SharePhotos(
            self._client, self._user_id, self._viewpoint_id, photo_ids,
            [follower.user_id for follower in self._followers])
        yield acc_accum.Apply(self._client)
Ejemplo n.º 11
0
 def __init__(self, client, act_dict, target_user_id, source_user_id):
   super(MergeAccountsOperation, self).__init__(client)
   self._act_dict = act_dict
   self._target_user_id = target_user_id
   self._source_user_id = source_user_id
   self._acc_accum = AccountingAccumulator()
Ejemplo n.º 12
0
class MergeAccountsOperation(ViewfinderOperation):
  """The MergeAccounts operation consists of the following steps:

     STEP 1: MERGE VIEWPOINTS
       Add target user as a follower of all non-removed viewpoints followed by the source user.

     STEP 2: MERGE IDENTITIES
       Re-bind all source user identities to the target user.

     STEP 3: TERMINATE USER
       Terminates the source user.
  """
  _MAX_IDENTITIES = 25
  """Maximum number of identities that can be merged."""

  _FOLLOWER_LIMIT = 50
  """Number of followers that will be queried at a time."""

  def __init__(self, client, act_dict, target_user_id, source_user_id):
    super(MergeAccountsOperation, self).__init__(client)
    self._act_dict = act_dict
    self._target_user_id = target_user_id
    self._source_user_id = source_user_id
    self._acc_accum = AccountingAccumulator()

  @classmethod
  @gen.coroutine
  def Execute(cls, client, activity, target_user_id, source_user_id):
    """Entry point called by the operation framework."""
    yield MergeAccountsOperation(client, activity, target_user_id, source_user_id)._Merge()

  @gen.coroutine
  def _Merge(self):
    """Orchestrates the merge operation."""
    # Acquire op-lock for source user (should already have op-lock for target user).
    op_lock = yield gen.Task(Lock.Acquire,
                             self._client,
                             LockResourceType.Operation,
                             str(self._source_user_id),
                             owner_id=self._op.operation_id)
    try:
      # If checkpoint exists, may skip past viewpoint merge phase.
      state = self._op.checkpoint['state'] if self._op.checkpoint else 'vp'
      if state == 'vp':
        # Make target user a follower of all source user's viewpoints.
        yield self._MergeViewpoints()
        yield Operation.TriggerFailpoint(self._client)
        self._op.checkpoint = None
      else:
        assert state == 'id', state

      # Re-bind identities of the source user to the target user.
      yield self._MergeIdentities()
      yield Operation.TriggerFailpoint(self._client)

      # Terminate the source user.
      yield gen.Task(User.TerminateAccountOperation,
                     self._client,
                     user_id=self._source_user_id,
                     merged_with=self._target_user_id)
      yield Operation.TriggerFailpoint(self._client)
    finally:
      yield gen.Task(op_lock.Release, self._client)

  @gen.coroutine
  def _MergeViewpoints(self):
    """Loops over all viewpoints followed by the source user and merges any that have not been
    removed. Applies user accounting information accumulated in _MergeOneViewpoint.
    """
    # If checkpoint exists, then operation has been restarted, so re-merge the checkpointed viewpoint.
    if self._op.checkpoint is not None:
      start_key = self._op.checkpoint['id']
      yield self._MergeOneViewpoint(start_key)
    else:
      start_key = None

    # Scan remainder of viewpoints.
    while True:
      follower_list = yield gen.Task(Follower.RangeQuery,
                                     self._client,
                                     self._source_user_id,
                                     range_desc=None,
                                     limit=MergeAccountsOperation._FOLLOWER_LIMIT,
                                     col_names=None,
                                     excl_start_key=start_key)

      start_key = follower_list[-1].viewpoint_id if follower_list else None
      logging.info('scanned %d follower records for merging, last key=%s', len(follower_list), start_key)

      for follower in follower_list:
        # Skip removed viewpoints.
        if follower.IsRemoved():
          continue

        self._op.checkpoint = None
        yield self._MergeOneViewpoint(follower.viewpoint_id)

      if len(follower_list) < MergeAccountsOperation._FOLLOWER_LIMIT:
        break

    # Apply the accounting information that was accumulated from the viewpoints. 
    yield self._acc_accum.Apply(self._client)

  @gen.coroutine
  def _MergeOneViewpoint(self, viewpoint_id):
    """Adds the target user as a follower of the given viewpoint owned by the source user.
    Accumulates the size of all viewpoints that are merged. Creates notifications for the
    merged viewpoint. Sets a checkpoint containing follower and accounting information to
    be used if a restart occurs.
    """
    # Skip default and system viewpoints.
    viewpoint = yield gen.Task(Viewpoint.Query, self._client, viewpoint_id, None)
    if viewpoint.IsDefault() or viewpoint.IsSystem():
      return

    # Lock the viewpoint while querying and modifying to the viewpoint.
    vp_lock = yield gen.Task(Viewpoint.AcquireLock, self._client, viewpoint_id)
    try:
      if self._op.checkpoint is None:
        # Get list of existing followers.
        existing_follower_ids, _ = yield gen.Task(Viewpoint.QueryFollowerIds,
                                                  self._client,
                                                  viewpoint_id,
                                                  limit=Viewpoint.MAX_FOLLOWERS)

        # Skip viewpoint if target user is already a follower.
        if self._target_user_id in existing_follower_ids:
          return

        # Skip viewpoint if there are too many followers (this should virtually never happen, since client
        # enforces an even smaller limit).
        if len(existing_follower_ids) >= Viewpoint.MAX_FOLLOWERS:
          logging.warning('merge of user %d into user %d would exceed follower limit on viewpoint "%s"',
                          (self._source_user_id, self._target_user_id, viewpoint_id))
          return

        # Add size of this viewpoint to the accounting accumulator and checkpoint in case operation restarts.
        yield self._acc_accum.MergeAccounts(self._client, viewpoint_id, self._target_user_id)

        checkpoint = {'state': 'vp',
                      'id': viewpoint_id,
                      'existing': existing_follower_ids,
                      'account': self._acc_accum.GetUserVisibleTo(self._target_user_id)._asdict()}
        yield self._op.SetCheckpoint(self._client, checkpoint)
      else:
        # Re-constitute state from checkpoint.
        existing_follower_ids = self._op.checkpoint['existing']
        accounting = Accounting.CreateFromKeywords(**self._op.checkpoint['account'])
        self._acc_accum.GetUserVisibleTo(self._target_user_id).IncrementStatsFrom(accounting)

      # Get the source follower.
      source_follower = yield gen.Task(Follower.Query, self._client, self._source_user_id, viewpoint_id, None)

      # Now actually add the target user as a follower.
      target_follower = (yield viewpoint.AddFollowers(self._client,
                                                      source_follower.adding_user_id,
                                                      existing_follower_ids,
                                                      [self._target_user_id],
                                                      self._op.timestamp))[0]

      # Get list of existing follower db objects.
      follower_keys = [DBKey(follower_id, viewpoint_id) for follower_id in existing_follower_ids]
      existing_followers = yield gen.Task(Follower.BatchQuery, self._client, follower_keys, None)

      # Synthesize a unique activity id by adding viewpoint id to the activity id.
      truncated_ts, device_id, (client_id, server_id) = Activity.DeconstructActivityId(self._act_dict['activity_id'])
      activity_id = Activity.ConstructActivityId(truncated_ts, device_id, (client_id, viewpoint_id))
      activity_dict = {'activity_id': activity_id,
                       'timestamp': self._act_dict['timestamp']}

      # Create merge-related notifications.
      yield NotificationManager.NotifyMergeViewpoint(self._client,
                                                     viewpoint_id,
                                                     existing_followers,
                                                     target_follower,
                                                     self._source_user_id,
                                                     activity_dict,
                                                     self._op.timestamp)
    finally:
      yield gen.Task(Viewpoint.ReleaseLock, self._client, viewpoint_id, vp_lock)

  @gen.coroutine
  def _MergeIdentities(self):
    """Re-binds all identities attached to the source user to the target user. Sends corresponding
    notifications for any merged identities. Sets a checkpoint so that the exact same set of
    identities will be merged if a restart occurs.
    """
    if self._op.checkpoint is None:
      # Get set of identities that need to re-bound to the target user.
      query_expr = ('identity.user_id={id}', {'id': self._source_user_id})
      identity_keys = yield gen.Task(Identity.IndexQueryKeys,
                                     self._client,
                                     query_expr,
                                     limit=MergeAccountsOperation._MAX_IDENTITIES)

      checkpoint = {'state': 'id',
                    'ids': [key.hash_key for key in identity_keys]}
      yield self._op.SetCheckpoint(self._client, checkpoint)
    else:
      identity_keys = [DBKey(id, None) for id in self._op.checkpoint['ids']]

    # Get all the identity objects and re-bind them to the target user.
    identities = yield gen.Task(Identity.BatchQuery, self._client, identity_keys, None)
    for identity in identities:
      identity.expires = 0
      identity.user_id = self._target_user_id
      yield gen.Task(identity.Update, self._client)

    # Send notifications for all identities that were re-bound.
    yield NotificationManager.NotifyMergeIdentities(self._client,
                                                    self._target_user_id,
                                                    [identity.key for identity in identities],
                                                    self._op.timestamp)
Ejemplo n.º 13
0
  def _CreateWelcomeConversation(self):
    """Creates the welcome conversation at the db level. Operations are not used in order
    to avoid creating notifications, sending alerts, taking locks, running nested operations,
    etc.
    """
    from viewfinder.backend.www.system_users import NARRATOR_USER
    from viewfinder.backend.www.system_users import NARRATOR_UPLOAD_PHOTOS, NARRATOR_UPLOAD_PHOTOS_2, NARRATOR_UPLOAD_PHOTOS_3

    # Accumulate accounting changes.
    self._acc_accum = AccountingAccumulator()

    self._unique_id = self._unique_id_start
    self._update_seq = 1

    # Create the viewpoint.
    self._viewpoint_id = Viewpoint.ConstructViewpointId(self._new_user.webapp_dev_id, self._unique_id)
    self._unique_id += 1
    initial_follower_ids = [self._new_user.user_id]
    viewpoint, followers = yield Viewpoint.CreateNewWithFollowers(self._client,
                                                                  follower_ids=initial_follower_ids,
                                                                  user_id=NARRATOR_USER.user_id,
                                                                  viewpoint_id=self._viewpoint_id,
                                                                  type=Viewpoint.SYSTEM,
                                                                  title='Welcome...',
                                                                  timestamp=self._op.timestamp)

    # Narrator creates and introduces the conversation.
    yield self._CreateActivity(NARRATOR_USER,
                               self._op.timestamp - 60,
                               Activity.CreateShareNew,
                               ep_dicts=[],
                               follower_ids=initial_follower_ids)

    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 60,
                            'Welcome to Viewfinder, a new way to privately share photos with your friends.')

    # Narrator shares photos.
    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 59,
                            'Select as many photos as you want to share with exactly who you want.')

    photo_ids = [ph_dict['photo_id'] for ph_dict in NARRATOR_UPLOAD_PHOTOS['photos']]
    episode = yield self._CreateEpisodeWithPosts(NARRATOR_USER,
                                                 NARRATOR_UPLOAD_PHOTOS['episode']['episode_id'],
                                                 NARRATOR_UPLOAD_PHOTOS['photos'])
    yield self._CreateActivity(NARRATOR_USER,
                               self._op.timestamp - 58,
                               Activity.CreateShareExisting,
                               ep_dicts=[{'new_episode_id': episode.episode_id, 'photo_ids': photo_ids}])

    # Set cover photo on viewpoint now that episode id is known.
    viewpoint.cover_photo = {'episode_id': episode.episode_id,
                             'photo_id': NARRATOR_UPLOAD_PHOTOS['photos'][0]['photo_id']}
    yield gen.Task(viewpoint.Update, self._client)

    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 56,
                            'Your friends can also add photos to the conversation, '
                            'creating unique collaborative albums.')

    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 55,
                            'You can add as many photos, messages and friends as you want to the conversation, '
                            'leading to a memorable shared experience.')

    # Narrator shares more photos.
    photo_ids = [ph_dict['photo_id'] for ph_dict in NARRATOR_UPLOAD_PHOTOS_2['photos']]
    episode = yield self._CreateEpisodeWithPosts(NARRATOR_USER,
                                                 NARRATOR_UPLOAD_PHOTOS_2['episode']['episode_id'],
                                                 NARRATOR_UPLOAD_PHOTOS_2['photos'])
    yield self._CreateActivity(NARRATOR_USER,
                               self._op.timestamp - 54,
                               Activity.CreateShareExisting,
                               ep_dicts=[{'new_episode_id': episode.episode_id, 'photo_ids': photo_ids}])

    # Single-photo comment.
    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 53,
                            'Hold and press on photos to comment on specific pics.',
                            asset_id=NARRATOR_UPLOAD_PHOTOS_2['photos'][1]['photo_id'])

    # Narrator rambles on for a while.
    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 52,
                            'Use mobile #\'s or email addresses to add new people if they\'re not yet on Viewfinder.');

    # Narrator shares more photos.
    photo_ids = [ph_dict['photo_id'] for ph_dict in NARRATOR_UPLOAD_PHOTOS_3['photos']]
    episode = yield self._CreateEpisodeWithPosts(NARRATOR_USER,
                                                 NARRATOR_UPLOAD_PHOTOS_3['episode']['episode_id'],
                                                 NARRATOR_UPLOAD_PHOTOS_3['photos'])
    yield self._CreateActivity(NARRATOR_USER,
                               self._op.timestamp - 51,
                               Activity.CreateShareExisting,
                               ep_dicts=[{'new_episode_id': episode.episode_id, 'photo_ids': photo_ids}])


    # Conclusion.
    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 50,
                            'Viewfinder is perfect for vacations, weddings, or any shared experience where you want '
                            'to share photos without posting them for everyone to see.')

    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 49,
                            'Start sharing now.')

    # Validate that we allocated enough ids and counted update_seq properly.
    assert self._unique_id == self._unique_id_start + CreateProspectiveOperation._ASSET_ID_COUNT, self._unique_id
    assert self._update_seq == CreateProspectiveOperation._UPDATE_SEQ_COUNT, self._update_seq

    # Set update_seq on the new viewpoint.
    viewpoint.update_seq = self._update_seq
    yield gen.Task(viewpoint.Update, self._client)

    # Remove this viewpoint for all sample users so that accounting will be correct (also in case
    # we want to sync a device to Nick's account and see if new users are trying to chat). Also
    # update viewed_seq so that entire conversation is "read" for each sample user.
    for follower in followers:
      if follower.user_id != self._new_user.user_id:
        follower.viewed_seq = viewpoint.update_seq
        yield follower.RemoveViewpoint(self._client)

    # Commit accounting changes.
    yield self._acc_accum.Apply(self._client)
Ejemplo n.º 14
0
class CreateProspectiveOperation(ViewfinderOperation):
  """The CreateProspective operation expects the caller to allocate the new user's id and
  web device id. The caller is also responsible for ensuring that the user does not yet
  exist.
  """
  _ASSET_ID_COUNT = 24
  """Number of asset ids that will be allocated for the welcome conversation."""

  _UPDATE_SEQ_COUNT = 13
  """Number of viewpoint updates that will be made for the welcome conversation."""

  def __init__(self, client, new_user_id, webapp_dev_id, identity_key, reason=None):
    super(CreateProspectiveOperation, self).__init__(client)
    self._new_user_id = new_user_id
    self._webapp_dev_id = webapp_dev_id
    self._identity_key = identity_key
    self._reason = reason

  @classmethod
  @gen.coroutine
  def Execute(cls, client, user_id, webapp_dev_id, identity_key, reason=None):
    """Entry point called by the operation framework."""
    yield CreateProspectiveOperation(client, user_id, webapp_dev_id, identity_key, reason=reason)._CreateProspective()

  @gen.coroutine
  def _CreateProspective(self):
    """Create the prospective user and identity."""
    self._new_user, _ = yield User.CreateProspective(self._client,
                                                     self._new_user_id,
                                                     self._webapp_dev_id,
                                                     self._identity_key,
                                                     self._op.timestamp)

    # If system user is defined, then create the welcome conversation.
    # For now, add a check to ensure the welcome conversation is not created in production.
    if system_users.NARRATOR_USER is not None:
      # Checkpoint the allocated asset id range used to create the welcome conversation.
      if self._op.checkpoint is None:
        # NOTE: Asset ids are allocated from the new user's ids. This is different than the
        #       usual practice of allocating from the sharer's ids. 
        self._unique_id_start = yield gen.Task(User.AllocateAssetIds,
                                               self._client,
                                               self._new_user_id,
                                               CreateProspectiveOperation._ASSET_ID_COUNT)

        checkpoint = {'id': self._unique_id_start}
        yield self._op.SetCheckpoint(self._client, checkpoint)
      else:
        self._unique_id_start = self._op.checkpoint['id']

      yield self._CreateWelcomeConversation()

    # Add an analytics entry for this user.
    analytics = Analytics.Create(entity='us:%d' % self._new_user_id,
                                 type=Analytics.USER_CREATE_PROSPECTIVE,
                                 timestamp=self._op.timestamp,
                                 payload=self._reason)
    yield gen.Task(analytics.Update, self._client)

    yield Operation.TriggerFailpoint(self._client)

  @gen.coroutine
  def _CreateWelcomeConversation(self):
    """Creates the welcome conversation at the db level. Operations are not used in order
    to avoid creating notifications, sending alerts, taking locks, running nested operations,
    etc.
    """
    from viewfinder.backend.www.system_users import NARRATOR_USER
    from viewfinder.backend.www.system_users import NARRATOR_UPLOAD_PHOTOS, NARRATOR_UPLOAD_PHOTOS_2, NARRATOR_UPLOAD_PHOTOS_3

    # Accumulate accounting changes.
    self._acc_accum = AccountingAccumulator()

    self._unique_id = self._unique_id_start
    self._update_seq = 1

    # Create the viewpoint.
    self._viewpoint_id = Viewpoint.ConstructViewpointId(self._new_user.webapp_dev_id, self._unique_id)
    self._unique_id += 1
    initial_follower_ids = [self._new_user.user_id]
    viewpoint, followers = yield Viewpoint.CreateNewWithFollowers(self._client,
                                                                  follower_ids=initial_follower_ids,
                                                                  user_id=NARRATOR_USER.user_id,
                                                                  viewpoint_id=self._viewpoint_id,
                                                                  type=Viewpoint.SYSTEM,
                                                                  title='Welcome...',
                                                                  timestamp=self._op.timestamp)

    # Narrator creates and introduces the conversation.
    yield self._CreateActivity(NARRATOR_USER,
                               self._op.timestamp - 60,
                               Activity.CreateShareNew,
                               ep_dicts=[],
                               follower_ids=initial_follower_ids)

    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 60,
                            'Welcome to Viewfinder, a new way to privately share photos with your friends.')

    # Narrator shares photos.
    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 59,
                            'Select as many photos as you want to share with exactly who you want.')

    photo_ids = [ph_dict['photo_id'] for ph_dict in NARRATOR_UPLOAD_PHOTOS['photos']]
    episode = yield self._CreateEpisodeWithPosts(NARRATOR_USER,
                                                 NARRATOR_UPLOAD_PHOTOS['episode']['episode_id'],
                                                 NARRATOR_UPLOAD_PHOTOS['photos'])
    yield self._CreateActivity(NARRATOR_USER,
                               self._op.timestamp - 58,
                               Activity.CreateShareExisting,
                               ep_dicts=[{'new_episode_id': episode.episode_id, 'photo_ids': photo_ids}])

    # Set cover photo on viewpoint now that episode id is known.
    viewpoint.cover_photo = {'episode_id': episode.episode_id,
                             'photo_id': NARRATOR_UPLOAD_PHOTOS['photos'][0]['photo_id']}
    yield gen.Task(viewpoint.Update, self._client)

    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 56,
                            'Your friends can also add photos to the conversation, '
                            'creating unique collaborative albums.')

    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 55,
                            'You can add as many photos, messages and friends as you want to the conversation, '
                            'leading to a memorable shared experience.')

    # Narrator shares more photos.
    photo_ids = [ph_dict['photo_id'] for ph_dict in NARRATOR_UPLOAD_PHOTOS_2['photos']]
    episode = yield self._CreateEpisodeWithPosts(NARRATOR_USER,
                                                 NARRATOR_UPLOAD_PHOTOS_2['episode']['episode_id'],
                                                 NARRATOR_UPLOAD_PHOTOS_2['photos'])
    yield self._CreateActivity(NARRATOR_USER,
                               self._op.timestamp - 54,
                               Activity.CreateShareExisting,
                               ep_dicts=[{'new_episode_id': episode.episode_id, 'photo_ids': photo_ids}])

    # Single-photo comment.
    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 53,
                            'Hold and press on photos to comment on specific pics.',
                            asset_id=NARRATOR_UPLOAD_PHOTOS_2['photos'][1]['photo_id'])

    # Narrator rambles on for a while.
    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 52,
                            'Use mobile #\'s or email addresses to add new people if they\'re not yet on Viewfinder.');

    # Narrator shares more photos.
    photo_ids = [ph_dict['photo_id'] for ph_dict in NARRATOR_UPLOAD_PHOTOS_3['photos']]
    episode = yield self._CreateEpisodeWithPosts(NARRATOR_USER,
                                                 NARRATOR_UPLOAD_PHOTOS_3['episode']['episode_id'],
                                                 NARRATOR_UPLOAD_PHOTOS_3['photos'])
    yield self._CreateActivity(NARRATOR_USER,
                               self._op.timestamp - 51,
                               Activity.CreateShareExisting,
                               ep_dicts=[{'new_episode_id': episode.episode_id, 'photo_ids': photo_ids}])


    # Conclusion.
    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 50,
                            'Viewfinder is perfect for vacations, weddings, or any shared experience where you want '
                            'to share photos without posting them for everyone to see.')

    yield self._PostComment(NARRATOR_USER,
                            self._op.timestamp - 49,
                            'Start sharing now.')

    # Validate that we allocated enough ids and counted update_seq properly.
    assert self._unique_id == self._unique_id_start + CreateProspectiveOperation._ASSET_ID_COUNT, self._unique_id
    assert self._update_seq == CreateProspectiveOperation._UPDATE_SEQ_COUNT, self._update_seq

    # Set update_seq on the new viewpoint.
    viewpoint.update_seq = self._update_seq
    yield gen.Task(viewpoint.Update, self._client)

    # Remove this viewpoint for all sample users so that accounting will be correct (also in case
    # we want to sync a device to Nick's account and see if new users are trying to chat). Also
    # update viewed_seq so that entire conversation is "read" for each sample user.
    for follower in followers:
      if follower.user_id != self._new_user.user_id:
        follower.viewed_seq = viewpoint.update_seq
        yield follower.RemoveViewpoint(self._client)

    # Commit accounting changes.
    yield self._acc_accum.Apply(self._client)

  @gen.coroutine
  def _CreateActivity(self, sharer_user, timestamp, activity_func, **kwargs):
    """Creates an activity by invoking "activity_func" with the given args."""
    activity_id = Activity.ConstructActivityId(timestamp, self._new_user.webapp_dev_id, self._unique_id)
    self._unique_id += 1
    activity = yield activity_func(self._client,
                                   sharer_user.user_id,
                                   self._viewpoint_id,
                                   activity_id,
                                   timestamp,
                                   update_seq=self._update_seq,
                                   **kwargs)
    self._update_seq += 1
    raise gen.Return(activity)

  @gen.coroutine
  def _CreateEpisodeWithPosts(self, sharer_user, parent_ep_id, ph_dicts):
    """Creates a new episode containing the given photos."""
    # Create the episode.
    episode_id = Episode.ConstructEpisodeId(self._op.timestamp, self._new_user.webapp_dev_id, self._unique_id)
    self._unique_id += 1
    episode = yield gen.Task(Episode.CreateNew,
                             self._client,
                             episode_id=episode_id,
                             parent_ep_id=parent_ep_id,
                             user_id=sharer_user.user_id,
                             viewpoint_id=self._viewpoint_id,
                             publish_timestamp=util.GetCurrentTimestamp(),
                             timestamp=self._op.timestamp,
                             location=ph_dicts[0].get('location', None),
                             placemark=ph_dicts[0].get('placemark', None))

    # Create the photos from photo dicts.
    photo_ids = [ph_dict['photo_id'] for ph_dict in ph_dicts]
    for photo_id in photo_ids:
      yield gen.Task(Post.CreateNew, self._client, episode_id=episode_id, photo_id=photo_id)

    # Update accounting, but only apply to the new user, since system users will remove
    # themselves from the viewpoint.
    yield self._acc_accum.SharePhotos(self._client,
                                      sharer_user.user_id,
                                      self._viewpoint_id,
                                      photo_ids,
                                      [self._new_user.user_id])

    # Update viewpoint shared by total for the sharing user.
    self._acc_accum.GetViewpointSharedBy(self._viewpoint_id, sharer_user.user_id).IncrementFromPhotoDicts(ph_dicts)

    raise gen.Return(episode)

  @gen.coroutine
  def _PostComment(self, sharer_user, timestamp, message, asset_id=None):
    """Creates a new comment and a corresponding activity."""
    comment_id = Comment.ConstructCommentId(timestamp, self._new_user.webapp_dev_id, self._unique_id)
    self._unique_id += 1
    comment = yield Comment.CreateNew(self._client,
                                      viewpoint_id=self._viewpoint_id,
                                      comment_id=comment_id,
                                      user_id=sharer_user.user_id,
                                      asset_id=asset_id,
                                      timestamp=timestamp,
                                      message=message)

    # Create post_comment activity.
    yield self._CreateActivity(sharer_user, timestamp, Activity.CreatePostComment, cm_dict={'comment_id': comment_id})

    raise gen.Return(comment)