def create_tv_config_map(device_ids, async_kvstore_client, request_context):
    """
    Function to create tv config map for initializing TVBookmark class
    :param device_ids: device ids to fetch tv configs for
    :param request_context: Used to authenticate kvstore requests
    :param async_kvstore_client: kvstore client used to make kvstore requests
    """
    # When we are setting a tv bookmark, we're only given device ids
    tv_config_map = {}
    tv_configs = yield get_drone_mode_tvs(request_context,
                                          async_kvstore_client,
                                          device_ids=device_ids)

    LOGGER.debug('device_ids in create_tv_config_map=%s', device_ids)
    LOGGER.debug('tv_configs in create_tv_config_map=%s', tv_configs)
    for tv_config in tv_configs:
        device_id = b64encode_to_str(base64.urlsafe_b64decode(str(tv_config.get(constants.KEY))))
        LOGGER.debug('tv_config for device_id=%s is: %s', device_id, tv_config)
        # Don't save captain id and captain url.  bookmark should be agnostic to this concept
        # only upon activation should we care about the captain stuff. This way, when we activate
        # a config in a grid configuration, it starts off with no captain data, and their for a captain
        # is immediately elected.
        if not all(key in tv_config for key in TVConfig.required_kvstore_keys()):
            raise SpacebridgeApiRequestError('No config data exists for device_id={}'
                                             .format(device_id),
                                             status_code=http.NOT_FOUND)
        tv_config[constants.CAPTAIN_ID] = ''
        tv_config[constants.CAPTAIN_URL] = ''
        tv_config_object = TVConfig(**tv_config)
        tv_config_map[device_id] = json.loads(tv_config_object.to_json())

    defer.returnValue(tv_config_map)
def process_tv_captain_url_request(request_context,
                                   client_single_request,
                                   single_server_response,
                                   async_client_factory):
    """
    This method processes captain url requests from drone mode TVs.
    It is called when picking a captain when a grid request is activated, and
    reelecting a captain when the captain dies.

    :param request_context: Used to authenticate kvstore requests
    :param client_single_request: client request object protobuf
    :param single_server_response: server response object protobuf
    :param async_client factory: factory class used to generate kvstore and spacebridge clients
    """
    captain_url_request = client_single_request.tvCaptainUrlRequest
    update_flag = captain_url_request.updateFlag
    captain_id = client_single_request.tvCaptainUrlRequest.captainId
    async_kvstore_client = async_client_factory.kvstore_client()

    # fetch tv config for captain id
    tvs = yield get_drone_mode_tvs(request_context,
                                   async_kvstore_client,
                                   device_ids=[captain_id])
    if not tvs:
        raise SpacebridgeApiRequestError('Invalid captain id provided. captain_id={}'.format(captain_id),
                                         status_code=http.BAD_REQUEST)
    tv = tvs[0]

    LOGGER.debug('tvs in captain url request = %s', tvs)
    tv_config = TVConfig(**tv)

    # if we're updating the captain url, or if we're trying to be elected
    # captain and no one is yet, we do the same thing
    if not (tv_config.captain_url or tv_config.captain_id) or update_flag:
        device_ids = tv_config.tv_grid['device_ids']
        captain_url = captain_url_request.captainUrl
        timestamp = get_current_timestamp_str()
        yield update_grid_members(request_context,
                                  async_kvstore_client,
                                  device_ids,
                                  captain_id,
                                  captain_url,
                                  timestamp)
        yield process_subscriptions(request_context,
                                    async_client_factory,
                                    tv_device_ids=device_ids)

    else:
        LOGGER.debug('captain url already set for device_id=%s, doing nothing', tv_config.device_id)
    single_server_response.tvCaptainUrlResponse.SetInParent()
def fetch_active_tv_configs(user_subscription_map, async_kvstore_client,
                            request_context):
    """
    Fetch active tv configs based on
    subscription data
    :param user_subscription_map: Map from user to list of subscriptions
    :param async_kvstore_client: kvstore client used to make requests
    :param request_context: request context used for kvstore client
    @returns: tv_config_json_list
    """
    # fetch tv config data from drone mode tvs collection
    kvstore_requests = []
    LOGGER.debug('user_subscription_map=%s', user_subscription_map)
    for user, subscription_array in user_subscription_map.items():
        device_ids = [
            subscription.device_id for subscription in subscription_array
        ]
        LOGGER.debug("device_ids in subscription array for user=%s: =%s", user,
                     device_ids)

        kvstore_request = get_drone_mode_tvs(request_context,
                                             async_kvstore_client,
                                             device_ids=device_ids,
                                             user=user)
        kvstore_requests.append(kvstore_request)

    tv_config_json_list = []
    exceptions = []
    responses = yield defer.DeferredList(kvstore_requests, consumeErrors=True)
    for response in responses:
        if isinstance(response[1], Failure):
            exceptions.append(response[1])
        else:
            (_, tv_config_json) = response
            LOGGER.debug('response in fetch active config=%s', response)
            tv_config_json_list.extend(tv_config_json)
    LOGGER.debug(
        'Finished fetching tv configs for subscription updates, '
        'tv_config_json_list=%s', tv_config_json_list)
    if exceptions:
        LOGGER.error(
            'Encountered exceptions fetching drone mode tv data: e=%s',
            exceptions)
    defer.returnValue(tv_config_json_list)
def update_grid_members(request_context,
                        async_kvstore_client,
                        device_ids,
                        captain_id,
                        captain_url,
                        timestamp,
                        user=None):
    """
    Function to update the grid members
    with the correct captain id, captain url, and device
    ids data

    :param request_context: used to make kvstore request
    :param async_kvstore_client: client used to make kvstore request
    @device_ids: device ids to update
    @captain_id: captain id
    @captain_url: captain url
    """
    if not user:
        user = request_context.current_user
    tvs = yield get_drone_mode_tvs(request_context,
                                   async_kvstore_client,
                                   device_ids=device_ids,
                                   user=user)
    for tv in tvs:
        # if grid was deleted or if this somehow doesn't exist, don't do anything.
        if constants.TV_GRID in tv:
            tv[constants.TV_GRID][constants.DEVICE_IDS] = device_ids
            tv[constants.CAPTAIN_URL] = captain_url
            tv[constants.CAPTAIN_ID] = captain_id
            tv[constants.TIMESTAMP] = timestamp
    written_ids = yield async_kvstore_client.async_batch_save_request(
        request_context.system_auth_header,
        constants.DRONE_MODE_TVS_COLLECTION_NAME,
        tvs,
        owner=user)
    LOGGER.debug("updated tvs:%s", written_ids)
    defer.returnValue(written_ids)
def fetch_valid_tvs(request_context,
                    async_kvstore_client,
                    user=None,
                    tv_ids=None):
    """
    Function to fetch valid tvs for drone mode
    :param request_context: request context used to make kvstore requests
    :param async_kvstore_client: async client used to make kvstore requests
    :param device_id: optional device id to filter on
    """
    # fetch list of all registered tvs
    valid_tv_protos = []
    valid_tv_json = []
    if not user:
        user = request_context.current_user
    tv_list = yield get_registered_tvs(request_context.system_auth_header,
                                       user,
                                       async_kvstore_client,
                                       device_ids=tv_ids)
    # fetch devices from drone_mode_tvs collection
    # if there is data, return that, otherwise just return blank TV data
    # construct set containing device ids
    drone_mode_tvs = yield get_drone_mode_tvs(request_context,
                                              async_kvstore_client,
                                              user=user)
    LOGGER.debug('registered_tvs=%s, drone mode tvs=%s', tv_list,
                 drone_mode_tvs)
    drone_mode_tv_dict = {}
    for element in drone_mode_tvs:
        if constants.CONTENT in element:
            raw_id = base64.urlsafe_b64decode(str(element[constants.KEY]))
            encoded_id = b64encode_to_str(raw_id)
            element[constants.DEVICE_ID] = encoded_id
            drone_mode_tv_dict[encoded_id] = element

    if tv_list:
        active_subscriptions = yield fetch_subscriptions(
            request_context.auth_header,
            async_kvstore_client,
            user_list=[user],
            subscription_type=constants.DRONE_MODE_TV,
            device_ids=tv_ids)

        LOGGER.debug('active_subscriptions=%s', active_subscriptions)

        active_subscription_ids = {
            subscription.device_id
            for subscription in active_subscriptions
        }
        for device in tv_list:
            device_id = device[constants.DEVICE_ID]
            tv_proto = drone_mode_pb2.TVData()
            tv_proto.device_id = device_id
            tv_proto.display_name = device['device_name']
            tv_proto.is_active = device_id in active_subscription_ids
            tv_proto.tv_config.SetInParent()
            if device_id in drone_mode_tv_dict:
                tv_config = TVConfig(**drone_mode_tv_dict[device_id])
                tv_config.set_protobuf(tv_proto.tv_config)
            valid_tv_protos.append(tv_proto)
            json_obj = MessageToDict(tv_proto,
                                     including_default_value_fields=True,
                                     use_integers_for_enums=True,
                                     preserving_proto_field_name=True)
            # since we're storing the user choices and input tokens as blobs,
            # when we deserialize from proto to dict, we need to json dumps each of the fields
            json_obj[constants.TV_CONFIG][constants.INPUT_TOKENS] = json.dumps(
                json_obj.get(constants.TV_CONFIG,
                             {}).get(constants.INPUT_TOKENS, {}))
            json_obj[constants.TV_CONFIG][constants.USER_CHOICES] = json.dumps(
                json_obj.get(constants.TV_CONFIG,
                             {}).get(constants.USER_CHOICES, {}))

            valid_tv_json.append(json_obj)

    LOGGER.debug(
        'finished fetch valid tvs: valid_tv_protos=%s, valid_tv_json=%s',
        valid_tv_protos, valid_tv_json)
    defer.returnValue([valid_tv_protos, valid_tv_json])
def process_tv_config_delete_request(request_context,
                                     client_single_request,
                                     single_server_response,
                                     async_client_factory):
    """This method will process a tvConfigDeleteRequest.

    :param request_context: Used to authenticate kvstore requests
    :param client_single_request: client request object protobuf
    :param single_server_response: server response object protobuf
    :param async_client factory: factory class used to generate kvstore and spacebridge clients
    """
    async_kvstore_client = async_client_factory.kvstore_client()

    tv_config_delete_request = client_single_request.tvConfigDeleteRequest

    device_ids = tv_config_delete_request.deviceId

    LOGGER.debug('device_ids to delete=%s', device_ids)
    yield validate_devices(set(device_ids), request_context, async_kvstore_client)
    # need to fetch configs before deleting to make sure we're not deleting the captain
    # and orphaning the workers, if so, for each one that is a captain, 
    # we need to fetch the other configs in the grid
    # delete the to be deleted captain from the device_id list, and update
    # the existing members of the grid

    tvs = yield get_drone_mode_tvs(request_context, async_kvstore_client, device_ids=device_ids)
    # fetch configs from kvstore
    # check to see if they are currently captain
    for tv in tvs:
        raw_id = base64.urlsafe_b64decode(str(tv[constants.KEY]))
        encoded_id = b64encode_to_str(raw_id)
        tv[constants.DEVICE_ID] = encoded_id

        if (has_grid(tv, is_json=True)):
            tv_config = TVConfig(**tv)
            grid_ids = tv.get(constants.TV_GRID, {}).get(constants.DEVICE_IDS, [])
            yield remove_from_grid(tv_config=tv_config,
                                   device_ids=grid_ids,
                                   request_context=request_context,
                                   async_kvstore_client=async_kvstore_client)

    entries_to_delete = []

    for device_id in device_ids:
        kvstore_key = b64_to_urlsafe_b64(device_id)
        post_data = {constants.KEY: kvstore_key}
        entries_to_delete.append(post_data)

    deleted_ids = yield async_kvstore_client.async_batch_save_request(
        request_context.system_auth_header,
        constants.DRONE_MODE_TVS_COLLECTION_NAME,
        entries_to_delete,
        owner=request_context.current_user)
    deleted_device_ids = [urlsafe_b64_to_b64(deleted_id) for deleted_id in deleted_ids]
    if deleted_device_ids:
        single_server_response.tvConfigDeleteResponse.deletedIds.extend(deleted_device_ids)

    if not deleted_ids:
        raise SpacebridgeApiRequestError('None of the device_ids={} were deleted'.format(device_ids), status_code=http.INTERNAL_SERVER_ERROR)

    elif len(deleted_ids) != len(device_ids):
        kvstore_keys = {b64_to_urlsafe_b64(device_id) for device_id in device_ids}
        LOGGER.error('TV configs with these ids: %s were deleted, '
                     'while TV configs with these ids: %s were not.',
                     deleted_ids, list(kvstore_keys-set(deleted_ids)))
    # update current deleted tv subscriptions and
    # all ipads registered to current user

    yield process_subscriptions(request_context,
                                async_client_factory,
                                tv_device_ids=device_ids)


    LOGGER.info('Successful TV Config Delete Request for device_ids=%s', deleted_device_ids)
def process_tv_config_set_request(request_context,
                                  client_single_request,
                                  single_server_response,
                                  async_client_factory):
    """
    This method will process a tvConfigSetRequest.

    :param request_context: Used to authenticate kvstore requests
    :param client_single_request: client request object protobuf
    :param single_server_response: server response object protobuf
    :param async_client factory: factory class used to generate kvstore and spacebridge clients
    """
    async_kvstore_client = async_client_factory.kvstore_client()
    tv_config_set_request = client_single_request.tvConfigSetRequest

    tv_config_proto = tv_config_set_request.tvConfig
    tv_config = TVConfig()
    tv_config.from_protobuf(tv_config_proto)
    timestamp = get_current_timestamp_str()
    ids_to_update = [tv_config.device_id]
    # Check to make sure device is valid before proceeding
    yield validate_devices(set(ids_to_update), request_context, async_kvstore_client)

    # Before setting config, we need to check if
    # we're setting a config on a tv that is currently in
    # a grid configuration, so we need to fetch it.
    tvs = yield get_drone_mode_tvs(request_context,
                                   async_kvstore_client,
                                   device_ids=[tv_config.device_id])

    if tvs:
        tv = tvs[0]
        existing_config = TVConfig(**tv)
        # If it's a grid, we remove it from the existing grid list,
        if has_grid(tv, is_json=True) and not has_grid(tv_config_proto):
            device_ids = ids_to_update = existing_config.tv_grid[constants.DEVICE_IDS]
            yield remove_from_grid(tv_config=existing_config,
                                   device_ids=device_ids,
                                   request_context=request_context,
                                   async_kvstore_client=async_kvstore_client,
                                   timestamp=timestamp)

    # set timestamp here
    tv_config.timestamp = timestamp
    tv_config_json = json.loads(tv_config.to_json())

    # delete device id
    if constants.DEVICE_ID in tv_config_json:
        del tv_config_json[constants.DEVICE_ID]

    LOGGER.debug('tv_config_json=%s', tv_config_json)

    # convert device id to urlsafe b64 encoded to use as kvstore key
    urlsafe_b64encoded_device_id = b64_to_urlsafe_b64(tv_config.device_id)
    tv_config_json[constants.KEY] = urlsafe_b64encoded_device_id

    response = yield async_kvstore_client.async_kvstore_post_or_update_request(
        constants.DRONE_MODE_TVS_COLLECTION_NAME,
        json.dumps(tv_config_json),
        request_context.system_auth_header,
        key_id=urlsafe_b64encoded_device_id,
        owner=request_context.current_user)

    yield check_and_raise_error(response,
                                request_context,
                                'Write TV Config',
                                valid_codes=[http.OK, http.CREATED])

    single_server_response.tvConfigSetResponse.SetInParent()
    # update tv subscription for config we just created
    # (and affected grid device ids, if applicable) as well as
    # all ipads registered to current user
    yield process_subscriptions(request_context,
                                async_client_factory,
                                tv_device_ids=ids_to_update)

    LOGGER.info("Successful TV Config Set Request for device id=%s", tv_config.device_id)
def process_tv_config_bulk_set_request(request_context,
                                       client_single_request,
                                       single_server_response,
                                       async_client_factory):
    """
    Bulk tv config set request.  Used to bulk send tv config set requests
    when we're setting a grid configuration

    :param request_context: Used to authenticate kvstore requests
    :param client_single_request: client request object protobuf
    :param single_server_response: server response object protobuf
    :param async_client factory: factory class used to generate kvstore and spacebridge clients
    """

    async_kvstore_client = async_client_factory.kvstore_client()
    tv_config_protos = client_single_request.tvConfigBulkSetRequest.tvConfig
    is_token_update = client_single_request.tvConfigBulkSetRequest.isTokenUpdate
    LOGGER.debug('tv_config_protos=%s', tv_config_protos)
    device_ids = {proto.device_id for proto in tv_config_protos}
    timestamp = get_current_timestamp_str()
    warnings = yield validate_devices(device_ids, request_context, async_kvstore_client)
    # Before setting configs, we need to check if
    # we're setting configs on tvs that are currently in
    # grid configurations, so we need to fetch them.
    # This logic is ignored if we're doing a token update

    existing_grids_to_update = set()
    if not is_token_update:
        tvs = yield get_drone_mode_tvs(request_context,
                                       async_kvstore_client,
                                       device_ids=device_ids)
        if tvs:
            requests = []
            for tv in tvs:
                existing_config = TVConfig(**tv)
                # If it's a grid, we remove it from the existing grid list
                if has_grid(tv, is_json=True):
                    grid_device_ids = existing_config.tv_grid[constants.DEVICE_IDS]
                    existing_grids_to_update.update(grid_device_ids)
                    request = remove_from_grid(tv_config=existing_config,
                                               device_ids=grid_device_ids,
                                               request_context=request_context,
                                               async_kvstore_client=async_kvstore_client,
                                               timestamp=timestamp)
                    requests.append(request)

            if requests:
                exceptions = []
                responses = yield defer.DeferredList(requests, consumeErrors=True)
                for response in responses:
                    if isinstance(response[1], Failure):
                        exceptions.append(response[1])
                LOGGER.debug('Finished updating modified tvs')
                if exceptions:
                    LOGGER.error('Encountered exceptions updating tvs, e=%s', exceptions)
                    # if we couldn't update any of the existing configs, we should not continue the request
                    if len(exceptions) == len(requests):
                        raise SpacebridgeApiRequestError('Unable to update all existing tv configs',
                                                         status_code=http.INTERNAL_SERVER_ERROR)



    post_data = []
    for tv_config_proto in tv_config_protos:
        tv_config = TVConfig()
        tv_config.from_protobuf(tv_config_proto)

        # set timestamp here
        tv_config.timestamp = timestamp
        tv_config_json = json.loads(tv_config.to_json())

        # delete device id
        if constants.DEVICE_ID in tv_config_json:
            del tv_config_json[constants.DEVICE_ID]

        LOGGER.debug('tv_config_json=%s', tv_config_json)

        # convert device id to urlsafe b64 encoded to use as kvstore key
        urlsafe_b64encoded_device_id = b64_to_urlsafe_b64(tv_config.device_id)
        tv_config_json[constants.KEY] = urlsafe_b64encoded_device_id
        post_data.append(tv_config_json)
    LOGGER.debug('post_data=%s', post_data)
    updated_keys = yield async_kvstore_client.async_batch_save_request(
        request_context.system_auth_header,
        constants.DRONE_MODE_TVS_COLLECTION_NAME,
        post_data,
        owner=request_context.current_user)
    ids_to_update = {urlsafe_b64_to_b64(key) for key in updated_keys}
    ids_to_update.update(existing_grids_to_update)
    ids_to_update = list(ids_to_update)

    LOGGER.debug('ids_to_update=%s', ids_to_update)
    yield process_subscriptions(request_context,
                                async_client_factory,
                                tv_device_ids=ids_to_update)
    if warnings:
        single_server_response.tvConfigBulkSetResponse.warnings.extend(warnings)
    single_server_response.tvConfigBulkSetResponse.SetInParent()