#!/usr/bin/env python2.7
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#




"""Channel API.

This module allows App Engine apps to push messages to a client.

Functions defined in this module:
  create_channel: Creates a channel to send messages to.
  send_message: Send a message to any clients listening on the given channel.
"""






import os

from google.appengine.api import api_base_pb
from google.appengine.api import apiproxy_stub_map
from google.appengine.api.channel import channel_service_pb
from google.appengine.runtime import apiproxy_errors








MAXIMUM_CLIENT_ID_LENGTH = 256


MAXIMUM_TOKEN_DURATION_MINUTES = 24 * 60






MAXIMUM_MESSAGE_LENGTH = 32767


class Error(Exception):
  """Base error class for this module."""


class InvalidChannelClientIdError(Error):
  """Error that indicates a bad client id."""


class InvalidChannelTokenDurationError(Error):
  """Error that indicates the requested duration is invalid."""


class InvalidMessageError(Error):
  """Error that indicates a message is malformed."""


class AppIdAliasRequired(Error):
  """Error that indicates you must assign an application alias to your app."""


def _ToChannelError(error):
  """Translate an application error to a channel Error, if possible.

  Args:
    error: An ApplicationError to translate.

  Returns:
    The appropriate channel service error, if a match is found, or the original
    ApplicationError.
  """
  error_map = {
      channel_service_pb.ChannelServiceError.INVALID_CHANNEL_KEY:
        InvalidChannelClientIdError,
      channel_service_pb.ChannelServiceError.BAD_MESSAGE:
        InvalidMessageError,
      channel_service_pb.ChannelServiceError.APPID_ALIAS_REQUIRED:
        AppIdAliasRequired
      }

  if error.application_error in error_map:
    return error_map[error.application_error](error.error_detail)
  else:
    return error


def _GetService():
  """Gets the service name to use, based on if we're on the dev server."""
  if os.environ.get('SERVER_SOFTWARE', '').startswith('Devel'):
    return 'channel'
  else:
    return 'xmpp'


def _ValidateClientId(client_id):
  """Validates a client id.

  Args:
    client_id: The client id provided by the application.

  Returns:
    If the client id is of type str, returns the original client id.
    If the client id is of type unicode, returns the id encoded to utf-8.

  Raises:
    InvalidChannelClientIdError: if client id is not an instance of str or
        unicode, or if the (utf-8 encoded) string is longer than 64 characters.
  """
  if not isinstance(client_id, basestring):
    raise InvalidChannelClientIdError('"%s" is not a string.' % client_id)

  if isinstance(client_id, unicode):
    client_id = client_id.encode('utf-8')


  if len(client_id) > MAXIMUM_CLIENT_ID_LENGTH:
    msg = 'Client id length %d is greater than max length %d' % (
         len(client_id), MAXIMUM_CLIENT_ID_LENGTH)
    raise InvalidChannelClientIdError(msg)

  return client_id



def create_channel(client_id, duration_minutes=None):
  """Create a channel.

  Args:
    client_id: A string to identify this channel on the server side.
    duration_minutes: An int specifying the number of minutes for which the
        returned token should be valid.

  Returns:
    A token that the client can use to connect to the channel.

  Raises:
    InvalidChannelClientIdError: if clientid is not an instance of str or
        unicode, or if the (utf-8 encoded) string is longer than 64 characters.
    InvalidChannelTokenDurationError: if duration_minutes is not a number, less
        than 1, or greater than 1440 (the number of minutes in a day).
    Other errors returned by _ToChannelError
  """


  client_id = _ValidateClientId(client_id)

  if not duration_minutes is None:
    if not isinstance(duration_minutes, (int, long)):
      raise InvalidChannelTokenDurationError(
         'Argument duration_minutes must be integral')
    elif duration_minutes < 1:
      raise InvalidChannelTokenDurationError(
         'Argument duration_minutes must not be less than 1')
    elif duration_minutes > MAXIMUM_TOKEN_DURATION_MINUTES:
      msg = ('Argument duration_minutes must be less than %d'
             % (MAXIMUM_TOKEN_DURATION_MINUTES + 1))
      raise InvalidChannelTokenDurationError(msg)


  request = channel_service_pb.CreateChannelRequest()
  response = channel_service_pb.CreateChannelResponse()

  request.set_application_key(client_id)
  if not duration_minutes is None:
    request.set_duration_minutes(duration_minutes)

  try:
    apiproxy_stub_map.MakeSyncCall(_GetService(),
                                   'CreateChannel',
                                   request,
                                   response)
  except apiproxy_errors.ApplicationError, e:
    raise _ToChannelError(e)

  return response.token()



def send_message(client_id, message):
  """Send a message to a channel.

  Args:
    client_id: The client id passed to create_channel.
    message: A string representing the message to send.

  Raises:
    InvalidChannelClientIdError: if client_id is not an instance of str or
        unicode, or if the (utf-8 encoded) string is longer than 64 characters.
    InvalidMessageError: if the message isn't a string or is too long.
    Errors returned by _ToChannelError
  """

  client_id = _ValidateClientId(client_id)

  if isinstance(message, unicode):
    message = message.encode('utf-8')
  elif not isinstance(message, str):
    raise InvalidMessageError('Message must be a string')

  if len(message) > MAXIMUM_MESSAGE_LENGTH:
    raise InvalidMessageError(
        'Message must be no longer than %d chars' % MAXIMUM_MESSAGE_LENGTH)

  request = channel_service_pb.SendMessageRequest()
  response = api_base_pb.VoidProto()

  request.set_application_key(client_id)
  request.set_message(message)

  try:
    apiproxy_stub_map.MakeSyncCall(_GetService(),
                                   'SendChannelMessage',
                                   request,
                                   response)
  except apiproxy_errors.ApplicationError, e:
    raise _ToChannelError(e)
