#!/usr/bin/env python
#
# 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.
#




"""XMPP API.

This module allows AppEngine apps to interact with a bot representing that app
on the Google Talk network.

Functions defined in this module:
  get_presence: Gets the presence for a JID.
  send_message: Sends a chat message to any number of JIDs.
  send_invite: Sends an invitation to chat to a JID.
  send_presence: Sends a presence to a JID.

Classes defined in this module:
  Message: A class to encapsulate received messages.
"""










from google.appengine.api import apiproxy_stub_map
from google.appengine.api.xmpp import xmpp_service_pb
from google.appengine.runtime import apiproxy_errors



NO_ERROR    = xmpp_service_pb.XmppMessageResponse.NO_ERROR
INVALID_JID = xmpp_service_pb.XmppMessageResponse.INVALID_JID
OTHER_ERROR = xmpp_service_pb.XmppMessageResponse.OTHER_ERROR




MESSAGE_TYPE_NONE = ""
MESSAGE_TYPE_CHAT = "chat"
MESSAGE_TYPE_ERROR = "error"
MESSAGE_TYPE_GROUPCHAT = "groupchat"
MESSAGE_TYPE_HEADLINE = "headline"
MESSAGE_TYPE_NORMAL = "normal"

_VALID_MESSAGE_TYPES = frozenset([MESSAGE_TYPE_NONE, MESSAGE_TYPE_CHAT,
                                  MESSAGE_TYPE_ERROR, MESSAGE_TYPE_GROUPCHAT,
                                  MESSAGE_TYPE_HEADLINE, MESSAGE_TYPE_NORMAL])




PRESENCE_TYPE_AVAILABLE = ""
PRESENCE_TYPE_UNAVAILABLE = "unavailable"
PRESENCE_TYPE_PROBE = "probe"
_VALID_PRESENCE_TYPES = frozenset([PRESENCE_TYPE_AVAILABLE,
                                   PRESENCE_TYPE_UNAVAILABLE,
                                   PRESENCE_TYPE_PROBE])




PRESENCE_SHOW_NONE = ""
PRESENCE_SHOW_AWAY = "away"
PRESENCE_SHOW_CHAT = "chat"
PRESENCE_SHOW_DND = "dnd"
PRESENCE_SHOW_XA = "xa"
_VALID_PRESENCE_SHOWS = frozenset([PRESENCE_SHOW_NONE, PRESENCE_SHOW_AWAY,
                                   PRESENCE_SHOW_CHAT, PRESENCE_SHOW_DND,
                                   PRESENCE_SHOW_XA])


MAX_STATUS_MESSAGE_SIZE = 1024

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


class InvalidJidError(Error):
  """Error that indicates a request for an invalid JID."""


class InvalidTypeError(Error):
  """Error that indicates a request has an invalid type."""


class InvalidXmlError(Error):
  """Error that indicates a send message request has invalid XML."""


class NoBodyError(Error):
  """Error that indicates a send message request has no body."""


class InvalidMessageError(Error):
  """Error that indicates a received message was invalid or incomplete."""


class InvalidShowError(Error):
  """Error that indicates a send presence request has an invalid show."""


class InvalidStatusError(Error):
  """Error that indicates a send presence request has an invalid status."""


def get_presence(jid, from_jid=None):
  """Gets the presence for a JID.

  Args:
    jid: The JID of the contact whose presence is requested.
    from_jid: The optional custom JID to use for sending. Currently, the default
      is <appid>@appspot.com. This is supported as a value. Custom JIDs can be
      of the form <anything>@<appid>.appspotchat.com.

  Returns:
    bool, Whether the user is online.

  Raises:
    InvalidJidError if any of the JIDs passed are invalid.
    Error if an unspecified error happens processing the request.
  """
  if not jid:
    raise InvalidJidError()

  request = xmpp_service_pb.PresenceRequest()
  response = xmpp_service_pb.PresenceResponse()

  request.set_jid(_to_str(jid))
  if from_jid:
    request.set_from_jid(_to_str(from_jid))

  try:
    apiproxy_stub_map.MakeSyncCall("xmpp",
                                   "GetPresence",
                                   request,
                                   response)
  except apiproxy_errors.ApplicationError, e:
    if (e.application_error ==
        xmpp_service_pb.XmppServiceError.INVALID_JID):
      raise InvalidJidError()
    else:
      raise Error()

  return bool(response.is_available())


def send_invite(jid, from_jid=None):
  """Sends an invitation to chat to a JID.

  Args:
    jid: The JID of the contact to invite.
    from_jid: The optional custom JID to use for sending. Currently, the default
      is <appid>@appspot.com. This is supported as a value. Custom JIDs can be
      of the form <anything>@<appid>.appspotchat.com.

  Raises:
    InvalidJidError if the JID passed is invalid.
    Error if an unspecified error happens processing the request.
  """
  if not jid:
    raise InvalidJidError()

  request = xmpp_service_pb.XmppInviteRequest()
  response = xmpp_service_pb.XmppInviteResponse()

  request.set_jid(_to_str(jid))
  if from_jid:
    request.set_from_jid(_to_str(from_jid))

  try:
    apiproxy_stub_map.MakeSyncCall("xmpp",
                                   "SendInvite",
                                   request,
                                   response)
  except apiproxy_errors.ApplicationError, e:
    if (e.application_error ==
        xmpp_service_pb.XmppServiceError.INVALID_JID):
      raise InvalidJidError()
    else:
      raise Error()

  return


def send_message(jids, body, from_jid=None, message_type=MESSAGE_TYPE_CHAT,
                 raw_xml=False):
  """Sends a chat message to a list of JIDs.

  Args:
    jids: A list of JIDs to send the message to, or a single JID to send the
      message to.
    from_jid: The optional custom JID to use for sending. Currently, the default
      is <appid>@appspot.com. This is supported as a value. Custom JIDs can be
      of the form <anything>@<appid>.appspotchat.com.
    body: The body of the message.
    message_type: Optional type of the message. Should be one of the types
      specified in RFC 3921, section 2.1.1. An empty string will result in a
      message stanza without a type attribute. For convenience, all of the
      valid types are in the MESSAGE_TYPE_* constants in this file. The
      default is MESSAGE_TYPE_CHAT. Anything else will throw an exception.
    raw_xml: Optionally specifies that the body should be interpreted as XML. If
      this is false, the contents of the body will be escaped and placed inside
      of a body element inside of the message. If this is true, the contents
      will be made children of the message.

  Returns:
    list, A list of statuses, one for each JID, corresponding to the result of
      sending the message to that JID. Or, if a single JID was passed in,
      returns the status directly.

  Raises:
    InvalidJidError if there is no valid JID in the list.
    InvalidTypeError if the type argument is invalid.
    InvalidXmlError if the body is malformed XML and raw_xml is True.
    NoBodyError if there is no body.
    Error if another error occurs processing the request.
  """
  request = xmpp_service_pb.XmppMessageRequest()
  response = xmpp_service_pb.XmppMessageResponse()

  if not body:
    raise NoBodyError()

  if not jids:
    raise InvalidJidError()

  if not message_type in _VALID_MESSAGE_TYPES:
    raise InvalidTypeError()

  single_jid = False
  if isinstance(jids, basestring):
    single_jid = True
    jids = [jids]

  for jid in jids:
    if not jid:
      raise InvalidJidError()
    request.add_jid(_to_str(jid))

  request.set_body(_to_str(body))
  request.set_type(_to_str(message_type))
  request.set_raw_xml(raw_xml)
  if from_jid:
    request.set_from_jid(_to_str(from_jid))

  try:
    apiproxy_stub_map.MakeSyncCall("xmpp",
                                   "SendMessage",
                                   request,
                                   response)
  except apiproxy_errors.ApplicationError, e:
    if (e.application_error ==
        xmpp_service_pb.XmppServiceError.INVALID_JID):
      raise InvalidJidError()
    elif (e.application_error ==
          xmpp_service_pb.XmppServiceError.INVALID_TYPE):
      raise InvalidTypeError()
    elif (e.application_error ==
          xmpp_service_pb.XmppServiceError.INVALID_XML):
      raise InvalidXmlError()
    elif (e.application_error ==
          xmpp_service_pb.XmppServiceError.NO_BODY):
      raise NoBodyError()
    raise Error()

  if single_jid:
    return response.status_list()[0]
  return response.status_list()


def send_presence(jid, status=None, from_jid=None,
                  presence_type=PRESENCE_TYPE_AVAILABLE,
                  presence_show=PRESENCE_SHOW_NONE):
  """Sends a presence to a given JID.

  Args:
    jid: A JID to send the presence to.
    status: The optional status message. Size is limited to 1KB.
    from_jid: The optional custom JID to use for sending. Currently, the default
      is <appid>@appspot.com. This is supported as a value. Custom JIDs can be
      of the form <anything>@<appid>.appspotchat.com.
    presence_type: Optional type of the presence. This accepts a subset of the
      types specified in RFC 3921, section 2.2.1. An empty string will result
      in a presence stanza without a type attribute. For convenience, all of the
      valid types are in the PRESENCE_TYPE_* constants in this file. The default
      is PRESENCE_TYPE_AVAILABLE. Anything else will throw an exception.
    presence_show: Optional show value for the presence. Should be one of the
      values specified in RFC 3921, section 2.2.2.1. An empty string will result
      in a presence stanza without a show element. For convenience, all of the
      valid types are in the PRESENCE_SHOW_* constants in this file. The
      default is PRESENCE_SHOW_NONE. Anything else will throw an exception.

  Raises:
    InvalidJidError if there is no valid JID in the list.
    InvalidTypeError if the type argument is invalid.
    InvalidShowError if the show argument is invalid.
    InvalidStatusError if the status argument is too large.
    Error if another error occurs processing the request.
  """
  request = xmpp_service_pb.XmppSendPresenceRequest()
  response = xmpp_service_pb.XmppSendPresenceResponse()

  if not jid:
    raise InvalidJidError()

  if presence_type and not _to_str(presence_type) in _VALID_PRESENCE_TYPES:
    raise InvalidTypeError()

  if presence_show and not _to_str(presence_show) in _VALID_PRESENCE_SHOWS:
    raise InvalidShowError()

  if status and len(status) > MAX_STATUS_MESSAGE_SIZE:
    raise InvalidStatusError()

  request.set_jid(_to_str(jid))
  if status:
    request.set_status(_to_str(status))
  if presence_type:
    request.set_type(_to_str(presence_type))
  if presence_show:
    request.set_show(_to_str(presence_show))
  if from_jid:
    request.set_from_jid(_to_str(from_jid))

  try:
    apiproxy_stub_map.MakeSyncCall("xmpp",
                                   "SendPresence",
                                   request,
                                   response)
  except apiproxy_errors.ApplicationError, e:
    if (e.application_error ==
        xmpp_service_pb.XmppServiceError.INVALID_JID):
      raise InvalidJidError()
    elif (e.application_error ==
          xmpp_service_pb.XmppServiceError.INVALID_TYPE):
      raise InvalidTypeError()
    elif (e.application_error ==
          xmpp_service_pb.XmppServiceError.INVALID_SHOW):
      raise InvalidShowError()
    raise Error()

  return




class Message(object):
  """Encapsulates an XMPP message received by the application."""

  def __init__(self, vars):
    """Constructs a new XMPP Message from an HTTP request.

    Args:
      vars: A dict-like object to extract message arguments from.
    """
    try:
      self.__sender = vars["from"]
      self.__to = vars["to"]
      self.__body = vars["body"]
    except KeyError, e:
      raise InvalidMessageError(e[0])
    self.__command = None
    self.__arg = None

  @property
  def sender(self):
    return self.__sender

  @property
  def to(self):
    return self.__to

  @property
  def body(self):
    return self.__body

  def __parse_command(self):
    if self.__arg != None:
      return


    body = self.__body
    if body.startswith('\\'):
      body = '/' + body[1:]

    self.__arg = ''
    if body.startswith('/'):
      parts = body.split(' ', 1)
      self.__command = parts[0][1:]
      if len(parts) > 1:
        self.__arg = parts[1].strip()
    else:
      self.__arg = self.__body.strip()

  @property
  def command(self):
    self.__parse_command()
    return self.__command

  @property
  def arg(self):
    self.__parse_command()
    return self.__arg

  def reply(self, body, message_type=MESSAGE_TYPE_CHAT, raw_xml=False,
            send_message=send_message):
    """Convenience function to reply to a message.

    Args:
      body: str: The body of the message
      message_type, raw_xml: As per send_message.
      send_message: Used for testing.

    Returns:
      A status code as per send_message.

    Raises:
      See send_message.
    """
    return send_message([self.sender], body, from_jid=self.to,
                        message_type=message_type, raw_xml=raw_xml)


def _to_str(value):
  """Helper function to make sure unicode values converted to utf-8

  Args:
    value: str or unicode to convert to utf-8.

  Returns:
    UTF-8 encoded str of value, otherwise value unchanged.
  """
  if isinstance(value, unicode):
    return value.encode('utf-8')
  return value
