123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877 |
- ## 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 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 'id' in self.attrs: 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 list(_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('')]: 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, thread and type properties of new message are pre-set as reply to this message. """
- m=Message(to=self.getFrom(),frm=self.getTo(),body=text,typ=self.getType())
- 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 getQuery(self):
- """ Return the IQ's child element if it exists, None otherwise."""
- typ = self.getType()
- query=None
- for child in self.getChildren():
- if ('error' != typ) or ('error' != child.getName()):
- query=child
- break
- return query
- def getQueryNS(self):
- """ Return the namespace of the 'query' child element."""
- tag=self.getQuery()
- if tag: return tag.getNamespace()
- def getQuerynode(self):
- """ Return the 'node' attribute value of the 'query' child element."""
- return self.getQuery().getAttr('node')
- def getQueryPayload(self):
- """ Return the 'query' child element payload."""
- tag=self.getQuery()
- if tag: return tag.getPayload()
- def getQueryChildren(self):
- """ Return the 'query' child element child nodes."""
- tag=self.getQuery()
- if tag: return tag.getChildren()
- def setQuery(self, name=None):
- """ Change the name of the query node, creating it if needed. Keep the existing name if none is given (use 'query' if it's a creation). Return the query node."""
- query=self.getQuery()
- if query is None:
- query=self.addChild('query')
- if name is not None:
- query.setName(name)
- return query
- def setQueryNS(self,namespace):
- """ Set the namespace of the 'query' child element."""
- self.setQuery().setNamespace(namespace)
- def setQueryPayload(self,payload):
- """ Set the 'query' child element payload."""
- self.setQuery().setPayload(payload)
- def setQuerynode(self,node):
- """ Set the 'node' attribute value of the 'query' child element."""
- self.setQuery().setAttr('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()})
- iq.setQuery(self.getQuery().getName()).setNamespace(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 XMPP protocol."""
- if name in ERRORS:
- 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,str]: 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) 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) 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 list(data.keys()): newdata.append(DataField(name,data[name]))
- data=newdata
- for child in data:
- if type(child) in [type(''),type('')]: 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) 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)
|