xmpp: import xmppy-0.5.0rc1
[uccdoor.git] / xmpp / protocol.py
diff --git a/xmpp/protocol.py b/xmpp/protocol.py
new file mode 100644 (file)
index 0000000..3e49b8d
--- /dev/null
@@ -0,0 +1,860 @@
+##   protocol.py 
+##
+##   Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+##   This program is free software; you can redistribute it and/or modify
+##   it under the terms of the GNU General Public License as published by
+##   the Free Software Foundation; either version 2, or (at your option)
+##   any later version.
+##
+##   This program is distributed in the hope that it will be useful,
+##   but WITHOUT ANY WARRANTY; without even the implied warranty of
+##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+##   GNU General Public License for more details.
+
+# $Id: protocol.py,v 1.60 2009/04/07 11:14:28 snakeru Exp $
+
+"""
+Protocol module contains tools that is needed for processing of 
+xmpp-related data structures.
+"""
+
+from simplexml import Node,ustr
+import time
+NS_ACTIVITY         ='http://jabber.org/protocol/activity'                  # XEP-0108
+NS_ADDRESS          ='http://jabber.org/protocol/address'                   # XEP-0033
+NS_ADMIN            ='http://jabber.org/protocol/admin'                     # XEP-0133
+NS_ADMIN_ADD_USER               =NS_ADMIN+'#add-user'                       # XEP-0133
+NS_ADMIN_DELETE_USER            =NS_ADMIN+'#delete-user'                    # XEP-0133
+NS_ADMIN_DISABLE_USER           =NS_ADMIN+'#disable-user'                   # XEP-0133
+NS_ADMIN_REENABLE_USER          =NS_ADMIN+'#reenable-user'                  # XEP-0133
+NS_ADMIN_END_USER_SESSION       =NS_ADMIN+'#end-user-session'               # XEP-0133
+NS_ADMIN_GET_USER_PASSWORD      =NS_ADMIN+'#get-user-password'              # XEP-0133
+NS_ADMIN_CHANGE_USER_PASSWORD   =NS_ADMIN+'#change-user-password'           # XEP-0133
+NS_ADMIN_GET_USER_ROSTER        =NS_ADMIN+'#get-user-roster'                # XEP-0133
+NS_ADMIN_GET_USER_LASTLOGIN     =NS_ADMIN+'#get-user-lastlogin'             # XEP-0133
+NS_ADMIN_USER_STATS             =NS_ADMIN+'#user-stats'                     # XEP-0133
+NS_ADMIN_EDIT_BLACKLIST         =NS_ADMIN+'#edit-blacklist'                 # XEP-0133
+NS_ADMIN_EDIT_WHITELIST         =NS_ADMIN+'#edit-whitelist'                 # XEP-0133
+NS_ADMIN_REGISTERED_USERS_NUM   =NS_ADMIN+'#get-registered-users-num'       # XEP-0133
+NS_ADMIN_DISABLED_USERS_NUM     =NS_ADMIN+'#get-disabled-users-num'         # XEP-0133
+NS_ADMIN_ONLINE_USERS_NUM       =NS_ADMIN+'#get-online-users-num'           # XEP-0133
+NS_ADMIN_ACTIVE_USERS_NUM       =NS_ADMIN+'#get-active-users-num'           # XEP-0133
+NS_ADMIN_IDLE_USERS_NUM         =NS_ADMIN+'#get-idle-users-num'             # XEP-0133
+NS_ADMIN_REGISTERED_USERS_LIST  =NS_ADMIN+'#get-registered-users-list'      # XEP-0133
+NS_ADMIN_DISABLED_USERS_LIST    =NS_ADMIN+'#get-disabled-users-list'        # XEP-0133
+NS_ADMIN_ONLINE_USERS_LIST      =NS_ADMIN+'#get-online-users-list'          # XEP-0133
+NS_ADMIN_ACTIVE_USERS_LIST      =NS_ADMIN+'#get-active-users-list'          # XEP-0133
+NS_ADMIN_IDLE_USERS_LIST        =NS_ADMIN+'#get-idle-users-list'            # XEP-0133
+NS_ADMIN_ANNOUNCE               =NS_ADMIN+'#announce'                       # XEP-0133
+NS_ADMIN_SET_MOTD               =NS_ADMIN+'#set-motd'                       # XEP-0133
+NS_ADMIN_EDIT_MOTD              =NS_ADMIN+'#edit-motd'                      # XEP-0133
+NS_ADMIN_DELETE_MOTD            =NS_ADMIN+'#delete-motd'                    # XEP-0133
+NS_ADMIN_SET_WELCOME            =NS_ADMIN+'#set-welcome'                    # XEP-0133
+NS_ADMIN_DELETE_WELCOME         =NS_ADMIN+'#delete-welcome'                 # XEP-0133
+NS_ADMIN_EDIT_ADMIN             =NS_ADMIN+'#edit-admin'                     # XEP-0133
+NS_ADMIN_RESTART                =NS_ADMIN+'#restart'                        # XEP-0133
+NS_ADMIN_SHUTDOWN               =NS_ADMIN+'#shutdown'                       # XEP-0133
+NS_AGENTS           ='jabber:iq:agents'                                     # XEP-0094 (historical)
+NS_AMP              ='http://jabber.org/protocol/amp'                       # XEP-0079
+NS_AMP_ERRORS                   =NS_AMP+'#errors'                           # XEP-0079
+NS_AUTH             ='jabber:iq:auth'                                       # XEP-0078
+NS_AVATAR           ='jabber:iq:avatar'                                     # XEP-0008 (historical)
+NS_BIND             ='urn:ietf:params:xml:ns:xmpp-bind'                     # RFC 3920
+NS_BROWSE           ='jabber:iq:browse'                                     # XEP-0011 (historical)
+NS_BYTESTREAM       ='http://jabber.org/protocol/bytestreams'               # XEP-0065
+NS_CAPS             ='http://jabber.org/protocol/caps'                      # XEP-0115
+NS_CHATSTATES       ='http://jabber.org/protocol/chatstates'                # XEP-0085
+NS_CLIENT           ='jabber:client'                                        # RFC 3921
+NS_COMMANDS         ='http://jabber.org/protocol/commands'                  # XEP-0050
+NS_COMPONENT_ACCEPT ='jabber:component:accept'                              # XEP-0114
+NS_COMPONENT_1      ='http://jabberd.jabberstudio.org/ns/component/1.0'     # Jabberd2
+NS_COMPRESS         ='http://jabber.org/protocol/compress'                  # XEP-0138
+NS_DATA             ='jabber:x:data'                                        # XEP-0004
+NS_DATA_LAYOUT      ='http://jabber.org/protocol/xdata-layout'              # XEP-0141
+NS_DATA_VALIDATE    ='http://jabber.org/protocol/xdata-validate'            # XEP-0122
+NS_DELAY            ='jabber:x:delay'                                       # XEP-0091 (deprecated)
+NS_DIALBACK         ='jabber:server:dialback'                               # RFC 3921
+NS_DISCO            ='http://jabber.org/protocol/disco'                     # XEP-0030
+NS_DISCO_INFO                   =NS_DISCO+'#info'                           # XEP-0030
+NS_DISCO_ITEMS                  =NS_DISCO+'#items'                          # XEP-0030
+NS_ENCRYPTED        ='jabber:x:encrypted'                                   # XEP-0027
+NS_EVENT            ='jabber:x:event'                                       # XEP-0022 (deprecated)
+NS_FEATURE          ='http://jabber.org/protocol/feature-neg'               # XEP-0020
+NS_FILE             ='http://jabber.org/protocol/si/profile/file-transfer'  # XEP-0096
+NS_GATEWAY          ='jabber:iq:gateway'                                    # XEP-0100
+NS_GEOLOC           ='http://jabber.org/protocol/geoloc'                    # XEP-0080
+NS_GROUPCHAT        ='gc-1.0'                                               # XEP-0045
+NS_HTTP_BIND        ='http://jabber.org/protocol/httpbind'                  # XEP-0124
+NS_IBB              ='http://jabber.org/protocol/ibb'                       # XEP-0047
+NS_INVISIBLE        ='presence-invisible'                                   # Jabberd2
+NS_IQ               ='iq'                                                   # Jabberd2
+NS_LAST             ='jabber:iq:last'                                       # XEP-0012
+NS_MESSAGE          ='message'                                              # Jabberd2
+NS_MOOD             ='http://jabber.org/protocol/mood'                      # XEP-0107
+NS_MUC              ='http://jabber.org/protocol/muc'                       # XEP-0045
+NS_MUC_ADMIN                    =NS_MUC+'#admin'                            # XEP-0045
+NS_MUC_OWNER                    =NS_MUC+'#owner'                            # XEP-0045
+NS_MUC_UNIQUE                   =NS_MUC+'#unique'                           # XEP-0045
+NS_MUC_USER                     =NS_MUC+'#user'                             # XEP-0045
+NS_MUC_REGISTER                 =NS_MUC+'#register'                         # XEP-0045
+NS_MUC_REQUEST                  =NS_MUC+'#request'                          # XEP-0045
+NS_MUC_ROOMCONFIG               =NS_MUC+'#roomconfig'                       # XEP-0045
+NS_MUC_ROOMINFO                 =NS_MUC+'#roominfo'                         # XEP-0045
+NS_MUC_ROOMS                    =NS_MUC+'#rooms'                            # XEP-0045
+NS_MUC_TRAFIC                   =NS_MUC+'#traffic'                          # XEP-0045
+NS_NICK             ='http://jabber.org/protocol/nick'                      # XEP-0172
+NS_OFFLINE          ='http://jabber.org/protocol/offline'                   # XEP-0013
+NS_PHYSLOC          ='http://jabber.org/protocol/physloc'                   # XEP-0112
+NS_PRESENCE         ='presence'                                             # Jabberd2
+NS_PRIVACY          ='jabber:iq:privacy'                                    # RFC 3921
+NS_PRIVATE          ='jabber:iq:private'                                    # XEP-0049
+NS_PUBSUB           ='http://jabber.org/protocol/pubsub'                    # XEP-0060
+NS_REGISTER         ='jabber:iq:register'                                   # XEP-0077
+NS_RC               ='http://jabber.org/protocol/rc'                        # XEP-0146
+NS_ROSTER           ='jabber:iq:roster'                                     # RFC 3921
+NS_ROSTERX          ='http://jabber.org/protocol/rosterx'                   # XEP-0144
+NS_RPC              ='jabber:iq:rpc'                                        # XEP-0009
+NS_SASL             ='urn:ietf:params:xml:ns:xmpp-sasl'                     # RFC 3920
+NS_SEARCH           ='jabber:iq:search'                                     # XEP-0055
+NS_SERVER           ='jabber:server'                                        # RFC 3921
+NS_SESSION          ='urn:ietf:params:xml:ns:xmpp-session'                  # RFC 3921
+NS_SI               ='http://jabber.org/protocol/si'                        # XEP-0096
+NS_SI_PUB           ='http://jabber.org/protocol/sipub'                     # XEP-0137
+NS_SIGNED           ='jabber:x:signed'                                      # XEP-0027
+NS_STANZAS          ='urn:ietf:params:xml:ns:xmpp-stanzas'                  # RFC 3920
+NS_STREAMS          ='http://etherx.jabber.org/streams'                     # RFC 3920
+NS_TIME             ='jabber:iq:time'                                       # XEP-0090 (deprecated)
+NS_TLS              ='urn:ietf:params:xml:ns:xmpp-tls'                      # RFC 3920
+NS_VACATION         ='http://jabber.org/protocol/vacation'                  # XEP-0109
+NS_VCARD            ='vcard-temp'                                           # XEP-0054
+NS_VCARD_UPDATE     ='vcard-temp:x:update'                                  # XEP-0153
+NS_VERSION          ='jabber:iq:version'                                    # XEP-0092
+NS_WAITINGLIST      ='http://jabber.org/protocol/waitinglist'               # XEP-0130
+NS_XHTML_IM         ='http://jabber.org/protocol/xhtml-im'                  # XEP-0071
+NS_XMPP_STREAMS     ='urn:ietf:params:xml:ns:xmpp-streams'                  # RFC 3920
+
+xmpp_stream_error_conditions="""
+bad-format --  --  -- The entity has sent XML that cannot be processed.
+bad-namespace-prefix --  --  -- The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix.
+conflict --  --  -- The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream.
+connection-timeout --  --  -- The entity has not generated any traffic over the stream for some period of time.
+host-gone --  --  -- The value of the 'to' attribute provided by the initiating entity in the stream header corresponds to a hostname that is no longer hosted by the server.
+host-unknown --  --  -- The value of the 'to' attribute provided by the initiating entity in the stream header does not correspond to a hostname that is hosted by the server.
+improper-addressing --  --  -- A stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value).
+internal-server-error --  --  -- The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.
+invalid-from -- cancel --  -- The JID or hostname provided in a 'from' address does not match an authorized JID or validated domain negotiated between servers via SASL or dialback, or between a client and a server via authentication and resource authorization.
+invalid-id --  --  -- The stream ID or dialback ID is invalid or does not match an ID previously provided.
+invalid-namespace --  --  -- The streams namespace name is something other than "http://etherx.jabber.org/streams" or the dialback namespace name is something other than "jabber:server:dialback".
+invalid-xml --  --  -- The entity has sent invalid XML over the stream to a server that performs validation.
+not-authorized --  --  -- The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action related to stream negotiation.
+policy-violation --  --  -- The entity has violated some local service policy.
+remote-connection-failed --  --  -- The server is unable to properly connect to a remote resource that is required for authentication or authorization.
+resource-constraint --  --  -- The server lacks the system resources necessary to service the stream.
+restricted-xml --  --  -- The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character.
+see-other-host --  --  -- The server will not provide service to the initiating entity but is redirecting traffic to another host.
+system-shutdown --  --  -- The server is being shut down and all active streams are being closed.
+undefined-condition --  --  -- The error condition is not one of those defined by the other conditions in this list.
+unsupported-encoding --  --  -- The initiating entity has encoded the stream in an encoding that is not supported by the server.
+unsupported-stanza-type --  --  -- The initiating entity has sent a first-level child of the stream that is not supported by the server.
+unsupported-version --  --  -- The value of the 'version' attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server.
+xml-not-well-formed --  --  -- The initiating entity has sent XML that is not well-formed."""
+xmpp_stanza_error_conditions="""
+bad-request -- 400 -- modify -- The sender has sent XML that is malformed or that cannot be processed.
+conflict -- 409 -- cancel -- Access cannot be granted because an existing resource or session exists with the same name or address.
+feature-not-implemented -- 501 -- cancel -- The feature requested is not implemented by the recipient or server and therefore cannot be processed.
+forbidden -- 403 -- auth -- The requesting entity does not possess the required permissions to perform the action.
+gone -- 302 -- modify -- The recipient or server can no longer be contacted at this address.
+internal-server-error -- 500 -- wait -- The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error.
+item-not-found -- 404 -- cancel -- The addressed JID or item requested cannot be found.
+jid-malformed -- 400 -- modify -- The value of the 'to' attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme.
+not-acceptable -- 406 -- cancel -- The recipient or server understands the request but is refusing to process it because it does not meet criteria defined by the recipient or server.
+not-allowed -- 405 -- cancel -- The recipient or server does not allow any entity to perform the action.
+not-authorized -- 401 -- auth -- The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials.
+payment-required -- 402 -- auth -- The requesting entity is not authorized to access the requested service because payment is required.
+recipient-unavailable -- 404 -- wait -- The intended recipient is temporarily unavailable.
+redirect -- 302 -- modify -- The recipient or server is redirecting requests for this information to another entity.
+registration-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because registration is required.
+remote-server-not-found -- 404 -- cancel -- A remote server or service specified as part or all of the JID of the intended recipient does not exist.
+remote-server-timeout -- 504 -- wait -- A remote server or service specified as part or all of the JID of the intended recipient could not be contacted within a reasonable amount of time.
+resource-constraint -- 500 -- wait -- The server or recipient lacks the system resources necessary to service the request.
+service-unavailable -- 503 -- cancel -- The server or recipient does not currently provide the requested service.
+subscription-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because a subscription is required.
+undefined-condition -- 500 --  -- 
+unexpected-request -- 400 -- wait -- The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order)."""
+sasl_error_conditions="""
+aborted --  --  -- The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element.
+incorrect-encoding --  --  -- The data provided by the initiating entity could not be processed because the [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003. encoding is incorrect (e.g., because the encoding does not adhere to the definition in Section 3 of [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003.); sent in reply to a <response/> element or an <auth/> element with initial response data.
+invalid-authzid --  --  -- The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID; sent in reply to a <response/> element or an <auth/> element with initial response data.
+invalid-mechanism --  --  -- The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity; sent in reply to an <auth/> element.
+mechanism-too-weak --  --  -- The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a <response/> element or an <auth/> element with initial response data.
+not-authorized --  --  -- The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username); sent in reply to a <response/> element or an <auth/> element with initial response data.
+temporary-auth-failure --  --  -- The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an <auth/> element or <response/> element."""
+
+ERRORS,_errorcodes={},{}
+for ns,errname,errpool in [(NS_XMPP_STREAMS,'STREAM',xmpp_stream_error_conditions),
+                           (NS_STANZAS     ,'ERR'   ,xmpp_stanza_error_conditions),
+                           (NS_SASL        ,'SASL'  ,sasl_error_conditions)]:
+    for err in errpool.split('\n')[1:]:
+        cond,code,typ,text=err.split(' -- ')
+        name=errname+'_'+cond.upper().replace('-','_')
+        locals()[name]=ns+' '+cond
+        ERRORS[ns+' '+cond]=[code,typ,text]
+        if code: _errorcodes[code]=cond
+del ns,errname,errpool,err,cond,code,typ,text
+
+def isResultNode(node):
+    """ Returns true if the node is a positive reply. """
+    return node and node.getType()=='result'
+def isErrorNode(node):
+    """ Returns true if the node is a negative reply. """
+    return node and node.getType()=='error'
+
+class NodeProcessed(Exception):
+    """ Exception that should be raised by handler when the handling should be stopped. """
+class StreamError(Exception):
+    """ Base exception class for stream errors."""
+class BadFormat(StreamError): pass
+class BadNamespacePrefix(StreamError): pass
+class Conflict(StreamError): pass
+class ConnectionTimeout(StreamError): pass
+class HostGone(StreamError): pass
+class HostUnknown(StreamError): pass
+class ImproperAddressing(StreamError): pass
+class InternalServerError(StreamError): pass
+class InvalidFrom(StreamError): pass
+class InvalidID(StreamError): pass
+class InvalidNamespace(StreamError): pass
+class InvalidXML(StreamError): pass
+class NotAuthorized(StreamError): pass
+class PolicyViolation(StreamError): pass
+class RemoteConnectionFailed(StreamError): pass
+class ResourceConstraint(StreamError): pass
+class RestrictedXML(StreamError): pass
+class SeeOtherHost(StreamError): pass
+class SystemShutdown(StreamError): pass
+class UndefinedCondition(StreamError): pass
+class UnsupportedEncoding(StreamError): pass
+class UnsupportedStanzaType(StreamError): pass
+class UnsupportedVersion(StreamError): pass
+class XMLNotWellFormed(StreamError): pass
+
+stream_exceptions = {'bad-format': BadFormat,
+                     'bad-namespace-prefix': BadNamespacePrefix,
+                     'conflict': Conflict,
+                     'connection-timeout': ConnectionTimeout,
+                     'host-gone': HostGone,
+                     'host-unknown': HostUnknown,
+                     'improper-addressing': ImproperAddressing,
+                     'internal-server-error': InternalServerError,
+                     'invalid-from': InvalidFrom,
+                     'invalid-id': InvalidID,
+                     'invalid-namespace': InvalidNamespace,
+                     'invalid-xml': InvalidXML,
+                     'not-authorized': NotAuthorized,
+                     'policy-violation': PolicyViolation,
+                     'remote-connection-failed': RemoteConnectionFailed,
+                     'resource-constraint': ResourceConstraint,
+                     'restricted-xml': RestrictedXML,
+                     'see-other-host': SeeOtherHost,
+                     'system-shutdown': SystemShutdown,
+                     'undefined-condition': UndefinedCondition,
+                     'unsupported-encoding': UnsupportedEncoding,
+                     'unsupported-stanza-type': UnsupportedStanzaType,
+                     'unsupported-version': UnsupportedVersion,
+                     'xml-not-well-formed': XMLNotWellFormed}
+
+class JID:
+    """ JID object. JID can be built from string, modified, compared, serialised into string. """
+    def __init__(self, jid=None, node='', domain='', resource=''):
+        """ Constructor. JID can be specified as string (jid argument) or as separate parts.
+            Examples:
+            JID('node@domain/resource')
+            JID(node='node',domain='domain.org')
+        """
+        if not jid and not domain: raise ValueError('JID must contain at least domain name')
+        elif type(jid)==type(self): self.node,self.domain,self.resource=jid.node,jid.domain,jid.resource
+        elif domain: self.node,self.domain,self.resource=node,domain,resource
+        else:
+            if jid.find('@')+1: self.node,jid=jid.split('@',1)
+            else: self.node=''
+            if jid.find('/')+1: self.domain,self.resource=jid.split('/',1)
+            else: self.domain,self.resource=jid,''
+    def getNode(self):
+        """ Return the node part of the JID """
+        return self.node
+    def setNode(self,node):
+        """ Set the node part of the JID to new value. Specify None to remove the node part."""
+        self.node=node.lower()
+    def getDomain(self):
+        """ Return the domain part of the JID """
+        return self.domain
+    def setDomain(self,domain):
+        """ Set the domain part of the JID to new value."""
+        self.domain=domain.lower()
+    def getResource(self):
+        """ Return the resource part of the JID """
+        return self.resource
+    def setResource(self,resource):
+        """ Set the resource part of the JID to new value. Specify None to remove the resource part."""
+        self.resource=resource
+    def getStripped(self):
+        """ Return the bare representation of JID. I.e. string value w/o resource. """
+        return self.__str__(0)
+    def __eq__(self, other):
+        """ Compare the JID to another instance or to string for equality. """
+        try: other=JID(other)
+        except ValueError: return 0
+        return self.resource==other.resource and self.__str__(0) == other.__str__(0)
+    def __ne__(self, other):
+        """ Compare the JID to another instance or to string for non-equality. """
+        return not self.__eq__(other)
+    def bareMatch(self, other):
+        """ Compare the node and domain parts of the JID's for equality. """
+        return self.__str__(0) == JID(other).__str__(0)
+    def __str__(self,wresource=1):
+        """ Serialise JID into string. """
+        if self.node: jid=self.node+'@'+self.domain
+        else: jid=self.domain
+        if wresource and self.resource: return jid+'/'+self.resource
+        return jid
+    def __hash__(self):
+        """ Produce hash of the JID, Allows to use JID objects as keys of the dictionary. """
+        return hash(self.__str__())
+
+class Protocol(Node):
+    """ A "stanza" object class. Contains methods that are common for presences, iqs and messages. """
+    def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None):
+        """ Constructor, name is the name of the stanza i.e. 'message' or 'presence' or 'iq'.
+            to is the value of 'to' attribure, 'typ' - 'type' attribute
+            frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition
+            timestamp - the time value that needs to be stamped over stanza
+            xmlns - namespace of top stanza node
+            node - parsed or unparsed stana to be taken as prototype.
+        """
+        if not attrs: attrs={}
+        if to: attrs['to']=to
+        if frm: attrs['from']=frm
+        if typ: attrs['type']=typ
+        Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node)
+        if not node and xmlns: self.setNamespace(xmlns)
+        if self['to']: self.setTo(self['to'])
+        if self['from']: self.setFrom(self['from'])
+        if node and type(self)==type(node) and self.__class__==node.__class__ and self.attrs.has_key('id'): del self.attrs['id']
+        self.timestamp=None
+        for x in self.getTags('x',namespace=NS_DELAY):
+            try:
+                if not self.getTimestamp() or x.getAttr('stamp')<self.getTimestamp(): self.setTimestamp(x.getAttr('stamp'))
+            except: pass
+        if timestamp is not None: self.setTimestamp(timestamp)  # To auto-timestamp stanza just pass timestamp=''
+    def getTo(self):
+        """ Return value of the 'to' attribute. """
+        try: return self['to']
+        except: return None
+    def getFrom(self):
+        """ Return value of the 'from' attribute. """
+        try: return self['from']
+        except: return None
+    def getTimestamp(self):
+        """ Return the timestamp in the 'yyyymmddThhmmss' format. """
+        return self.timestamp
+    def getID(self):
+        """ Return the value of the 'id' attribute. """
+        return self.getAttr('id')
+    def setTo(self,val):
+        """ Set the value of the 'to' attribute. """
+        self.setAttr('to', JID(val))
+    def getType(self):
+        """ Return the value of the 'type' attribute. """
+        return self.getAttr('type')
+    def setFrom(self,val):
+        """ Set the value of the 'from' attribute. """
+        self.setAttr('from', JID(val))
+    def setType(self,val):
+        """ Set the value of the 'type' attribute. """
+        self.setAttr('type', val)
+    def setID(self,val):
+        """ Set the value of the 'id' attribute. """
+        self.setAttr('id', val)
+    def getError(self):
+        """ Return the error-condition (if present) or the textual description of the error (otherwise). """
+        errtag=self.getTag('error')
+        if errtag:
+            for tag in errtag.getChildren():
+                if tag.getName()<>'text': return tag.getName()
+            return errtag.getData()
+    def getErrorCode(self):
+        """ Return the error code. Obsolette. """
+        return self.getTagAttr('error','code')
+    def setError(self,error,code=None):
+        """ Set the error code. Obsolette. Use error-conditions instead. """
+        if code:
+            if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error)
+            else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error)
+        elif type(error) in [type(''),type(u'')]: error=ErrorNode(error)
+        self.setType('error')
+        self.addChild(node=error)
+    def setTimestamp(self,val=None):
+        """Set the timestamp. timestamp should be the yyyymmddThhmmss string."""
+        if not val: val=time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())
+        self.timestamp=val
+        self.setTag('x',{'stamp':self.timestamp},namespace=NS_DELAY)
+    def getProperties(self):
+        """ Return the list of namespaces to which belongs the direct childs of element"""
+        props=[]
+        for child in self.getChildren():
+            prop=child.getNamespace()
+            if prop not in props: props.append(prop)
+        return props
+    def __setitem__(self,item,val):
+        """ Set the item 'item' to the value 'val'."""
+        if item in ['to','from']: val=JID(val)
+        return self.setAttr(item,val)
+
+class Message(Protocol):
+    """ XMPP Message stanza - "push" mechanism."""
+    def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None):
+        """ Create message object. You can specify recipient, text of message, type of message
+            any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
+            Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """
+        Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
+        if body: self.setBody(body)
+        if subject: self.setSubject(subject)
+    def getBody(self):
+        """ Returns text of the message. """
+        return self.getTagData('body')
+    def getSubject(self):
+        """ Returns subject of the message. """
+        return self.getTagData('subject')
+    def getThread(self):
+        """ Returns thread of the message. """
+        return self.getTagData('thread')
+    def setBody(self,val):
+        """ Sets the text of the message. """
+        self.setTagData('body',val)
+    def setSubject(self,val):
+        """ Sets the subject of the message. """
+        self.setTagData('subject',val)
+    def setThread(self,val):
+        """ Sets the thread of the message. """
+        self.setTagData('thread',val)
+    def buildReply(self,text=None):
+        """ Builds and returns another message object with specified text.
+            The to, from and thread properties of new message are pre-set as reply to this message. """
+        m=Message(to=self.getFrom(),frm=self.getTo(),body=text)
+        th=self.getThread()
+        if th: m.setThread(th)
+        return m
+
+class Presence(Protocol):
+    """ XMPP Presence object."""
+    def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None):
+        """ Create presence object. You can specify recipient, type of message, priority, show and status values
+            any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
+            Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as presence. """
+        Protocol.__init__(self, 'presence', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
+        if priority: self.setPriority(priority)
+        if show: self.setShow(show)
+        if status: self.setStatus(status)
+    def getPriority(self):
+        """ Returns the priority of the message. """
+        return self.getTagData('priority')
+    def getShow(self):
+        """ Returns the show value of the message. """
+        return self.getTagData('show')
+    def getStatus(self):
+        """ Returns the status string of the message. """
+        return self.getTagData('status')
+    def setPriority(self,val):
+        """ Sets the priority of the message. """
+        self.setTagData('priority',val)
+    def setShow(self,val):
+        """ Sets the show value of the message. """
+        self.setTagData('show',val)
+    def setStatus(self,val):
+        """ Sets the status string of the message. """
+        self.setTagData('status',val)
+
+    def _muc_getItemAttr(self,tag,attr):
+        for xtag in self.getTags('x'):
+            for child in xtag.getTags(tag):
+                return child.getAttr(attr)
+    def _muc_getSubTagDataAttr(self,tag,attr):
+        for xtag in self.getTags('x'):
+            for child in xtag.getTags('item'):
+                for cchild in child.getTags(tag):
+                    return cchild.getData(),cchild.getAttr(attr)
+        return None,None
+    def getRole(self):
+        """Returns the presence role (for groupchat)"""
+        return self._muc_getItemAttr('item','role')
+    def getAffiliation(self):
+        """Returns the presence affiliation (for groupchat)"""
+        return self._muc_getItemAttr('item','affiliation')
+    def getNick(self):
+        """Returns the nick value (for nick change in groupchat)"""
+        return self._muc_getItemAttr('item','nick')
+    def getJid(self):
+        """Returns the presence jid (for groupchat)"""
+        return self._muc_getItemAttr('item','jid')
+    def getReason(self):
+        """Returns the reason of the presence (for groupchat)"""
+        return self._muc_getSubTagDataAttr('reason','')[0]
+    def getActor(self):
+        """Returns the reason of the presence (for groupchat)"""
+        return self._muc_getSubTagDataAttr('actor','jid')[1]
+    def getStatusCode(self):
+        """Returns the status code of the presence (for groupchat)"""
+        return self._muc_getItemAttr('status','code')
+
+class Iq(Protocol): 
+    """ XMPP Iq object - get/set dialog mechanism. """
+    def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None):
+        """ Create Iq object. You can specify type, query namespace
+            any additional attributes, recipient of the iq, sender of the iq, any additional payload (f.e. jabber:x:data node) and namespace in one go.
+            Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as an iq. """
+        Protocol.__init__(self, 'iq', to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node)
+        if payload: self.setQueryPayload(payload)
+        if queryNS: self.setQueryNS(queryNS)
+    def getQueryNS(self):
+        """ Return the namespace of the 'query' child element."""
+        tag=self.getTag('query')
+        if tag: return tag.getNamespace()
+    def getQuerynode(self):
+        """ Return the 'node' attribute value of the 'query' child element."""
+        return self.getTagAttr('query','node')
+    def getQueryPayload(self):
+        """ Return the 'query' child element payload."""
+        tag=self.getTag('query')
+        if tag: return tag.getPayload()
+    def getQueryChildren(self):
+        """ Return the 'query' child element child nodes."""
+        tag=self.getTag('query')
+        if tag: return tag.getChildren()
+    def setQueryNS(self,namespace):
+        """ Set the namespace of the 'query' child element."""
+        self.setTag('query').setNamespace(namespace)
+    def setQueryPayload(self,payload):
+        """ Set the 'query' child element payload."""
+        self.setTag('query').setPayload(payload)
+    def setQuerynode(self,node):
+        """ Set the 'node' attribute value of the 'query' child element."""
+        self.setTagAttr('query','node',node)
+    def buildReply(self,typ):
+        """ Builds and returns another Iq object of specified type.
+            The to, from and query child node of new Iq are pre-set as reply to this Iq. """
+        iq=Iq(typ,to=self.getFrom(),frm=self.getTo(),attrs={'id':self.getID()})
+        if self.getTag('query'): iq.setQueryNS(self.getQueryNS())
+        return iq
+
+class ErrorNode(Node):
+    """ XMPP-style error element.
+        In the case of stanza error should be attached to XMPP stanza.
+        In the case of stream-level errors should be used separately. """
+    def __init__(self,name,code=None,typ=None,text=None):
+        """ Create new error node object.
+            Mandatory parameter: name - name of error condition.
+            Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol."""
+        if ERRORS.has_key(name):
+            cod,type,txt=ERRORS[name]
+            ns=name.split()[0]
+        else: cod,ns,type,txt='500',NS_STANZAS,'cancel',''
+        if typ: type=typ
+        if code: cod=code
+        if text: txt=text
+        Node.__init__(self,'error',{},[Node(name)])
+        if type: self.setAttr('type',type)
+        if not cod: self.setName('stream:error')
+        if txt: self.addChild(node=Node(ns+' text',{},[txt]))
+        if cod: self.setAttr('code',cod)
+
+class Error(Protocol):
+    """ Used to quickly transform received stanza into error reply."""
+    def __init__(self,node,error,reply=1):
+        """ Create error reply basing on the received 'node' stanza and the 'error' error condition.
+            If the 'node' is not the received stanza but locally created ('to' and 'from' fields needs not swapping)
+            specify the 'reply' argument as false."""
+        if reply: Protocol.__init__(self,to=node.getFrom(),frm=node.getTo(),node=node)
+        else: Protocol.__init__(self,node=node)
+        self.setError(error)
+        if node.getType()=='error': self.__str__=self.__dupstr__
+    def __dupstr__(self,dup1=None,dup2=None):
+        """ Dummy function used as preventor of creating error node in reply to error node.
+            I.e. you will not be able to serialise "double" error into string.
+        """
+        return ''
+
+class DataField(Node):
+    """ This class is used in the DataForm class to describe the single data item.
+        If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) 
+        then you will need to work with instances of this class. """
+    def __init__(self,name=None,value=None,typ=None,required=0,label=None,desc=None,options=[],node=None):
+        """ Create new data field of specified name,value and type.
+            Also 'required','desc' and 'options' fields can be set.
+            Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new datafiled.
+            """
+        Node.__init__(self,'field',node=node)
+        if name: self.setVar(name)
+        if type(value) in [list,tuple]: self.setValues(value)
+        elif value: self.setValue(value)
+        if typ: self.setType(typ)
+        elif not typ and not node: self.setType('text-single')
+        if required: self.setRequired(required)
+        if label: self.setLabel(label)
+        if desc: self.setDesc(desc)
+        if options: self.setOptions(options)
+    def setRequired(self,req=1):
+        """ Change the state of the 'required' flag. """
+        if req: self.setTag('required')
+        else:
+            try: self.delChild('required')
+            except ValueError: return
+    def isRequired(self):
+        """ Returns in this field a required one. """
+        return self.getTag('required')
+    def setLabel(self,label):
+        """ Set the label of this field. """
+        self.setAttr('label',label)
+    def getLabel(self):
+        """ Return the label of this field. """
+        return self.getAttr('label')
+    def setDesc(self,desc):
+        """ Set the description of this field. """
+        self.setTagData('desc',desc)
+    def getDesc(self):
+        """ Return the description of this field. """
+        return self.getTagData('desc')
+    def setValue(self,val):
+        """ Set the value of this field. """
+        self.setTagData('value',val)
+    def getValue(self):
+        return self.getTagData('value')
+    def setValues(self,lst):
+        """ Set the values of this field as values-list.
+            Replaces all previous filed values! If you need to just add a value - use addValue method."""
+        while self.getTag('value'): self.delChild('value')
+        for val in lst: self.addValue(val)
+    def addValue(self,val):
+        """ Add one more value to this field. Used in 'get' iq's or such."""
+        self.addChild('value',{},[val])
+    def getValues(self):
+        """ Return the list of values associated with this field."""
+        ret=[]
+        for tag in self.getTags('value'): ret.append(tag.getData())
+        return ret
+    def getOptions(self):
+        """ Return label-option pairs list associated with this field."""
+        ret=[]
+        for tag in self.getTags('option'): ret.append([tag.getAttr('label'),tag.getTagData('value')])
+        return ret
+    def setOptions(self,lst):
+        """ Set label-option pairs list associated with this field."""
+        while self.getTag('option'): self.delChild('option')
+        for opt in lst: self.addOption(opt)
+    def addOption(self,opt):
+        """ Add one more label-option pair to this field."""
+        if type(opt) in [str,unicode]: self.addChild('option').setTagData('value',opt)
+        else: self.addChild('option',{'label':opt[0]}).setTagData('value',opt[1])
+    def getType(self):
+        """ Get type of this field. """
+        return self.getAttr('type')
+    def setType(self,val):
+        """ Set type of this field. """
+        return self.setAttr('type',val)
+    def getVar(self):
+        """ Get 'var' attribute value of this field. """
+        return self.getAttr('var')
+    def setVar(self,val):
+        """ Set 'var' attribute value of this field. """
+        return self.setAttr('var',val)
+
+class DataReported(Node):
+    """ This class is used in the DataForm class to describe the 'reported data field' data items which are used in
+        'multiple item form results' (as described in XEP-0004).
+        Represents the fields that will be returned from a search. This information is useful when
+        you try to use the jabber:iq:search namespace to return dynamic form information.
+        """
+    def __init__(self,node=None):
+        """ Create new empty 'reported data' field. However, note that, according XEP-0004:
+            * It MUST contain one or more DataFields.
+            * Contained DataFields SHOULD possess a 'type' and 'label' attribute in addition to 'var' attribute
+            * Contained DataFields SHOULD NOT contain a <value/> element.
+            Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new
+            dataitem.
+        """
+        Node.__init__(self,'reported',node=node)
+        if node:
+            newkids=[]
+            for n in self.getChildren():
+                if    n.getName()=='field': newkids.append(DataField(node=n))
+                else: newkids.append(n)
+            self.kids=newkids
+    def getField(self,name):
+        """ Return the datafield object with name 'name' (if exists). """
+        return self.getTag('field',attrs={'var':name})
+    def setField(self,name,typ=None,label=None):
+        """ Create if nessessary or get the existing datafield object with name 'name' and return it.
+            If created, attributes 'type' and 'label' are applied to new datafield."""
+        f=self.getField(name)
+        if f: return f
+        return self.addChild(node=DataField(name,None,typ,0,label))
+    def asDict(self):
+        """ Represent dataitem as simple dictionary mapping of datafield names to their values."""
+        ret={}
+        for field in self.getTags('field'):
+            name=field.getAttr('var')
+            typ=field.getType()
+            if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi':
+                val=[]
+                for i in field.getTags('value'): val.append(i.getData())
+            else: val=field.getTagData('value')
+            ret[name]=val
+        if self.getTag('instructions'): ret['instructions']=self.getInstructions()
+        return ret
+    def __getitem__(self,name):
+        """ Simple dictionary interface for getting datafields values by their names."""
+        item=self.getField(name)
+        if item: return item.getValue()
+        raise IndexError('No such field')
+    def __setitem__(self,name,val):
+        """ Simple dictionary interface for setting datafields values by their names."""
+        return self.setField(name).setValue(val)
+
+class DataItem(Node):
+    """ This class is used in the DataForm class to describe data items which are used in 'multiple
+        item form results' (as described in XEP-0004).
+        """
+    def __init__(self,node=None):
+        """ Create new empty data item. However, note that, according XEP-0004, DataItem MUST contain ALL
+            DataFields described in DataReported.
+            Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new
+            dataitem.
+            """
+        Node.__init__(self,'item',node=node)
+        if node:
+            newkids=[]
+            for n in self.getChildren():
+                if    n.getName()=='field': newkids.append(DataField(node=n))
+                else: newkids.append(n)
+            self.kids=newkids
+    def getField(self,name):
+        """ Return the datafield object with name 'name' (if exists). """
+        return self.getTag('field',attrs={'var':name})
+    def setField(self,name):
+        """ Create if nessessary or get the existing datafield object with name 'name' and return it. """
+        f=self.getField(name)
+        if f: return f
+        return self.addChild(node=DataField(name))
+    def asDict(self):
+        """ Represent dataitem as simple dictionary mapping of datafield names to their values."""
+        ret={}
+        for field in self.getTags('field'):
+            name=field.getAttr('var')
+            typ=field.getType()
+            if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi':
+                val=[]
+                for i in field.getTags('value'): val.append(i.getData())
+            else: val=field.getTagData('value')
+            ret[name]=val
+        if self.getTag('instructions'): ret['instructions']=self.getInstructions()
+        return ret
+    def __getitem__(self,name):
+        """ Simple dictionary interface for getting datafields values by their names."""
+        item=self.getField(name)
+        if item: return item.getValue()
+        raise IndexError('No such field')
+    def __setitem__(self,name,val):
+        """ Simple dictionary interface for setting datafields values by their names."""
+        return self.setField(name).setValue(val)
+
+class DataForm(Node):
+    """ DataForm class. Used for manipulating dataforms in XMPP.
+        Relevant XEPs: 0004, 0068, 0122.
+        Can be used in disco, pub-sub and many other applications."""
+    def __init__(self, typ=None, data=[], title=None, node=None):
+        """
+            Create new dataform of type 'typ'; 'data' is the list of DataReported,
+            DataItem and DataField instances that this dataform contains; 'title'
+            is the title string.
+            You can specify the 'node' argument as the other node to be used as
+            base for constructing this dataform.
+
+            title and instructions is optional and SHOULD NOT contain newlines.
+            Several instructions MAY be present.
+            'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' )
+            'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) respectively.
+            'cancel' form can not contain any fields. All other forms contains AT LEAST one field.
+            'title' MAY be included in forms of type "form" and "result"
+        """
+        Node.__init__(self,'x',node=node)
+        if node:
+            newkids=[]
+            for n in self.getChildren():
+                if   n.getName()=='field': newkids.append(DataField(node=n))
+                elif n.getName()=='item': newkids.append(DataItem(node=n))
+                elif n.getName()=='reported': newkids.append(DataReported(node=n))
+                else: newkids.append(n)
+            self.kids=newkids
+        if typ: self.setType(typ)
+        self.setNamespace(NS_DATA)
+        if title: self.setTitle(title)
+        if type(data)==type({}):
+            newdata=[]
+            for name in data.keys(): newdata.append(DataField(name,data[name]))
+            data=newdata
+        for child in data:
+            if type(child) in [type(''),type(u'')]: self.addInstructions(child)
+            elif child.__class__.__name__=='DataField': self.kids.append(child)
+            elif child.__class__.__name__=='DataItem': self.kids.append(child)
+            elif child.__class__.__name__=='DataReported': self.kids.append(child)
+            else: self.kids.append(DataField(node=child))
+    def getType(self):
+        """ Return the type of dataform. """
+        return self.getAttr('type')
+    def setType(self,typ):
+        """ Set the type of dataform. """
+        self.setAttr('type',typ)
+    def getTitle(self):
+        """ Return the title of dataform. """
+        return self.getTagData('title')
+    def setTitle(self,text):
+        """ Set the title of dataform. """
+        self.setTagData('title',text)
+    def getInstructions(self):
+        """ Return the instructions of dataform. """
+        return self.getTagData('instructions')
+    def setInstructions(self,text):
+        """ Set the instructions of dataform. """
+        self.setTagData('instructions',text)
+    def addInstructions(self,text):
+        """ Add one more instruction to the dataform. """
+        self.addChild('instructions',{},[text])
+    def getField(self,name):
+        """ Return the datafield object with name 'name' (if exists). """
+        return self.getTag('field',attrs={'var':name})
+    def setField(self,name):
+        """ Create if nessessary or get the existing datafield object with name 'name' and return it. """
+        f=self.getField(name)
+        if f: return f
+        return self.addChild(node=DataField(name))
+    def asDict(self):
+        """ Represent dataform as simple dictionary mapping of datafield names to their values."""
+        ret={}
+        for field in self.getTags('field'):
+            name=field.getAttr('var')
+            typ=field.getType()
+            if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi':
+                val=[]
+                for i in field.getTags('value'): val.append(i.getData())
+            else: val=field.getTagData('value')
+            ret[name]=val
+        if self.getTag('instructions'): ret['instructions']=self.getInstructions()
+        return ret
+    def __getitem__(self,name):
+        """ Simple dictionary interface for getting datafields values by their names."""
+        item=self.getField(name)
+        if item: return item.getValue()
+        raise IndexError('No such field')
+    def __setitem__(self,name,val):
+        """ Simple dictionary interface for setting datafields values by their names."""
+        return self.setField(name).setValue(val)

UCC git Repository :: git.ucc.asn.au