Source code for google.appengine.api.xmpp

#!/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:
  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.

  get_presence: Method to get the presence for 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])
_PRESENCE_SHOW_MAPPING = {
    xmpp_service_pb.PresenceResponse.NORMAL: PRESENCE_SHOW_NONE,
    xmpp_service_pb.PresenceResponse.AWAY: PRESENCE_SHOW_AWAY,
    xmpp_service_pb.PresenceResponse.DO_NOT_DISTURB: PRESENCE_SHOW_DND,
    xmpp_service_pb.PresenceResponse.CHAT: PRESENCE_SHOW_CHAT,
    xmpp_service_pb.PresenceResponse.EXTENDED_AWAY: PRESENCE_SHOW_XA,
}


MAX_STATUS_MESSAGE_SIZE = 1024

[docs]class Error(Exception): """Base error class for this module."""
[docs]class InvalidJidError(Error): """Error that indicates a request for an invalid JID."""
[docs]class InvalidTypeError(Error): """Error that indicates a request has an invalid type."""
[docs]class InvalidXmlError(Error): """Error that indicates a send message request has invalid XML."""
[docs]class NoBodyError(Error): """Error that indicates a send message request has no body."""
[docs]class InvalidMessageError(Error): """Error that indicates a received message was invalid or incomplete."""
[docs]class InvalidShowError(Error): """Error that indicates a send presence request has an invalid show."""
[docs]class InvalidStatusError(Error): """Error that indicates a send presence request has an invalid status."""
[docs]class NondefaultModuleError(Error): """Error that indicates the XMPP API was used from a non-default module.""" def __init__(self): super(NondefaultModuleError, self).__init__( 'XMPP API does not support modules')
[docs]def get_presence(jid, from_jid=None, get_show=False): """Gets the presence for a JID. Args: jid: The JID of the contact whose presence is requested. This may also be a list of JIDs, which also implies get_show (below). 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. get_show: if True, return a tuple of (is_available, show). If a list of jids is given, this will always be True. Returns: At minimum, a boolean is_available representing whether the requested JID is available. If get_show is specified, a tuple (is_available, show) will be given. If a list of JIDs is given, a list of tuples will be returned, including is_available, show, and an additional boolean indicating if that JID was valid. Raises: InvalidJidError: Raised if no JID passed in is valid. Error: if an unspecified error happens processing the request. """ if not jid: raise InvalidJidError() request = xmpp_service_pb.BulkPresenceRequest() response = xmpp_service_pb.BulkPresenceResponse() if isinstance(jid, basestring): single_jid = True jidlist = [jid] else: single_jid = False get_show = True jidlist = jid for given_jid in jidlist: request.add_jid(_to_str(given_jid)) if from_jid: request.set_from_jid(_to_str(from_jid)) try: apiproxy_stub_map.MakeSyncCall("xmpp", "BulkGetPresence", 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.NONDEFAULT_MODULE): raise NondefaultModuleError() else: raise Error() def HandleSubresponse(subresponse): if get_show: if subresponse.has_presence(): presence = subresponse.presence() show = _PRESENCE_SHOW_MAPPING.get(presence, None) else: show = None return bool(subresponse.is_available()), show, subresponse.valid() else: return bool(subresponse.is_available()), subresponse.valid() results = [HandleSubresponse(s) for s in response.presence_response_list()] if not any(t[-1] for t in results): raise InvalidJidError() if single_jid: if get_show: return results[0][:-1] else: return results[0][0] else: return results
[docs]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() elif (e.application_error == xmpp_service_pb.XmppServiceError.NONDEFAULT_MODULE): raise NondefaultModuleError() else: raise Error() return
[docs]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() elif (e.application_error == xmpp_service_pb.XmppServiceError.NONDEFAULT_MODULE): raise NondefaultModuleError() raise Error() if single_jid: return response.status_list()[0] return response.status_list()
[docs]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() elif (e.application_error == xmpp_service_pb.XmppServiceError.NONDEFAULT_MODULE): raise NondefaultModuleError() raise Error()
[docs]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
[docs] 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

Send feedback about...

App Engine standard environment for Python