# Copyright 2012 Viewfinder Inc. All Rights Reserved.

"""JSON Schemas for request/response pairs used with Viewfinder ops.

All times are expressed in seconds (possibly subsecond floating-point
precision) since the epoch (January 1st, 1970) in UTC.

Every mutating request may be run synchronously by specifying
'synchronous' = True in the request header.
"""

__authors__ = ['spencer@emailscrubbed.com (Spencer Kimball)',
               'andy@emailscrubbed.com (Andy Kimball)']

from copy import deepcopy

from viewfinder.backend.db.follower import Follower
from viewfinder.backend.db.contact import Contact
from viewfinder.backend.db.settings import AccountSettings
from viewfinder.backend.db.viewpoint import Viewpoint


##
# HELPER METHODS
##

def _MakeOptional(property_dict, test_key):
  """Iterates through all key/value pairs in the property dictionary. If the "test_key" function
  returns True for a particular property key, then makes that property optional. Returns the
  updated property dict.
  """
  for key, value in property_dict.items():
    if test_key(key):
      property_dict[key]['required'] = False
  return property_dict

def _CopyProperties(target_dict, source_dict):
  """Deep copies properties in source_dict['properties'] to target_dict['properties']. Asserts
  if a property of the same name already exists in source_dict['properties'], but has a
  different value.
  """
  for key, value in source_dict['properties'].items():
    assert key not in target_dict['properties'] or target_dict['properties'][key] == value, (source_dict, target_dict)
    target_dict['properties'][key] = deepcopy(value)


##
# COMMON DATA STRUCTURES
##

# Headers object that must be at the top-level of every request or response message.

HEADERS = {
  'description': 'contains headers used for read-only methods',
  'type': 'object',
  'properties': {
    'version': {
      'description': 'version of the message format; this is not necessarily equal '
      'to the max version supported by the sender; for example, the server will '
      'respond in an older dialect to older clients',
      'type': 'integer',
      },
    'min_required_version': {
      'description': 'this field requires that the recipient be able to understand '
      'messages of this version or greater; if not, the recipient may report an '
      'error (server), or it may try again after upgrade to a later version (client)',
      'type': 'integer',
      'required': False,
      },
    'synchronous': {
      'description': 'used in tests to wait until a requested operation completes',
      'type': 'boolean',
      'required': False,
      },
    },
  }

OP_HEADERS = {
  'description': 'contains headers used for mutable methods',
  'type': 'object',
  'properties': {
    'op_id': {
      'description': 'id of the requested operation; the id is a composite of '
      'the device id and a unique operation id generated by that device; the '
      'client may provide the op_id, or if it does not, the server will '
      'generate one',
      'type': 'string',
      },
    'op_timestamp': {
      'description': 'timestamp of this requested operation; the client may '
      'provide the op_timestamp, or if it does not, the server will generate one',
      'type': 'number',
      },
    },
  }
_CopyProperties(target_dict=OP_HEADERS, source_dict=HEADERS)


# Location in degrees of latitude and longitude and accuracy in meters.

LOCATION = {
  'description': 'location in degrees of latitude & longitude and accuracy in meters',
  'type': 'object',
  'properties': {
    'latitude': {'type': 'number'},
    'longitude': {'type': 'number'},
    'accuracy': {'type': 'number'},
    },
  }

OPTIONAL_LOCATION = deepcopy(LOCATION)
OPTIONAL_LOCATION['required'] = False


# Hierarchical place names from country to street level.

PLACEMARK = {
  'description': 'placemark identifies a place by name from country to street level',
  'type': 'object',
  'properties': {
    'iso_country_code': {'type': 'string', 'blank': True, 'required': False},
    'country': {'type': 'string', 'blank': True, 'required': False},
    'state': {'type': 'string', 'blank': True, 'required': False},
    'locality': {'type': 'string', 'blank': True, 'required': False},
    'sublocality': {'type': 'string', 'blank': True, 'required': False},
    'thoroughfare': {'type': 'string', 'blank': True, 'required': False},
    'subthoroughfare': {'type': 'string', 'blank': True, 'required': False},
    },
  }

OPTIONAL_PLACEMARK = deepcopy(PLACEMARK)
OPTIONAL_PLACEMARK['required'] = False


# Select a set of assets, with some control over the scope of projection.

VIEWPOINT_SELECTION = {
  'description': 'select a set of viewpoints by id; if "get_attributes" is '
  'True or not specified, then return all attributes on the viewpoints; '
  '"get_followers", "get_activities", "get_episodes", and "get_comments" '
  'specify whether to return the corresponding collections associated with '
  'the viewpoint; "start_key" fields enable paging of the collections',
  'type': 'array',
  'items': {
    'type': 'object',
    'properties': {
      'viewpoint_id': {'type': 'string'},
      'get_attributes': {'type': 'boolean', 'required': False},
      'get_followers': {'type': 'boolean', 'required': False},
      'follower_start_key': {'type': 'string', 'required': False},
      'get_activities': {'type': 'boolean', 'required': False},
      'activity_start_key': {'type': 'string', 'required': False},
      'get_episodes': {'type': 'boolean', 'required': False},
      'episode_start_key': {'type': 'string', 'required': False},
      'get_comments': {'type': 'boolean', 'required': False},
      'comment_start_key': {'type': 'string', 'required': False},
      },
    },
  }

EPISODE_SELECTION = {
  'description': 'select a set of episodes by id; if "get_attributes" is '
  'True or not specified, then return all attributes on the episodes; if '
  '"get_photos" is True or not specified, return photos in the episode, '
  'starting with "photo_start_key" if it is specified',
  'type': 'array',
  'items': {
    'type': 'object',
    'properties': {
      'episode_id': {'type': 'string'},
      'get_attributes': {'type': 'boolean', 'required': False},
      'get_photos': {'type': 'boolean', 'required': False},
      'photo_start_key': {'type': 'string', 'required': False},
      },
    },
  }

USER_SELECTION = {
  'description': 'select set of users by id',
  'type': 'array',
  'items': {'type': 'number'},
  }

CONTACT_SELECTION = {
  'description': 'select set of contacts that have a sort_key greater '
  'than start_key',
  'type': 'object',
  'properties': {
    'all': {
      'description': 'invalidate all contacts, forcing complete client reload',
      'type': 'boolean', 'required': False,
      },
    'start_key': {'type': 'string'},
    },
  }


# Cover photo metadata.
COVER_PHOTO_METADATA = {
  'description': 'describes cover photo for a shared viewpoint',
  'type': 'object',
  'required': False,
  'properties': {
    'episode_id': {
      'description': 'episode_id of episode that contains cover photo',
      'type': 'string'
      },
    'photo_id': {
      'description': 'photo_id of cover photo',
      'type': 'string'
      },
    },
  }


# Activity metadata.

CREATE_ACTIVITY_METADATA = {
  'description': 'activity metadata for creation',
  'type': 'object',
  'properties': {
    'activity_id': {'type': 'string'},
    'timestamp': {
      'description': 'time that activity was created on the client',
      'type': 'number',
      },
    },
  }

ACTIVITY_POST_ARRAY = {
  'description': 'array of (episode, photo_id) tuples used by share '
  'and unshare activities',
  'type': 'array',
  'items': {
    'type': 'object',
    'properties': {
      'episode_id': {'type': 'string'},
      'photo_ids': {
        'type': 'array',
        'items': {'type': 'string'},
        },
      },
    },
  }

ACTIVITY_METADATA = {
  'description': 'full activity metadata (includes create metadata)',
  'type': 'object',
  'properties': {
    'viewpoint_id': {'type': 'string'},
    'user_id': {
      'description': 'id of user that caused the activity to be created',
      'type': 'number',
      },
    'update_seq': {
      'description': 'set to the value of the viewpoint\'s update_seq '
      'attribute after it is incremented during the creation of the '
      'activity',
      'type': 'number',
      },
    'add_followers': {
      'description': 'new followers added to this viewpoint',
      'required': False,
      'type': 'object',
      'properties': {
        'follower_ids': {
          'description': 'user ids of new viewpoint followers',
          'type': 'array',
          'items': {'type': 'number'},
          },
        },
      },
    'merge_accounts': {
      'description': 'user accounts merged; target user added to this viewpoint',
      'required': False,
      'type': 'object',
      'properties': {
        'target_user_id': {
          'description': 'user that receives the assets to be merged; this user remains after '
          'the merge is completed',
          'type': 'number',
          },
        'source_user_id': {
          'description': 'user that provides the assets to be merged; the account of this user '
          'is terminated after the merge is completed',
          'type': 'number',
          },
        },
      },
    'post_comment': {
      'description': 'comment posted to this viewpoint',
      'required': False,
      'type': 'object',
      'properties': {
        'comment_id': {'type': 'string'},
        },
      },
    'remove_followers': {
      'description': 'followers removed from this viewpoint',
      'required': False,
      'type': 'object',
      'properties': {
        'follower_ids': {
          'description': 'user ids of removed viewpoint followers',
          'type': 'array',
          'items': {'type': 'number'},
          },
        },
      },
    'save_photos': {
      'description': 'photos saved to default viewpoint',
      'required': False,
      'type': 'object',
      'properties': {
        'episodes': ACTIVITY_POST_ARRAY,
        },
      },
    'share_existing': {
      'description': 'photos shared to an already existing viewpoint',
      'required': False,
      'type': 'object',
      'properties': {
        'episodes': ACTIVITY_POST_ARRAY,
        },
      },
    'share_new': {
      'description': 'photos shared to a newly created viewpoint',
      'required': False,
      'type': 'object',
      'properties': {
        'episodes': ACTIVITY_POST_ARRAY,
        'follower_ids': {
          'description': 'user ids of new viewpoint followers; excludes '
          'the creating user id',
          'type': 'array',
          'items': {'type': 'number'},
          },
        },
      },
    'unshare': {
      'description': 'photos unshared from this viewpoint',
      'required': False,
      'type': 'object',
      'properties': {
        'episodes': ACTIVITY_POST_ARRAY,
        },
      },
    'update_episode': {
      'description': 'episode metadata updated in this viewpoint',
      'required': False,
      'type': 'object',
      'properties': {
        'episode_id': {'type': 'string'},
        },
      },
    'update_viewpoint': {
      'description': 'viewpoint metadata updated',
      'required': False,
      'type': 'object',
      'properties': {
        'viewpoint_id': {'type': 'string'},
        'prev_title': {'type': 'string', 'required': False},
        'prev_cover_photo': COVER_PHOTO_METADATA,
        },
      },
    'upload_episode': {
      'description': 'photos uploaded to an episode in this viewpoint',
      'required': False,
      'type': 'object',
      'properties': {
        'episode_id': {'type': 'string'},
        'photo_ids': {
          'type': 'array',
          'items': {'type': 'string'},
          },
        },
      },
    },
  }
_CopyProperties(target_dict=ACTIVITY_METADATA, source_dict=CREATE_ACTIVITY_METADATA)


# Photo metadata.

PHOTO_URL_METADATA = {
  'description': 'metadata for signed S3 URLs that reference photo image data',
  'type': 'object',
  'properties': {
    'tn_get_url': {
      'description': 'url for thumbnail resolution image file; '
      'URL expires in 24 hours--only returned with photo query responses',
      'type': 'string', 'required': False,
      },
    'med_get_url': {
      'description': 'url for medium-screen resolution image file (max 480 pixels); '
      'URL expires in 24 hours--only returned with photo query responses',
      'type': 'string', 'required': False,
      },
    'full_get_url': {
      'description': 'url for full-screen resolution image file (max 960 pixels); '
      'URL expires in 24 hours--only returned with photo query responses',
      'type': 'string', 'required': False,
      },
    'orig_get_url': {
      'description': 'url for full-screen resolution image file; '
      'URL expires in 24 hours--only returned with photo query responses',
      'type': 'string', 'required': False,
      },
    },
  }

USER_PHOTO_METADATA = {
  'description': 'per-user photo metadata',
  'type': 'object',
  'properties': {
    'photo_id': {'type': 'string'},
    'asset_keys': {
      'description': 'identifiers for copies of this photo in the user\'s devices\' native '
      'asset library.  This field is per-user, and its format is client-specific',
      'type': 'array',
      'required': False,
      'items': {'type': 'string'},
      },
    },
  }

UPDATE_PHOTO_METADATA = {
  'description': 'photo metadata for updates',
  'type': 'object',
  'properties': {
    'location': OPTIONAL_LOCATION,
    'placemark': OPTIONAL_PLACEMARK,
    'caption': {'type': 'string', 'required': False},
    'link': {'type': 'string', 'required': False},
    },
  }
_CopyProperties(target_dict=UPDATE_PHOTO_METADATA, source_dict=USER_PHOTO_METADATA)

UPLOAD_PHOTO_METADATA = {
  'description': 'photo metadata for upload (includes update metadata)',
  'type': 'object',
  'properties': {
    'timestamp': {
      'description': 'time that photo was created on the client',
      'type': 'number',
      },
    'aspect_ratio': {
      'description': 'floating point value: width / height',
      'type': 'number',
      },
    'tn_md5': {
      'description': 'thumbnail resolution md5 csum',
      'type': 'string',
      },
    'med_md5': {
      'description': 'medium resolution md5 csum (max 480 pixels)',
      'type': 'string',
      },
    'full_md5': {
      'description': 'full-screen resolution md5 csum (max 960 pixels)',
      'type': 'string',
      },
    'orig_md5': {
      'description': 'original resolution md5 csum',
      'type': 'string',
      },
    'tn_size': {
      'description': 'thumbnail resolution size in bytes',
      'type': 'integer', 'required': False,
      },
    'med_size': {
      'description': 'medium resolution size in bytes (max 480 pixels)',
      'type': 'integer', 'required': False,
      },
    'full_size': {
      'description': 'full-screen resolution size in bytes (max 960 pixels)',
      'type': 'integer', 'required': False,
      },
    'orig_size': {
      'description': 'original resolution size in bytes',
      'type': 'integer', 'required': False,
      },
    'content_type': {
      'description': 'image file content type (e.g. image/jpeg)',
      'type': 'string', 'required': False,
      },
    'parent_id': {
      'description': 'if specified, this photo was derived from another',
      'type': 'string', 'required': False,
      },
    },
  }
_CopyProperties(target_dict=UPLOAD_PHOTO_METADATA, source_dict=UPDATE_PHOTO_METADATA)

PHOTO_METADATA = {
  'description': 'full photo metadata (includes upload metadata)',
  'type': 'object',
  'properties': {
    'user_id': {
      'description': 'id of user that created the photo',
      'type': 'number'
      },
    'episode_id': {
      'description': 'episode in which the photo was originally uploaded',
      'type': 'string', 'required': False,
      },
    'labels': {
      'description': 'set of boolean modifiers affecting the photo (e.g. "removed")',
      'type': 'array', 'required': False, 'items': {'type': 'string'},
      },
    'sharing_user_id': {
      'description': 'user who shared this photo (if applicable)',
      'type': 'number', 'required': False,
      },
    },
  }
_CopyProperties(target_dict=PHOTO_METADATA, source_dict=UPLOAD_PHOTO_METADATA)
_CopyProperties(target_dict=PHOTO_METADATA, source_dict=PHOTO_URL_METADATA)

# Older photos may be missing one or more MD5 attributes.
PHOTO_METADATA['properties']['tn_md5']['required'] = False
PHOTO_METADATA['properties']['med_md5']['required'] = False
PHOTO_METADATA['properties']['full_md5']['required'] = False
PHOTO_METADATA['properties']['orig_md5']['required'] = False

POST_PHOTO_METADATA = deepcopy(PHOTO_METADATA)


# Episode metadata.

UPDATE_EPISODE_METADATA = {
  'description': 'episode metadata for updates',
  'type': 'object',
  'properties': {
    'episode_id': {'type': 'string'},
    'title': {'type': 'string', 'required': False},
    'description': {'type': 'string', 'required': False},
    'location': OPTIONAL_LOCATION,
    'placemark': OPTIONAL_PLACEMARK,
    },
  }

UPLOAD_EPISODE_METADATA = {
  'description': 'episode metadata for upload (includes update metadata)',
  'type': 'object',
  'properties': {
    'timestamp': {
      'description': 'timestamp of the newest photo in the episode',
      'type': 'number',
      },
    },
  }
_CopyProperties(target_dict=UPLOAD_EPISODE_METADATA, source_dict=UPDATE_EPISODE_METADATA)

EPISODE_METADATA = {
  'description': 'full episode metadata (includes upload metadata)',
  'type': 'object',
  'properties': {
    'user_id': {
      'description': 'id of user that created the episode',
      'type': 'number'
      },
    'viewpoint_id': {
      'description': 'viewpoint to which the episode belongs',
      'type': 'string',
      },
    'publish_timestamp': {
      'description': 'time at which the episode was uploaded',
      'type': 'number',
      },
    'sharing_user_id': {
      'description': 'user who shared this episode (if applicable)',
      'type': 'number', 'required': False,
      },
    'parent_ep_id': {
      'description': 'id of the parent episode, if one exists',
      'type': 'string', 'required': False,
      },
    },
  }
_CopyProperties(target_dict=EPISODE_METADATA, source_dict=UPLOAD_EPISODE_METADATA)


# Follower metadata.

UPDATE_FOLLOWER_METADATA = {
  'description': 'follower metadata for updates',
  'type': 'object',
  'properties': {
    'viewpoint_id': {'type': 'string'},
    'labels': {
      'description': 'set of boolean permissions and modifiers affecting '
      'the viewpoint (e.g. "personal")',
      'type': 'array', 'required': False, 'uniqueItems': True,
      'items': {'type': 'string', 'enum': Follower.ALL_LABELS},
      },
    'viewed_seq': {
      'description': 'sequence number of last viewpoint update that the '
      'client has viewed on any device; the client and server will always '
      '"ratchet up" this value; they will ignore any value that is smaller '
      'than a value already received',
      'type': 'number', 'required': False,
      },
    },
  }

FRIEND_FOLLOWER_METADATA = {
  'description': 'follower metadata that is returned to other followers of same viewpoint when '
  'they invoke query_viewpoints',
  'type': 'object',
  'properties': {
    'follower_id': {'type': 'number'},
    'labels': {
      'description': 'set of boolean permissions and modifiers affecting the follower\'s '
      'relationship to the viewpoint',
      'type': 'array', 'required': False, 'uniqueItems': True,
      'items': {'type': 'string', 'enum': [Follower.REMOVED, Follower.UNREVIVABLE]},
      },
    'adding_user_id': {
      'description': 'user who added this follower to the viewpoint; for older viewpoints, '
      'this may not be present; it also is not present for the user that created the viewpoint',
      'type': 'number', 'required': False,
      },
    'follower_timestamp': {
      'description': 'timestamp at which follower was added to the viewpoint; if not present, '
      'assume follower was added more than 7 days ago',
      'type': 'number', 'required': False,
      },
    },
  }


# Viewpoint metadata.

UPDATE_VIEWPOINT_METADATA = {
  'description': 'viewpoint metadata for updates',
  'type': 'object',
  'properties': {
    'viewpoint_id': {'type': 'string'},
    'title': {'type': 'string', 'required': False},
    'description': {'type': 'string', 'required': False},
    'name': {'type': 'string', 'required': False},
    'cover_photo': COVER_PHOTO_METADATA,
    },
  }

CREATE_VIEWPOINT_METADATA = {
  'description': 'viewpoint metadata for create (includes update metadata)',
  'type': 'object',
  'properties': {
    'type': {
      'description': 'kind of viewpoint (only allow event viewpoint to be created by users)',
      'type': 'string', 'enum': [Viewpoint.EVENT],
      },
    },
  }
_CopyProperties(target_dict=CREATE_VIEWPOINT_METADATA, source_dict=UPDATE_VIEWPOINT_METADATA)

VIEWPOINT_METADATA = {
  'description': 'full viewpoint metadata (includes create metadata)',
  'type': 'object',
  'properties': {
    'follower_id': {
      'description': 'id of the calling user who follows this viewpoint',
      'type': 'number',
      },
    'user_id': {
      'description': 'id of user that created the viewpoint',
      'type': 'number'
      },
    'timestamp': {
      'description': 'timestamp at which viewpoint was created',
      'type': 'number',
      },
    'update_seq': {
      'description': 'sequence number of the last add, remove, or update of '
      'any assets or metadata within the viewpoint; only updates to shared '
      'assets increment this value (i.e. not changes to user-specific '
      'tables like Follower or UserPost)',
      'type': 'number', 'required': False,
      },
    'adding_user_id': {
      'description': 'user who added this follower to the viewpoint (if applicable)',
      'type': 'number', 'required': False,
      },
    'last_updated': {
      'description': 'timestamp of the activity that was last added to '
      'the viewpoint',
      'type': 'number', 'required': False,
      },
    },
  }
_CopyProperties(target_dict=VIEWPOINT_METADATA, source_dict=CREATE_VIEWPOINT_METADATA)
_CopyProperties(target_dict=VIEWPOINT_METADATA, source_dict=UPDATE_FOLLOWER_METADATA)
_CopyProperties(target_dict=VIEWPOINT_METADATA['properties']['cover_photo'], source_dict=PHOTO_URL_METADATA)
VIEWPOINT_METADATA['properties']['type']['enum'] = Viewpoint.TYPES


# Copying episodes between viewpoints.

COPY_EPISODES_METADATA = {
  'description': 'array of episode copy information; each item specifies the existing episode '
  'id, the new episode id, and the photo ids to include in the copied episode',
  'type': 'array',
  'items': {
    'type': 'object',
    'properties': {
      'existing_episode_id': {
        'description': 'id of the episode from which the copy originates; '
        'this will be the parent_ep_id of the new episode',
        'type': 'string',
        },
      'new_episode_id': {
        'description': 'id of the new episode to create',
        'type': 'string',
        },
      'photo_ids': {
        'description': 'ids of photos to copy from the existing episode',
        'type': 'array',
        'items': {'type': 'string'},
        },
      },
    },
  }

OPTIONAL_COPY_EPISODES_METADATA = deepcopy(COPY_EPISODES_METADATA)
OPTIONAL_COPY_EPISODES_METADATA['required'] = False


# Device metadata.

DEVICE_METADATA = {
  'description': 'full device metadata properties',
  'type': 'object',
  'properties': {
    'device_id': {
      'description': 'unique identifier of the device. Generated on the server.',
      'type': 'number',
      },
    'name': {
      'description': 'name of device',
      'type': 'string', 'blank': True, 'required': False,
      },
    'version': {
      'description': 'version of the Viewfinder mobile application',
      'type': 'string', 'blank': True, 'required': False,
      },
    'platform': {
      'description': 'mobile platform (e.g. iPhone 4S, Samsung Galaxy S)',
      'type': 'string', 'blank': True, 'required': False,
      },
    'os': {
      'description': 'mobile os (e.g. iOS 5.0.1, Android 4.0)',
      'type': 'string', 'blank': True, 'required': False,
      },
    'push_token': {
      'description': 'opaque token for push notifications',
      'type': 'string', 'blank': True, 'required': False,
      },
    'device_uuid': {
      'description': 'per-install unique device id. Generated on the device.',
      'type': 'string', 'blank': True, 'required': False,
      },
    'language': {
      'description': 'device language code',
      'type': 'string', 'blank': True, 'required': False,
      },
    'country': {
      'description': 'device country code',
      'type': 'string', 'blank': True, 'required': False,
      },
    'test_udid': {
      'description': 'unique device ID. Only sent by DEV and ADHOC builds. ID matches that found on testflight.',
      'type': 'string', 'blank': True, 'required': False,
      },
    },
  }

# Device id is optional when registering a device.
REGISTER_DEVICE_METADATA = deepcopy(DEVICE_METADATA)
REGISTER_DEVICE_METADATA['required'] = False
REGISTER_DEVICE_METADATA['properties']['device_id']['required'] = False


# Information message sent as part of the ping response.
INFO_MESSAGE = {
  'description': 'an informative message to the client',
  'type': 'object',
  'properties': {
    'title': {'type': 'string'},
    'body': {'type': 'string', 'required': False},
    'link': {'type': 'string', 'required': False},
    'identifier': {
      'description': 'Unique identifier for this message. The client will not re-display a message with the '
      'same identifier. However, a new identifier, then an old again will work (eg: A -> B -> A)',
      'type': 'string',
      },
    'severity': {
      'description': 'Severity level. One of "SILENT", "INFO", "ATTENTION", "DISABLE_NETWORK"',
      'type': 'string',
      },
    },
  }

PING_RESPONSE_MESSAGE = deepcopy(INFO_MESSAGE)
PING_RESPONSE_MESSAGE['required'] = False


# Comment metadata.

POST_COMMENT_METADATA = {
  'description': 'comment metadata used when posting a comment',
  'type': 'object',
  'properties': {
    'viewpoint_id': {'type': 'string'},
    'comment_id': {'type': 'string'},
    'asset_id': {
      'description': 'id of the viewpoint asset to which this comment is '
      'attached; this may be a photo, if the comment was about a photo; it '
      'may be another comment, if this comment was a direct response to '
      'that comment',
      'type': 'string',
      'required': False,
      },
    'timestamp': {
      'description': 'timestamp of the new comment; this timestamp MUST '
      'be the same across successive request attempts by the client in '
      'order to guarantee idempotency',
      'type': 'number',
      },
    'message': {
      'description': 'text of the comment',
      'type': 'string',
      },
    },
  }

COMMENT_METADATA = {
  'description': 'full comment metadata (includes post metadata)',
  'type': 'object',
  'properties': {
    'user_id': {
      'description': 'id of user that caused the comment to be created',
      'type': 'number',
      },
    },
  }
_CopyProperties(target_dict=COMMENT_METADATA, source_dict=POST_COMMENT_METADATA)


# Contact metadata.

UPLOAD_CONTACT_METADATA = {
  'description': '(name, given_name, family_name, rank, contact_source, identities) tuple',
  'type': 'object',
  'properties': {
    'contact_source': {
      'description': 'Source of contacts: ip (iPhone), or m (Manual)',
      'type': 'string',
      'enum': Contact.UPLOAD_SOURCES
      },
    'identities': {
      'description': 'Order of this list will be preserved by server for query_contacts responses',
      'type': 'array',
      'maxItems': 50,
      'items': {
        'type': 'object',
        'properties': {
          'identity': {'type': 'string', 'maxLength': 1000},
          'description': {'type': 'string', 'maxLength': 1000, 'required': False},
          },
        },
      },
    'name': {'type': 'string', 'maxLength': 1000, 'required': False},
    'given_name': {'type': 'string', 'maxLength': 1000, 'required': False},
    'family_name': {'type': 'string', 'maxLength': 1000, 'required': False},
    'rank': {'type': 'number', 'required': False},
    },
  }

QUERY_CONTACT_METADATA = {
  'description': 'Metadata returned in query_contacts response',
  'type': 'object',
  'properties': {
    'contact_id': {'type': 'string'},
    'labels': {
      'description': 'set of boolean modifiers affecting the contact (e.g. "removed")',
      'type': 'array', 'required': False, 'items': {'type': 'string'},
      },
    },
  }
_CopyProperties(target_dict=QUERY_CONTACT_METADATA, source_dict=UPLOAD_CONTACT_METADATA)
QUERY_CONTACT_METADATA['properties']['contact_source']['enum'] = Contact.ALL_SOURCES
QUERY_CONTACT_METADATA['properties']['identities']['items']['properties']['user_id'] = \
  {'type': 'number', 'required': False}
QUERY_CONTACT_METADATA['properties']['identities']['required'] = False

FOLLOWER_CONTACTS_METADATA = {
  'description': 'array of contacts to add as followers of a viewpoint',
  'type': 'array',
  'items': {
    'description': 'contacts: identity key and name if available. '
    'user_id is required if known.',
    'type': 'object',
    'properties': {
      'user_id': {'type': 'number', 'required': False},
      'identity': {'type': 'string', 'required': False},
      'name': {'type': 'string', 'required': False},
      },
    },
  }


# Invalidate structure.

INVALIDATE = {
  'description': 'each notification can select parts of the asset tree to '
  'invalidate if the operation that triggered the notification modified '
  'the tree',
  'type': 'object',
  'properties': {
    'all': {
      'description': 'invalidate all assets, forcing complete client reload',
      'type': 'boolean',
      },
    'viewpoints': VIEWPOINT_SELECTION,
    'episodes': EPISODE_SELECTION,
    'users': USER_SELECTION,
    'contacts': CONTACT_SELECTION,
    },
  }


# Usage information for a given category.
USAGE_CATEGORY_METADATA = {
  'description': 'usage information for a single category',
  'type': 'object',
  'required': False,
  'properties': {
    'num_photos': { 'type': 'number', 'required': False },
    'tn_size': { 'type': 'number', 'required': False },
    'med_size': { 'type': 'number', 'required': False },
    'full_size': { 'type': 'number', 'required': False },
    'orig_size': { 'type': 'number', 'required': False },
  },
}

# Usage information for a single user.
USAGE_METADATA = {
  'description': 'usage information by category',
  'type': 'object',
  'properties': {
    'owned_by': deepcopy(USAGE_CATEGORY_METADATA),
    'shared_by': deepcopy(USAGE_CATEGORY_METADATA),
    'visible_to': deepcopy(USAGE_CATEGORY_METADATA),
  },
}

# The optional variant is used in NOTIFICATION. Currently, the last notification in the response to
# QueryNotifications will have the usage information.
OPTIONAL_USAGE_METADATA = deepcopy(USAGE_METADATA)
OPTIONAL_USAGE_METADATA['required'] = False


# Notification structure.

NOTIFICATION = {
  'description': 'a union of notifications delivered to client asynchronously',
  'type': 'object',
  'properties': {
    'notification_id': {'type': 'number'},
    'name': {'type': 'string'},
    'sender_id': {'type': 'number'},
    'op_id': {
      'description': 'id of the operation that produced this notification; this attribute '
      'will be missing if no operation was involved',
      'type': 'string', 'required': False,
      },
    'timestamp': {'type': 'number'},
    'invalidate': deepcopy(INVALIDATE),
    'inline': {
      'description': 'some common invalidations are in-lined in the notification '
      'in order to avoid extra round-trips',
      'type': 'object', 'required': False,
      'properties': {
        'activity': deepcopy(ACTIVITY_METADATA),
        'viewpoint': {
          'description': 'if this notification updates the value of the update_seq '
          'and / or viewed_seq attributes, then in-line the changed value(s) in '
          'order to reduce round-trips',
          'type': 'object', 'required': False,
          'properties': {
            'viewpoint_id': {'type': 'string'},
            'update_seq': {
              'description': 'value of the viewpoint update_seq attribute after '
              'it was incremented by the operation; the client will "ratchet up" '
              'this value, discarding any that is smaller than a value already '
              'received',
              'type': 'number', 'required': False,
              },
            'viewed_seq': {
              'description': 'value of the follower viewed_seq attribute after '
              'it was incremented for the user who submitted the operation; '
              'the client will "ratchet up" this value, discarding any that '
              'is smaller than a value already received',
              'type': 'number', 'required': False,
              },
            },
          },
        'comment': deepcopy(COMMENT_METADATA),
        'user': {
          'description': 'user information', 'type': 'object', 'required': False,
          'usage': deepcopy(OPTIONAL_USAGE_METADATA),
        },
      },
    },
  },
}
_MakeOptional(NOTIFICATION['properties']['invalidate']['properties'], lambda key: True)
NOTIFICATION['properties']['invalidate']['required'] = False
NOTIFICATION['properties']['inline']['properties']['activity']['required'] = False
NOTIFICATION['properties']['inline']['properties']['comment']['required'] = False


# Error response.

ERROR_RESPONSE = {
  'description': 'on an error, returns code and message for debugging client',
  'type': 'object',
  'properties': {
    'error': {
      'type': 'object',
      'required': False,
      'properties': {
        'method': {'type': 'string', 'required': False},
        'id': {'type': 'string', 'required': False},
        'message': {'type': 'string', 'blank': True},
        },
      },
    },
  }


# Prospective user invitation.

PROSPECTIVE_USER_INVITATION = {
  'description': 'format of the prospective user invitation query parameter',
  'type': 'object',
  'properties': {
    'timestamp': {
      'description': 'timestamp at which the invitation was issued',
      'type': 'number'
      },
    'identity': {
      'description': 'identity to which this invitation was made; this may '
      'be different than the actual identity of the bearer, as in cases '
      'where the link was forwarded',
      'type': 'string'
      },
    'viewpoint_id': {
      'description': 'viewpoint to which this invitation grants access; the '
      'bearer should not have access to sensitive data in other viewpoints',
      'type': 'string'
      },
    'service_sig': {
      'description': 'signature with service-wide secret that is used only to '
      'sign invitations; filters out attempted attempted forgeries at a minimal '
      'cost in terms of server resources; the service_sig attribute is not '
      'included in the signature',
      'type': 'string'},
    }
  }


# Subscription metadata.

SUBSCRIPTION_METADATA = {
  'description': 'information about a subscription',
  'type': 'object',
  'properties': {
    'transaction_id': {'type': 'string'},
    'subscription_id': {'type': 'string'},
    'timestamp': {'type': 'number'},
    'expiration_ts': {'type': 'number'},
    'product_type': {'type': 'string'},
    'quantity': {'type': 'number'},
    'payment_type': {'type': 'string'},
    'extra_info': {
      'description': 'additional data about the transaction; format depends on payment_type',
      'type': 'object',
      'required': False,
      'additionalProperties': {},
      },
    },
  }


# Friend metadata.

FRIEND_METADATA = {
  'description': 'information stored for per user about friends of that user; only the user '
  'can view and update this information',
  'type': 'object',
  'required': False,
  'properties': {
    'user_id': {'type': 'number'},
    'nickname': {
      'type': ['string', 'null'],
      'required': False,
      },
    },
  }


# User account settings metadata.

UPDATE_ACCOUNT_SETTINGS_METADATA = {
  'description': 'options and choices affecting the user account that can be updated by the '
  'user; see header comment in the AccountSettings class for details on allowed settings',
  'type': 'object',
  'required': False,
  'properties': {
    'email_alerts': {'type': 'string', 'required': False, 'enum': AccountSettings.ALL_EMAIL_ALERTS},
    'sms_alerts': {'type': 'string', 'required': False, 'enum': AccountSettings.ALL_SMS_ALERTS},
    'push_alerts': {'type': 'string', 'required': False, 'enum': AccountSettings.ALL_PUSH_ALERTS},
    'storage_options': {
      'type': 'array', 'required': False, 'uniqueItems': True,
      'items': {'type': 'string', 'enum': AccountSettings.ALL_STORAGE_OPTIONS},
      },
    },
  }

# At this time, all user account settings can be updated by the user.
ACCOUNT_SETTINGS_METADATA = UPDATE_ACCOUNT_SETTINGS_METADATA

# Identity metadata.
IDENTITY_METADATA = {
  'description': 'Identity metadata. Returned in list_identifies and query_users on self.',
  'type': 'object',
  'properties': {
    'identity': {
      'description': 'e.g. Email:spencer.kimball.gmail.com | Phone:6464174337 | FacebookGraph:62052443',
      'type': 'string',
      },
    'authority': {
      'description': 'e.g. Google | Facebook | Twitter | Viewfinder | <empty>',
      'type': 'string', 'required': False,
      },
    },
  }


# User profile metadata.

UPDATE_USER_PROFILE_METADATA = {
  'description': 'public profile of a user; all properties can be updated by the user',
  'type': 'object',
  'properties': {
    'name': {
      'description': 'full name of the user; if any name part (name, given_name, or family_name) '
      'is given, then all parts are set -- any missing parts are set to None; this helps to '
      'avoid accidental divergence',
      'type': 'string',
      'required': False,
      'dependencies': 'given_name',
      },
    'given_name': {'type': 'string', 'required': False, 'dependencies': 'name'},
    'family_name': {'type': 'string', 'required': False, 'dependencies': 'name'},
    'picture': {
      'description': 'URL to avatar photo',
      'type': 'string',
      'required': False,
      },
    },
  }

USER_PROFILE_METADATA = {
  'description': 'additional user profile properties that cannot be updated by the user, '
  'but are returned by query_users',
  'type': 'object',
  'properties': {
    'email': {'type': 'string', 'required': False},
    'labels': {
      'description': 'set of boolean modifiers affecting the user (e.g. "terminated")',
      'type': 'array', 'required': False, 'items': {'type': 'string'},
      },
    'merged_with': {'type': 'number', 'required': False},
    'private': {
      'description': 'additional fields that are only present when querying for the '
      'authenticated user',
      'type': 'object',
      'required': False,
      'properties': {
        'subscriptions': {
          'description': 'all active subscriptions for this user',
          'type': 'array',
          'items': SUBSCRIPTION_METADATA,
          },
        'account_settings': ACCOUNT_SETTINGS_METADATA,
        'no_password': {
          'description': 'if true, then this user has no password set',
          'type': 'boolean', 'required': False,
          },
        'user_identities': {
          'description': 'all identities for this user',
          'type': 'array',
          'items': IDENTITY_METADATA,
          },
        },
      },
    },
  }
_CopyProperties(target_dict=USER_PROFILE_METADATA, source_dict=UPDATE_USER_PROFILE_METADATA)

# Don't require first name to be set on returned users, even if name is set.
del USER_PROFILE_METADATA['properties']['name']['dependencies']

# Add friend attributes.
_CopyProperties(target_dict=USER_PROFILE_METADATA, source_dict=FRIEND_METADATA)


# Confirmed identity.

CONFIRMED_IDENTITY = {
  'description': 'an identity is confirmed when it is paired with an access token, the '
  'possession of which proves control of the identity',
  'type': 'object',
  'properties': {
    'identity': {
      'description': 'identity to verify; make sure to use the full identity scheme '
      '(e.g. Email:foo@example.com, Phone:+16461234567)',
      'type': 'string',
      },
    'access_token': {
      'description': 'N-digit access code sent to email address or SMS phone number',
      'type': 'string',
      },
    },
  }


##
# AUTH METHODS
##

# User cookies and identity access tokens.
#
# /link/facebook, /link/google, /link/viewfinder
# /login/facebook, /login/google, /login/viewfinder
# /register/facebook, /register/google, /register/viewfinder
# /login_reset/viewfinder
# /merge_token/viewfinder
AUTH_REQUEST = {
  'description': 'registers new users, logs in existing users, or links identities to existing '
  'users; connects to Facebook or Google to gather information about the user, including his/her '
  'contacts',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'use_session_cookie': {
      'description': 'if true, then the user cookie is set to expire when the user ends the '
      'session (e.g. by closing the browser)',
      'type': 'boolean',
      'required': False
      },
    },
  }

AUTH_RESPONSE = {
  'description': 'returns user-id & device-id for registered user',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'user_id': {
      'description': 'id of the user that was registered',
      'type': 'number',
      },
    'device_id': {
      'description': 'id of the device that was registered; this will not be present if the '
      'device section was not present in the original request',
      'type': 'number', 'required': False,
      },
    },
  }

# Used by /<auth>/facebook and /<auth>/google.
AUTH_FB_GOOGLE_REQUEST = deepcopy(AUTH_REQUEST)
AUTH_FB_GOOGLE_REQUEST['properties']['device'] = REGISTER_DEVICE_METADATA

# Used by /<auth>/viewfinder.
AUTH_VIEWFINDER_REQUEST = deepcopy(AUTH_REQUEST)
AUTH_VIEWFINDER_REQUEST['properties']['device'] = REGISTER_DEVICE_METADATA
AUTH_VIEWFINDER_REQUEST['properties']['auth_info'] = {
  'description': 'identity and optional user registration information used in Viewfinder auth '
  'requests; this is in addition to the inherited AUTH_REQUEST fields',
  'type': 'object',
  'properties': {
    'identity': {
      'description': 'identity to authenticate; make sure to use the full identity scheme '
      '(e.g. Email:foo@example.com, Phone:6464174337)',
      'type': 'string'
      },
    },
  }

AUTH_VIEWFINDER_RESPONSE = {
  'description': 'returned by Viewfinder auth methods after an email/SMS message has been sent',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'token_digits': {
      'description': 'number of digits in the access token that was sent',
      'type': 'number',
      },
    },
  }

# Used by /login/viewfinder.
LOGIN_VIEWFINDER_PROPERTIES = {
  'description': 'additional auth_info properties used only in user login',
  'type': 'object',
  'properties': {
    'password': {
      'description': 'user\'s password; if not specified then an authentication email/SMS '
      'will be sent; otherwise, the password will be validated and allow the auth process to '
      'be short-circuited',
      'type': 'string',
      'required': False,
      },
    },
  }

LOGIN_VIEWFINDER_REQUEST = deepcopy(AUTH_VIEWFINDER_REQUEST)
_CopyProperties(target_dict=LOGIN_VIEWFINDER_REQUEST['properties']['auth_info'],
                source_dict=LOGIN_VIEWFINDER_PROPERTIES)

# Used by /merge_token/viewfinder
MERGE_TOKEN_REQUEST = {
  'description': 'sends an access token which proves control of an identity, and which will '
  'be passed to /service/merge_accounts',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'identity': {
      'description': 'e.g. Email:spencer.kimball.gmail.com | Phone:+16464174337',
      'type': 'string',
      },
    'error_if_linked': {
      'description': 'reports ALREADY_LINKED error if the identity is already linked to a user '
      'account',
      'type': 'boolean', 'required': False,
      },
    },
  }

# Used by /register/viewfinder.
REGISTER_VIEWFINDER_PROPERTIES = {
  'description': 'additional auth_info properties used only in user registration',
  'type': 'object',
  'properties': {
    'password': {
      'description': 'user password to set as part of registration',
      'type': 'string',
      'required': False,
      },
    'name': {
      'description': 'full user name',
      'type': 'string',
      'dependencies': 'given_name',
      },
    'given_name': {
      'description': 'user\'s given name (i.e. first name)',
      'type': 'string',
      'required': False,
      'dependencies': 'name',
      },
    'family_name': {
      'description': 'user\'s family name (i.e. last name)',
      'type': 'string',
      'required': False,
      'dependencies': 'name',
      },
    },
  }

REGISTER_VIEWFINDER_REQUEST = deepcopy(AUTH_VIEWFINDER_REQUEST)
_CopyProperties(target_dict=REGISTER_VIEWFINDER_REQUEST['properties']['auth_info'],
                source_dict=REGISTER_VIEWFINDER_PROPERTIES)

# Used by /verify/viewfinder.
# Verifies access code and complete the auth operation that was started by a call to /<auth>/viewfinder.
VERIFY_VIEWFINDER_REQUEST = deepcopy(AUTH_REQUEST)
_CopyProperties(target_dict=VERIFY_VIEWFINDER_REQUEST, source_dict=CONFIRMED_IDENTITY)

# Used by auth.html.
CONFIRM_PASSWORD_REQUEST = {
  'description': 'confirm user password before completing user registration',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'password': {
      'description': 'password which will be checked against the password that was supplied '
      'during user registration',
      'type': 'string',
      },
    },
  }

CONFIRM_PASSWORD_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


##
# SERVICE METHODS
##


# Add followers to an existing viewpoint.
#
# /service/add_followers

ADD_FOLLOWERS_REQUEST = {
  'description': 'add resolved contacts as followers of an existing '
  'viewpoint',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    'viewpoint_id': {
      'description': 'id of the viewpoint to which to add followers',
      'type': 'string',
      },
    'contacts': FOLLOWER_CONTACTS_METADATA,
    },
  }

ADD_FOLLOWERS_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Allocate unique ids for use when uploading photos or creating episodes.
#
# /service/allocate_ids

ALLOCATE_IDS_REQUEST = {
  'description': 'allocate unique ids for the requesting user',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'asset_types': {
      'description': 'An array of single-character prefixes describing the asset ids to be '
      'generated.  Assets can be of mixed type - for example, you may request an operation '
      'id and an activity id in a single request by passing the array ["o", "a"].',
      'type': 'array',
      'items': {
        'type' : 'string'
        }
      },
    },
  }

ALLOCATE_IDS_RESPONSE = {
  'description': 'returns the first id in a pre-allocated block',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'asset_ids': {
      'description': 'An array of asset ids generated by the server.  Ids are returned in the '
      'same order they appeared in the "asset_types" array of the request.',
      'type': 'array',
      'items': {
        'type' : 'string'
        }
      },
    'timestamp': {
      'description': 'The timestamp used by the server to generate ids which require a '
      'timestamp component.',
      'type': 'number',
      }
    },
  }


# Build archive of a users photos/conversations/etc.
#
# /service/build_archive

BUILD_ARCHIVE_REQUEST = {
  'description': 'build archive of photos/comments/etc for requesting user',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'email': {'type': 'string'},
    },
  }

BUILD_ARCHIVE_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Fetch calendar(s) for user.
#
# /service/get_calendar

GET_CALENDAR_REQUEST = {
  'description': 'fetch calendar(s) for user; specify "holidays" for locale-specific '
  'holidays. Will default to en_US if the user\'s locale is not known.',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'calendars': {
      'description': 'calendar IDs; specify none for locale-specific holiday calendar',
      'type': 'array',
      'items': {
        'type': 'object',
        'properties': {
          'calendar_id': {'type': 'string'},
          'year': {'type': 'number'},
          },
        },
      },
    },
  }

GET_CALENDAR_RESPONSE = {
  'description': 'returns a list of events corresponding to each calendar and year',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'calendars': {
      'type': 'array',
      'items': {
        'type': 'object',
        'properties': {
          'calendar_id': {'type': 'string'},
          'year': {'type': 'number'},
          'events': {
            'description': 'calendar events by name and date; dates are unix time in UTC',
            'type': 'array',
            'items': {
              'type': 'object',
              'properties': {
                'name': {'type': 'string'},
                'dtstart': {'type': 'number'},
                'dtend': {'type': 'number'},
                },
              },
            },
          },
        },
      },
    },
  }


# Hide photos from user's personal library and inbox view.
#
# /service/hide_photos

HIDE_PHOTOS_REQUEST = {
  'description': 'hide a list of posts by id from the user\'s personal '
  'library or inbox view by labeling them as hidden for that user',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'episodes': {
      'type': 'array',
      'items': {
        'type': 'object',
        'properties': {
          'episode_id': {
            'description': 'id of the episode containing photos to hide '
            'from the user\'s personal library or inbox view',
            'type': 'string',
            },
          'photo_ids': {
            'description': 'ids of photos to hide from the user\'s '
            'personal library or inbox view',
            'type': 'array',
            'items': {'type': 'string'},
            },
          },
        },
      },
    },
  }

HIDE_PHOTOS_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# List all user identities.
#
# /service/list_identities
LIST_IDENTITIES_REQUEST = {
  'description': 'list all identities linked to this account',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }

LIST_IDENTITIES_RESPONSE = {
  'description': 'the list of all linked identities',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'identities': {
      'type': 'array',
      'items': IDENTITY_METADATA
      },
    },
  }


# Merge one user account with another.
#
# /service/merge_accounts

MERGE_ACCOUNTS_REQUEST = {
  'description': 'merge assets from another user account or identity into the account of the '
  'current user',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    'source_user_cookie': {
      'description': 'user cookie for the account from which to merge; this cookie can be '
      'obtained by calling the /login handler and getting the contents of the "user" HTTP '
      'cookie that it returns; this cookie must be confirmed, meaning that it cannot have '
      'been created from a password; either this field or the source_identity need to be '
      'specified',
      'type': 'string', 'required': False,
      },
    'source_identity': {
      'description': 'confirmed identity linked to the account from which to merge; it is also '
      'possible for the identity to not be linked to any account, in which case it is simply '
      'linked to the target account',
      'type': 'object', 'required': False,
      'properties': { },
      },
    },
  }
_CopyProperties(target_dict=MERGE_ACCOUNTS_REQUEST['properties']['source_identity'],
                source_dict=CONFIRMED_IDENTITY)

MERGE_ACCOUNTS_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Get a new client log upload URL from the server.
#
# /service/new_client_log_url

NEW_CLIENT_LOG_URL_REQUEST = {
  'description': 'fetches an S3 PUT url for writing client device log '
  'to server for debugging',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'timestamp': {'type': 'number'},
    'client_log_id': {
      'description': 'an arbitrary client identifier for the log; must be '
      'unique across all calls made by the device',
      'type': 'string'
      },
    'content_type': {
      'description': 'optionally specify an alternate content-type for the '
      'client log. By default, uses application/octet to support old clients '
      'which incorrectly specify this. TODO(spencer): change default to octet-stream.',
      'type': 'string',
      'required': False
      },
    'content_md5': {'type': 'string', 'required': False},
    'num_bytes': {'type': 'number', 'required': False},
    },
  }

NEW_CLIENT_LOG_URL_RESPONSE = {
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'client_log_put_url': {
      'description': 'pre-authorized url for client log; '
      'URL expires in 24 hours',
      'type': 'string'
      },
    },
  }


# Ping request. Unauthenticated request, periodically issued by the client.
# The response may contain an informative message (eg: new version available).
# Since the request does not require the user to be signed in, it is not handled by service.py
#
# /ping

PING_REQUEST = {
  'description': 'device ping',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'device': DEVICE_METADATA,
    },
  }

PING_RESPONSE = {
  'description': 'ping response',
  'type': 'object',
  'properties': {
    'message': PING_RESPONSE_MESSAGE,
    },
  }


# Add new comment to the viewpoint.
#
# /service/post_comment

POST_COMMENT_REQUEST = {
  'description': 'adds a new comment to the viewpoint, optionally attached to '
  'another asset in the same viewpoint (such as a photo or another comment)',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    },
  }
_CopyProperties(target_dict=POST_COMMENT_REQUEST, source_dict=POST_COMMENT_METADATA)

POST_COMMENT_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    }
  }


# Fetch user contact list.
#
# /service/query_contacts

QUERY_CONTACTS_REQUEST = {
  'description': 'fetch (name, identity, rank) contact tuples; specify '
  'start_key to begin querying where a previous invocation left off',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'start_key': {'type': 'string', 'required': False},
    'limit': {'type': 'number', 'required': False},
    },
  }

QUERY_CONTACTS_RESPONSE = {
  'description': 'returns a list of contacts',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'num_contacts': {'type': 'number'},
    'contacts': {
      'type': 'array',
      'items': QUERY_CONTACT_METADATA,
      },
    'last_key': {
      'description': 'the last fetched contact sort key; supply this value to '
      'the next invocation of query_contacts to continue scan',
      'type': 'string', 'required': False,
      },
    }
  }


# Query photos (posts) from episodes.
#
# /service/query_episodes

QUERY_EPISODES_REQUEST = {
  'description': 'query photo metadata and associated post information '
  'from specified episodes',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'episodes': EPISODE_SELECTION,
    'photo_limit': {
      'description': 'maximum number of photos to query from each episode id',
      'type': 'number', 'required': False,
      },
    },
  }

QUERY_EPISODES_RESPONSE = {
  'description': 'a list of photos from each requested episode. The photo metadata '
  'is augmented by associated post information',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'episodes': {
      'description': 'episode query responses',
      'type': 'array',
      'items': {
        'type': 'object',
        'properties': {
          'photos': {
            'description': 'post + photo metadata',
            'type': 'array', 'required': False,
            'items': POST_PHOTO_METADATA,
            },
          'last_key': {
            'description': 'the last-processed photo in the episode; supply with'
            'next invocation of QUERY_EPISODES to continue',
            'type': 'string', 'required': False,
            },
          },
        },
      },
    },
  }
_CopyProperties(target_dict=QUERY_EPISODES_RESPONSE['properties']['episodes']['items'],
                source_dict=EPISODE_METADATA)

# If get_attributes=False, only episode_id will be returned; all other properties are optional.
_MakeOptional(QUERY_EPISODES_RESPONSE['properties']['episodes']['items']['properties'],
              lambda key: key != 'episode_id')


# Query for viewpoints that are followed by the calling user.
#
# /service/query_followed

QUERY_FOLLOWED_REQUEST = {
  'description': 'query metadata of viewpoints that are followed by calling user',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'start_key': {'type': 'string', 'required': False},
    'limit': {'type': 'number', 'required': False},
    },
  }

QUERY_FOLLOWED_RESPONSE = {
  'description': 'a list of viewpoints that are followed by calling user',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'viewpoints': {
      'description': 'viewpoint query responses',
      'type': 'array',
      'items': VIEWPOINT_METADATA,
      },
    'last_key': {
      'description': 'the last-processed followed viewpoint; supply with '
      'next invocation of QUERY_FOLLOWED to continue; values can be sorted '
      'lexicographically',
      'type': 'string', 'required': False,
      },
    },
  }


# Query notifications.
#
# /service/query_notifications

QUERY_NOTIFICATIONS_REQUEST = {
  'description': 'poll list of pending notifications for the user',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'scan_forward': {
      'description': 'if true or not specified, then notifications are '
      'queried in forward (ascending) order; otherwise, they are queried '
      'in reverse order',
      'type': 'boolean', 'required': False,
      },
    'start_key': {
      'description': 'clients should supply the last_key returned with the '
      'response to a prior invocation to get subsequent notifications',
      'type': 'string', 'required': False,
      },
    'limit': {
      'description': 'maximum number of notifications to return',
      'type': 'number', 'required': False,
      },
    'max_long_poll': {
      'description': 'maximum duration for long-polling requests (in seconds)',
      'type': 'number', 'required': False,
      },
    },
  }

QUERY_NOTIFICATIONS_RESPONSE = {
  'description': 'Notifications pending for user',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'notifications': {
      'description': 'an array of notification objects',
      'type': 'array',
      'items': NOTIFICATION,
      },
    'last_key': {
      'description': 'the last-processed notification key; supply this value with the '
      'next invocation of QUERY_NOTIFICATIONS to continue; if not supplied, no '
      'notifications were available; values can be sorted lexicographically',
      'type': 'string', 'required': False
      },
    'retry_after': {
      'description': 'advisory request from the server to wait before issuing another background query_notifications.',
      'type': 'number', 'required': False
      },
    },
  }


# Query user metadata by user id.
#
# /service/query_users

QUERY_USERS_REQUEST = {
  'description': 'query user metadata by user ids; only users which consider the caller a '
  'friend will provide profile info; in this case, a "friend" label is returned',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'user_ids': USER_SELECTION,
    },
  }

QUERY_USERS_RESPONSE = {
  'description': 'user metadata for each valid, supplied user id; if a user id was supplied '
  'with the request, but not returned with the response, then the user does not exist',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'users': {
      'type': 'array',
      'items': USER_PROFILE_METADATA,
      },
    },
  }


# Query episodes in specified viewpoints.
#
# /service/query_viewpoints

QUERY_VIEWPOINTS_REQUEST = {
  'description': 'query viewpoint and episode metadata from specified '
  'viewpoints',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'viewpoints': VIEWPOINT_SELECTION,
    'limit': {
      'description': 'maximum number of items to return in each episode, '
      'follower, or activity collection in the response',
      'type': 'number', 'required': False,
      },
    },
  }

QUERY_VIEWPOINTS_RESPONSE = {
  'description': 'a list of episodes from each requested viewpoint. The episode metadata '
  'is augmented by associated member information',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'viewpoints': {
      'description': 'viewpoint query responses',
      'type': 'array',
      'items': {
        'type': 'object',
        'properties': {
          'followers': {
            'description': 'ids of users following the viewpoint',
            'type': 'array', 'required': False,
            'items': FRIEND_FOLLOWER_METADATA,
            },
          'follower_last_key': {
            'description': 'the last-processed follower in the viewpoint; supply with'
            'next invocation of QUERY_VIEWPOINTS to continue',
            'type': 'string', 'required': False,
            },
          'activities': {
            'description': 'viewpoint activity metadata',
            'type': 'array', 'required': False,
            'items': ACTIVITY_METADATA,
            },
          'activity_last_key': {
            'description': 'the last-processed activity in the viewpoint; supply with'
            'next invocation of QUERY_VIEWPOINTS to continue',
            'type': 'string', 'required': False,
            },
          'episodes': {
            'description': 'episode + member metadata',
            'type': 'array', 'required': False,
            'items': EPISODE_METADATA,
            },
          'episode_last_key': {
            'description': 'the last-processed episode in the viewpoint; supply with'
            'next invocation of QUERY_VIEWPOINTS to continue',
            'type': 'string', 'required': False,
            },
          'comments': {
            'description': 'comment + member metadata',
            'type': 'array', 'required': False,
            'items': COMMENT_METADATA,
            },
          'comment_last_key': {
            'description': 'the last-processed comment in the viewpoint; supply with'
            'next invocation of QUERY_VIEWPOINTS to continue',
            'type': 'string', 'required': False,
            },
          },
        },
      },
    },
  }
_CopyProperties(target_dict=QUERY_VIEWPOINTS_RESPONSE['properties']['viewpoints']['items'],
                source_dict=VIEWPOINT_METADATA)

# If get_attributes=False, only viewpoint_id will be returned; all other properties are optional.
_MakeOptional(QUERY_VIEWPOINTS_RESPONSE['properties']['viewpoints']['items']['properties'],
              lambda key: key != 'viewpoint_id')


# Records an external (iTunes in-app purchase) subscription.
#
# /service/record_subscription

RECORD_SUBSCRIPTION_REQUEST = {
  'description': 'records an external subscription',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'receipt_data': {
      'description': 'base64-encoded itunes receipt data',
      'type': 'string',
      },
    },
  }

RECORD_SUBSCRIPTION_RESPONSE = {
  'description': 'returns information from the decoded subscription',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'subscription': SUBSCRIPTION_METADATA,
    },
  }


# Remove contacts from user contacts list.
#
# /service/remove_contacts

REMOVE_CONTACTS_REQUEST = {
  'description': '',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'contacts': {
      'description': 'list of contact_ids.  Only AddressBook and Manual contact_ids may be removed though this API',
      'type': 'array',
      'uniqueItems': True,
      'items': {'type': 'string'},
      },
    },
  }

REMOVE_CONTACTS_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Remove followers from an existing viewpoint.
#
# /service/remove_followers

REMOVE_FOLLOWERS_REQUEST = {
  'description': 'remove followers from an existing viewpoint',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    'viewpoint_id': {
      'description': 'id of the viewpoint from which to remove followers',
      'type': 'string',
      },
    'remove_ids': {
      'description': 'ids of followers to remove from the viewpoint; if a follower does not '
      'not exist on the viewpoint, it is ignored',
      'type': 'array',
      'items': {'type': 'integer'},
      },
    },
  }

REMOVE_FOLLOWERS_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Remove photos from user's personal collection.
#
# /service/remove_photos

REMOVE_PHOTOS_REQUEST = {
  'description': 'remove a list of photos by id from the user\'s personal '
  'collection by labeling them as removed for that user',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'episodes': {
      'type': 'array',
      'items': {
        'type': 'object',
        'properties': {
          'episode_id': {
            'description': 'id of the episode containing photos to remove '
            'from the user\'s personal collection',
            'type': 'string',
            },
          'photo_ids': {
            'description': 'ids of photos to remove from the user\'s '
            'personal collection',
            'type': 'array',
            'items': {'type': 'string'},
            },
          },
        },
      },
    },
  }

REMOVE_PHOTOS_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Remove viewpoint from a user's inbox.
#
# /service/remove_viewpoint

REMOVE_VIEWPOINT_REQUEST = {
  'description': 'remove viewpoint from a user\'s inbox',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'viewpoint_id': {
      'description': 'id of viewpoint to be removed from user\'s inbox',
      'type': 'string',
      },
    },
  }

REMOVE_VIEWPOINT_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Resolve contacts to fill in missing information.
#
# Currently only supports retrieving metadata for email-based identities.
# Used to enable sharing to email addresses that are not currently in
# the user's contacts.
#
# /service/resolve_contacts

RESOLVE_CONTACTS_REQUEST = {
  'description': 'resolve identities to contact metadata; each input identity '
  'should begin with "Email:"',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'identities': {
      'type': 'array',
      'items': {'type': 'string'},
      },
    },
  }

RESOLVE_CONTACTS_RESPONSE = {
  'description': 'returns a list of resolved contacts in the same order as the '
  'request; if the identity matched an existing user the user_id will be '
  'filled in',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'contacts': {
      'description': ('list of resolved contacts, in the same '
                      'order as the request.  Email identities for existing '
                      'users will have user_id and other fields filled in; '
                      'otherwise only the identity field will be present.'),
      'type': 'array',
      'items': {
        'description': 'partial user metadata',
        'type': 'object',
        'properties': {
          'user_id': {'type': 'number', 'required': False},
          'identity': {'type': 'string', 'required': False},
          'name': {'type': 'string', 'required': False},
          'given_name': {'type': 'string', 'required': False},
          'family_name': {'type': 'string', 'required': False},
          'labels': {
            'description': ('set of boolean modifiers affecting the user (e.g. "registered"). '
                            'The "friend" label and any data requiring friend status will not be '
                            'returned by this method.'),
            'type': 'array', 'required': False, 'items': {'type': 'string'},
          },
        }
      }
    },
  }
}


# Save photos to default viewpoint.
#
# /service/save_photos

SAVE_PHOTOS_REQUEST = {
  'description': 'save photos from existing episodes to new episodes in the current user\'s '
  'default viewpoint',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    'viewpoint_ids': {
      'description': 'server saves all episodes contained within these viewpoints; this is '
      'in addition to episodes that may be given in the "episodes" field; if an episode is '
      'specified in the "episodes" field, it is assumed to be complete and is skipped when '
      'scanning the viewpoint',
      'type': 'array',
      'required': False,
      'items': {'type': 'string'},
      },
    'episodes': OPTIONAL_COPY_EPISODES_METADATA,
    },
  }

SAVE_PHOTOS_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Share photos with the followers of an existing viewpoint.
#
# /service/share_existing

SHARE_EXISTING_REQUEST = {
  'description': 'share episodes with the followers of an existing '
  'viewpoint',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    'viewpoint_id': {'type': 'string'},
    'episodes': COPY_EPISODES_METADATA,
    },
  }

SHARE_EXISTING_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Share photos with contacts in a new viewpoint.
#
# /service/share_new

SHARE_NEW_REQUEST = {
  'description': 'share photos from existing episodes into a new viewpoint, '
  'with the resolved contacts as followers',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    'viewpoint': CREATE_VIEWPOINT_METADATA,
    'episodes': COPY_EPISODES_METADATA,
    'contacts': FOLLOWER_CONTACTS_METADATA,
    },
  }

SHARE_NEW_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Terminate user account.
#
# /service/terminate_account

TERMINATE_ACCOUNT_REQUEST = {
  'description': 'terminate a user account -- unlink all identities, mute '
  'all alerts, disable all sharing',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    },
  }

TERMINATE_ACCOUNT_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Unlink existing identity.
#
# /sevice/unlink_identity
UNLINK_IDENTITY_REQUEST = {
  'description': 'unlink an identity from an account; succeeds if the specified identity '
  'is in fact linked and if it is not the last identity authenticated via trusted authority',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'identity': {
      'description': 'e.g. Email:spencer.kimball.gmail.com | Phone:6464174337 | FacebookGraph:62052443',
      'type': 'string',
      },
    },
  }

UNLINK_IDENTITY_RESPONSE = {
  'description': 'empty response; on error, the standard error response',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Unshare photos from a viewpoint.
#
# /service/unshare

UNSHARE_REQUEST = {
  'description': 'unshares photos from episodes in a viewpoint; also '
  'recursively unshares from all derived episodes',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    'viewpoint_id': {'type': 'string'},
    'episodes': {
      'type': 'array',
      'items': {
        'type': 'object',
        'properties': {
          'episode_id': {
            'description': 'id of the episode containing photos to unshare '
            'from the viewpoint',
            'type': 'string',
            },
          'photo_ids': {
            'description': 'ids of photos to unshare from the viewpoint',
            'type': 'array',
            'items': {'type': 'string'},
            },
          },
        },
      },
    },
  }

UNSHARE_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Updates device information including last_access time. Returns a
# newly encrypted user cookie. This is necessary in the case of a
# device that isn't used often and has failed multiple push
# notifications and had its push_token reset.
#
# /service/update_device

UPDATE_DEVICE_REQUEST = {
  'description': 'update device information',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'device_dict': DEVICE_METADATA,
    },
  }

UPDATE_DEVICE_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    }
  }

# Per-user photo metadata update.
#
# /service/update_user_photo

UPDATE_USER_PHOTO_REQUEST = {
  'description': 'updates the per-user metadata of an existing photo',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    },
  }
_CopyProperties(target_dict=UPDATE_USER_PHOTO_REQUEST, source_dict=USER_PHOTO_METADATA)

UPDATE_USER_PHOTO_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Episode metadata update.
#
# /service/update_episode

UPDATE_EPISODE_REQUEST = {
  'description': 'updates the metadata of an existing episode',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    },
  }
_CopyProperties(target_dict=UPDATE_EPISODE_REQUEST, source_dict=UPDATE_EPISODE_METADATA)

UPDATE_EPISODE_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Follower metadata update.
#
# /service/update_follower

UPDATE_FOLLOWER_REQUEST = {
  'description': 'updates the metadata of an existing follower',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'follower': UPDATE_FOLLOWER_METADATA,
    },
  }

UPDATE_FOLLOWER_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Friend metadata update.
#
# /service/update_friend

UPDATE_FRIEND_REQUEST = {
  'description': 'updates the metadata of a friend; updates only affect the view of the calling '
  'user',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'friend': FRIEND_METADATA,
    },
  }

UPDATE_FRIEND_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Photo metadata update.
#
# /service/update_photo

UPDATE_PHOTO_REQUEST = {
  'description': 'updates the metadata of an existing photo',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    },
  }
_CopyProperties(target_dict=UPDATE_PHOTO_REQUEST, source_dict=UPDATE_PHOTO_METADATA)

UPDATE_PHOTO_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# User metadata update.
#
# /service/update_user

UPDATE_USER_REQUEST = {
  'description': 'updates the metadata of an existing user',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'account_settings': UPDATE_ACCOUNT_SETTINGS_METADATA,
    'password': {
      'description': 'new user password; this field can only be set if using a recently '
      'confirmed user cookie, or if the old_password matches, or if no user password has '
      'yet been set',
      'type': 'string', 'required': False,
      },
    'old_password': {
      'description': 'if this matches the old password, then a new password can be set '
      'without needing a confirmed user cookie',
      'type': 'string', 'required': False,
      },
    },
  }
_CopyProperties(target_dict=UPDATE_USER_REQUEST, source_dict=UPDATE_USER_PROFILE_METADATA)

UPDATE_USER_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Viewpoint metadata update.
#
# /service/update_viewpoint

UPDATE_VIEWPOINT_REQUEST = {
  'description': 'updates the metadata of an existing viewpoint',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    },
  }
_CopyProperties(target_dict=UPDATE_VIEWPOINT_REQUEST, source_dict=UPDATE_VIEWPOINT_METADATA)
_CopyProperties(target_dict=UPDATE_VIEWPOINT_REQUEST, source_dict=UPDATE_FOLLOWER_METADATA)

UPDATE_VIEWPOINT_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    },
  }


# Upload user contact list.
#
# /service/upload_contacts

UPLOAD_CONTACTS_REQUEST = {
  'description': 'upload contact tuples',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'contacts': {
      'type': 'array',
      'maxItems': 50,
      'items': UPLOAD_CONTACT_METADATA,
      },
    },
  }

UPLOAD_CONTACTS_RESPONSE = {
  'description': 'returns nothing',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'contact_ids': {
      'description': 'list of server computed contact_ids generated from '
                     'the list of contacts in the upload_contacts request',
      'type': 'array',
      'items': {'type': 'string'},
      },
    }
  }


# Upload photo and episode metadata to service.
#
# /service/upload_episode

UPLOAD_EPISODE_REQUEST = {
  'description': 'episode id, optional metadata, and list of photos',
  'type': 'object',
  'properties': {
    'headers': OP_HEADERS,
    'activity': CREATE_ACTIVITY_METADATA,
    'episode': UPLOAD_EPISODE_METADATA,
    'photos': {
      'description': 'list of photos in episode',
      'type': 'array',
      'items': UPLOAD_PHOTO_METADATA,
      },
    },
  }

UPLOAD_EPISODE_RESPONSE = {
  'description': 'returns episode id and list of photo ids, one per metadata upload',
  'type': 'object',
  'properties': {
    'headers': HEADERS,
    'photos': {
      'description': 'photo info for each metadata upload',
      'type': 'array',
      'items': {
        'type': 'object',
        'properties': {
          'photo_id': {
            'description': 'server-assigned base64hex-encoded photo id',
            'type': 'string'
            },
          'orig_put_url': {
            'description': 'pre-authorized url for original resolution image file upload; '
            'URL expires in 24 hours',
            'type': 'string'
            },
          'full_put_url': {
            'description': 'pre-authorized url for full-screen resolution image file upload; '
            'URL expires in 24 hours',
            'type': 'string'
            },
          'med_put_url': {
            'description': 'pre-authorized url for medium resolution image file upload; '
            'URL expires in 24 hours',
            'type': 'string'
            },
          'tn_put_url': {
            'description': 'pre-authorized url for thumbnail resolution image file upload; '
            'URL expires in 24 hours',
            'type': 'string'
            },
          },
        },
      },
    },
  }
