3 ## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
5 ## This program is free software; you can redistribute it and/or modify
6 ## it under the terms of the GNU General Public License as published by
7 ## the Free Software Foundation; either version 2, or (at your option)
10 ## This program is distributed in the hope that it will be useful,
11 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 ## GNU General Public License for more details.
15 # $Id: protocol.py,v 1.60 2009/04/07 11:14:28 snakeru Exp $
18 Protocol module contains tools that is needed for processing of
19 xmpp-related data structures.
22 from simplexml import Node,ustr
24 NS_ACTIVITY ='http://jabber.org/protocol/activity' # XEP-0108
25 NS_ADDRESS ='http://jabber.org/protocol/address' # XEP-0033
26 NS_ADMIN ='http://jabber.org/protocol/admin' # XEP-0133
27 NS_ADMIN_ADD_USER =NS_ADMIN+'#add-user' # XEP-0133
28 NS_ADMIN_DELETE_USER =NS_ADMIN+'#delete-user' # XEP-0133
29 NS_ADMIN_DISABLE_USER =NS_ADMIN+'#disable-user' # XEP-0133
30 NS_ADMIN_REENABLE_USER =NS_ADMIN+'#reenable-user' # XEP-0133
31 NS_ADMIN_END_USER_SESSION =NS_ADMIN+'#end-user-session' # XEP-0133
32 NS_ADMIN_GET_USER_PASSWORD =NS_ADMIN+'#get-user-password' # XEP-0133
33 NS_ADMIN_CHANGE_USER_PASSWORD =NS_ADMIN+'#change-user-password' # XEP-0133
34 NS_ADMIN_GET_USER_ROSTER =NS_ADMIN+'#get-user-roster' # XEP-0133
35 NS_ADMIN_GET_USER_LASTLOGIN =NS_ADMIN+'#get-user-lastlogin' # XEP-0133
36 NS_ADMIN_USER_STATS =NS_ADMIN+'#user-stats' # XEP-0133
37 NS_ADMIN_EDIT_BLACKLIST =NS_ADMIN+'#edit-blacklist' # XEP-0133
38 NS_ADMIN_EDIT_WHITELIST =NS_ADMIN+'#edit-whitelist' # XEP-0133
39 NS_ADMIN_REGISTERED_USERS_NUM =NS_ADMIN+'#get-registered-users-num' # XEP-0133
40 NS_ADMIN_DISABLED_USERS_NUM =NS_ADMIN+'#get-disabled-users-num' # XEP-0133
41 NS_ADMIN_ONLINE_USERS_NUM =NS_ADMIN+'#get-online-users-num' # XEP-0133
42 NS_ADMIN_ACTIVE_USERS_NUM =NS_ADMIN+'#get-active-users-num' # XEP-0133
43 NS_ADMIN_IDLE_USERS_NUM =NS_ADMIN+'#get-idle-users-num' # XEP-0133
44 NS_ADMIN_REGISTERED_USERS_LIST =NS_ADMIN+'#get-registered-users-list' # XEP-0133
45 NS_ADMIN_DISABLED_USERS_LIST =NS_ADMIN+'#get-disabled-users-list' # XEP-0133
46 NS_ADMIN_ONLINE_USERS_LIST =NS_ADMIN+'#get-online-users-list' # XEP-0133
47 NS_ADMIN_ACTIVE_USERS_LIST =NS_ADMIN+'#get-active-users-list' # XEP-0133
48 NS_ADMIN_IDLE_USERS_LIST =NS_ADMIN+'#get-idle-users-list' # XEP-0133
49 NS_ADMIN_ANNOUNCE =NS_ADMIN+'#announce' # XEP-0133
50 NS_ADMIN_SET_MOTD =NS_ADMIN+'#set-motd' # XEP-0133
51 NS_ADMIN_EDIT_MOTD =NS_ADMIN+'#edit-motd' # XEP-0133
52 NS_ADMIN_DELETE_MOTD =NS_ADMIN+'#delete-motd' # XEP-0133
53 NS_ADMIN_SET_WELCOME =NS_ADMIN+'#set-welcome' # XEP-0133
54 NS_ADMIN_DELETE_WELCOME =NS_ADMIN+'#delete-welcome' # XEP-0133
55 NS_ADMIN_EDIT_ADMIN =NS_ADMIN+'#edit-admin' # XEP-0133
56 NS_ADMIN_RESTART =NS_ADMIN+'#restart' # XEP-0133
57 NS_ADMIN_SHUTDOWN =NS_ADMIN+'#shutdown' # XEP-0133
58 NS_AGENTS ='jabber:iq:agents' # XEP-0094 (historical)
59 NS_AMP ='http://jabber.org/protocol/amp' # XEP-0079
60 NS_AMP_ERRORS =NS_AMP+'#errors' # XEP-0079
61 NS_AUTH ='jabber:iq:auth' # XEP-0078
62 NS_AVATAR ='jabber:iq:avatar' # XEP-0008 (historical)
63 NS_BIND ='urn:ietf:params:xml:ns:xmpp-bind' # RFC 3920
64 NS_BROWSE ='jabber:iq:browse' # XEP-0011 (historical)
65 NS_BYTESTREAM ='http://jabber.org/protocol/bytestreams' # XEP-0065
66 NS_CAPS ='http://jabber.org/protocol/caps' # XEP-0115
67 NS_CHATSTATES ='http://jabber.org/protocol/chatstates' # XEP-0085
68 NS_CLIENT ='jabber:client' # RFC 3921
69 NS_COMMANDS ='http://jabber.org/protocol/commands' # XEP-0050
70 NS_COMPONENT_ACCEPT ='jabber:component:accept' # XEP-0114
71 NS_COMPONENT_1 ='http://jabberd.jabberstudio.org/ns/component/1.0' # Jabberd2
72 NS_COMPRESS ='http://jabber.org/protocol/compress' # XEP-0138
73 NS_DATA ='jabber:x:data' # XEP-0004
74 NS_DATA_LAYOUT ='http://jabber.org/protocol/xdata-layout' # XEP-0141
75 NS_DATA_VALIDATE ='http://jabber.org/protocol/xdata-validate' # XEP-0122
76 NS_DELAY ='jabber:x:delay' # XEP-0091 (deprecated)
77 NS_DIALBACK ='jabber:server:dialback' # RFC 3921
78 NS_DISCO ='http://jabber.org/protocol/disco' # XEP-0030
79 NS_DISCO_INFO =NS_DISCO+'#info' # XEP-0030
80 NS_DISCO_ITEMS =NS_DISCO+'#items' # XEP-0030
81 NS_ENCRYPTED ='jabber:x:encrypted' # XEP-0027
82 NS_EVENT ='jabber:x:event' # XEP-0022 (deprecated)
83 NS_FEATURE ='http://jabber.org/protocol/feature-neg' # XEP-0020
84 NS_FILE ='http://jabber.org/protocol/si/profile/file-transfer' # XEP-0096
85 NS_GATEWAY ='jabber:iq:gateway' # XEP-0100
86 NS_GEOLOC ='http://jabber.org/protocol/geoloc' # XEP-0080
87 NS_GROUPCHAT ='gc-1.0' # XEP-0045
88 NS_HTTP_BIND ='http://jabber.org/protocol/httpbind' # XEP-0124
89 NS_IBB ='http://jabber.org/protocol/ibb' # XEP-0047
90 NS_INVISIBLE ='presence-invisible' # Jabberd2
91 NS_IQ ='iq' # Jabberd2
92 NS_LAST ='jabber:iq:last' # XEP-0012
93 NS_MESSAGE ='message' # Jabberd2
94 NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107
95 NS_MUC ='http://jabber.org/protocol/muc' # XEP-0045
96 NS_MUC_ADMIN =NS_MUC+'#admin' # XEP-0045
97 NS_MUC_OWNER =NS_MUC+'#owner' # XEP-0045
98 NS_MUC_UNIQUE =NS_MUC+'#unique' # XEP-0045
99 NS_MUC_USER =NS_MUC+'#user' # XEP-0045
100 NS_MUC_REGISTER =NS_MUC+'#register' # XEP-0045
101 NS_MUC_REQUEST =NS_MUC+'#request' # XEP-0045
102 NS_MUC_ROOMCONFIG =NS_MUC+'#roomconfig' # XEP-0045
103 NS_MUC_ROOMINFO =NS_MUC+'#roominfo' # XEP-0045
104 NS_MUC_ROOMS =NS_MUC+'#rooms' # XEP-0045
105 NS_MUC_TRAFIC =NS_MUC+'#traffic' # XEP-0045
106 NS_NICK ='http://jabber.org/protocol/nick' # XEP-0172
107 NS_OFFLINE ='http://jabber.org/protocol/offline' # XEP-0013
108 NS_PHYSLOC ='http://jabber.org/protocol/physloc' # XEP-0112
109 NS_PRESENCE ='presence' # Jabberd2
110 NS_PRIVACY ='jabber:iq:privacy' # RFC 3921
111 NS_PRIVATE ='jabber:iq:private' # XEP-0049
112 NS_PUBSUB ='http://jabber.org/protocol/pubsub' # XEP-0060
113 NS_REGISTER ='jabber:iq:register' # XEP-0077
114 NS_RC ='http://jabber.org/protocol/rc' # XEP-0146
115 NS_ROSTER ='jabber:iq:roster' # RFC 3921
116 NS_ROSTERX ='http://jabber.org/protocol/rosterx' # XEP-0144
117 NS_RPC ='jabber:iq:rpc' # XEP-0009
118 NS_SASL ='urn:ietf:params:xml:ns:xmpp-sasl' # RFC 3920
119 NS_SEARCH ='jabber:iq:search' # XEP-0055
120 NS_SERVER ='jabber:server' # RFC 3921
121 NS_SESSION ='urn:ietf:params:xml:ns:xmpp-session' # RFC 3921
122 NS_SI ='http://jabber.org/protocol/si' # XEP-0096
123 NS_SI_PUB ='http://jabber.org/protocol/sipub' # XEP-0137
124 NS_SIGNED ='jabber:x:signed' # XEP-0027
125 NS_STANZAS ='urn:ietf:params:xml:ns:xmpp-stanzas' # RFC 3920
126 NS_STREAMS ='http://etherx.jabber.org/streams' # RFC 3920
127 NS_TIME ='jabber:iq:time' # XEP-0090 (deprecated)
128 NS_TLS ='urn:ietf:params:xml:ns:xmpp-tls' # RFC 3920
129 NS_VACATION ='http://jabber.org/protocol/vacation' # XEP-0109
130 NS_VCARD ='vcard-temp' # XEP-0054
131 NS_VCARD_UPDATE ='vcard-temp:x:update' # XEP-0153
132 NS_VERSION ='jabber:iq:version' # XEP-0092
133 NS_WAITINGLIST ='http://jabber.org/protocol/waitinglist' # XEP-0130
134 NS_XHTML_IM ='http://jabber.org/protocol/xhtml-im' # XEP-0071
135 NS_XMPP_STREAMS ='urn:ietf:params:xml:ns:xmpp-streams' # RFC 3920
137 xmpp_stream_error_conditions="""
138 bad-format -- -- -- The entity has sent XML that cannot be processed.
139 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.
140 conflict -- -- -- The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream.
141 connection-timeout -- -- -- The entity has not generated any traffic over the stream for some period of time.
142 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.
143 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.
144 improper-addressing -- -- -- A stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value).
145 internal-server-error -- -- -- The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.
146 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.
147 invalid-id -- -- -- The stream ID or dialback ID is invalid or does not match an ID previously provided.
148 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".
149 invalid-xml -- -- -- The entity has sent invalid XML over the stream to a server that performs validation.
150 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.
151 policy-violation -- -- -- The entity has violated some local service policy.
152 remote-connection-failed -- -- -- The server is unable to properly connect to a remote resource that is required for authentication or authorization.
153 resource-constraint -- -- -- The server lacks the system resources necessary to service the stream.
154 restricted-xml -- -- -- The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character.
155 see-other-host -- -- -- The server will not provide service to the initiating entity but is redirecting traffic to another host.
156 system-shutdown -- -- -- The server is being shut down and all active streams are being closed.
157 undefined-condition -- -- -- The error condition is not one of those defined by the other conditions in this list.
158 unsupported-encoding -- -- -- The initiating entity has encoded the stream in an encoding that is not supported by the server.
159 unsupported-stanza-type -- -- -- The initiating entity has sent a first-level child of the stream that is not supported by the server.
160 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.
161 xml-not-well-formed -- -- -- The initiating entity has sent XML that is not well-formed."""
162 xmpp_stanza_error_conditions="""
163 bad-request -- 400 -- modify -- The sender has sent XML that is malformed or that cannot be processed.
164 conflict -- 409 -- cancel -- Access cannot be granted because an existing resource or session exists with the same name or address.
165 feature-not-implemented -- 501 -- cancel -- The feature requested is not implemented by the recipient or server and therefore cannot be processed.
166 forbidden -- 403 -- auth -- The requesting entity does not possess the required permissions to perform the action.
167 gone -- 302 -- modify -- The recipient or server can no longer be contacted at this address.
168 internal-server-error -- 500 -- wait -- The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error.
169 item-not-found -- 404 -- cancel -- The addressed JID or item requested cannot be found.
170 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.
171 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.
172 not-allowed -- 405 -- cancel -- The recipient or server does not allow any entity to perform the action.
173 not-authorized -- 401 -- auth -- The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials.
174 payment-required -- 402 -- auth -- The requesting entity is not authorized to access the requested service because payment is required.
175 recipient-unavailable -- 404 -- wait -- The intended recipient is temporarily unavailable.
176 redirect -- 302 -- modify -- The recipient or server is redirecting requests for this information to another entity.
177 registration-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because registration is required.
178 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.
179 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.
180 resource-constraint -- 500 -- wait -- The server or recipient lacks the system resources necessary to service the request.
181 service-unavailable -- 503 -- cancel -- The server or recipient does not currently provide the requested service.
182 subscription-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because a subscription is required.
183 undefined-condition -- 500 -- --
184 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)."""
185 sasl_error_conditions="""
186 aborted -- -- -- The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element.
187 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.
188 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.
189 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.
190 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.
191 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.
192 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."""
194 ERRORS,_errorcodes={},{}
195 for ns,errname,errpool in [(NS_XMPP_STREAMS,'STREAM',xmpp_stream_error_conditions),
196 (NS_STANZAS ,'ERR' ,xmpp_stanza_error_conditions),
197 (NS_SASL ,'SASL' ,sasl_error_conditions)]:
198 for err in errpool.split('\n')[1:]:
199 cond,code,typ,text=err.split(' -- ')
200 name=errname+'_'+cond.upper().replace('-','_')
201 locals()[name]=ns+' '+cond
202 ERRORS[ns+' '+cond]=[code,typ,text]
203 if code: _errorcodes[code]=cond
204 del ns,errname,errpool,err,cond,code,typ,text
206 def isResultNode(node):
207 """ Returns true if the node is a positive reply. """
208 return node and node.getType()=='result'
209 def isErrorNode(node):
210 """ Returns true if the node is a negative reply. """
211 return node and node.getType()=='error'
213 class NodeProcessed(Exception):
214 """ Exception that should be raised by handler when the handling should be stopped. """
215 class StreamError(Exception):
216 """ Base exception class for stream errors."""
217 class BadFormat(StreamError): pass
218 class BadNamespacePrefix(StreamError): pass
219 class Conflict(StreamError): pass
220 class ConnectionTimeout(StreamError): pass
221 class HostGone(StreamError): pass
222 class HostUnknown(StreamError): pass
223 class ImproperAddressing(StreamError): pass
224 class InternalServerError(StreamError): pass
225 class InvalidFrom(StreamError): pass
226 class InvalidID(StreamError): pass
227 class InvalidNamespace(StreamError): pass
228 class InvalidXML(StreamError): pass
229 class NotAuthorized(StreamError): pass
230 class PolicyViolation(StreamError): pass
231 class RemoteConnectionFailed(StreamError): pass
232 class ResourceConstraint(StreamError): pass
233 class RestrictedXML(StreamError): pass
234 class SeeOtherHost(StreamError): pass
235 class SystemShutdown(StreamError): pass
236 class UndefinedCondition(StreamError): pass
237 class UnsupportedEncoding(StreamError): pass
238 class UnsupportedStanzaType(StreamError): pass
239 class UnsupportedVersion(StreamError): pass
240 class XMLNotWellFormed(StreamError): pass
242 stream_exceptions = {'bad-format': BadFormat,
243 'bad-namespace-prefix': BadNamespacePrefix,
244 'conflict': Conflict,
245 'connection-timeout': ConnectionTimeout,
246 'host-gone': HostGone,
247 'host-unknown': HostUnknown,
248 'improper-addressing': ImproperAddressing,
249 'internal-server-error': InternalServerError,
250 'invalid-from': InvalidFrom,
251 'invalid-id': InvalidID,
252 'invalid-namespace': InvalidNamespace,
253 'invalid-xml': InvalidXML,
254 'not-authorized': NotAuthorized,
255 'policy-violation': PolicyViolation,
256 'remote-connection-failed': RemoteConnectionFailed,
257 'resource-constraint': ResourceConstraint,
258 'restricted-xml': RestrictedXML,
259 'see-other-host': SeeOtherHost,
260 'system-shutdown': SystemShutdown,
261 'undefined-condition': UndefinedCondition,
262 'unsupported-encoding': UnsupportedEncoding,
263 'unsupported-stanza-type': UnsupportedStanzaType,
264 'unsupported-version': UnsupportedVersion,
265 'xml-not-well-formed': XMLNotWellFormed}
268 """ JID object. JID can be built from string, modified, compared, serialised into string. """
269 def __init__(self, jid=None, node='', domain='', resource=''):
270 """ Constructor. JID can be specified as string (jid argument) or as separate parts.
272 JID('node@domain/resource')
273 JID(node='node',domain='domain.org')
275 if not jid and not domain: raise ValueError('JID must contain at least domain name')
276 elif type(jid)==type(self): self.node,self.domain,self.resource=jid.node,jid.domain,jid.resource
277 elif domain: self.node,self.domain,self.resource=node,domain,resource
279 if jid.find('@')+1: self.node,jid=jid.split('@',1)
281 if jid.find('/')+1: self.domain,self.resource=jid.split('/',1)
282 else: self.domain,self.resource=jid,''
284 """ Return the node part of the JID """
286 def setNode(self,node):
287 """ Set the node part of the JID to new value. Specify None to remove the node part."""
288 self.node=node.lower()
290 """ Return the domain part of the JID """
292 def setDomain(self,domain):
293 """ Set the domain part of the JID to new value."""
294 self.domain=domain.lower()
295 def getResource(self):
296 """ Return the resource part of the JID """
298 def setResource(self,resource):
299 """ Set the resource part of the JID to new value. Specify None to remove the resource part."""
300 self.resource=resource
301 def getStripped(self):
302 """ Return the bare representation of JID. I.e. string value w/o resource. """
303 return self.__str__(0)
304 def __eq__(self, other):
305 """ Compare the JID to another instance or to string for equality. """
306 try: other=JID(other)
307 except ValueError: return 0
308 return self.resource==other.resource and self.__str__(0) == other.__str__(0)
309 def __ne__(self, other):
310 """ Compare the JID to another instance or to string for non-equality. """
311 return not self.__eq__(other)
312 def bareMatch(self, other):
313 """ Compare the node and domain parts of the JID's for equality. """
314 return self.__str__(0) == JID(other).__str__(0)
315 def __str__(self,wresource=1):
316 """ Serialise JID into string. """
317 if self.node: jid=self.node+'@'+self.domain
318 else: jid=self.domain
319 if wresource and self.resource: return jid+'/'+self.resource
322 """ Produce hash of the JID, Allows to use JID objects as keys of the dictionary. """
323 return hash(self.__str__())
325 class Protocol(Node):
326 """ A "stanza" object class. Contains methods that are common for presences, iqs and messages. """
327 def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None):
328 """ Constructor, name is the name of the stanza i.e. 'message' or 'presence' or 'iq'.
329 to is the value of 'to' attribure, 'typ' - 'type' attribute
330 frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition
331 timestamp - the time value that needs to be stamped over stanza
332 xmlns - namespace of top stanza node
333 node - parsed or unparsed stana to be taken as prototype.
335 if not attrs: attrs={}
336 if to: attrs['to']=to
337 if frm: attrs['from']=frm
338 if typ: attrs['type']=typ
339 Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node)
340 if not node and xmlns: self.setNamespace(xmlns)
341 if self['to']: self.setTo(self['to'])
342 if self['from']: self.setFrom(self['from'])
343 if node and type(self)==type(node) and self.__class__==node.__class__ and self.attrs.has_key('id'): del self.attrs['id']
345 for x in self.getTags('x',namespace=NS_DELAY):
347 if not self.getTimestamp() or x.getAttr('stamp')<self.getTimestamp(): self.setTimestamp(x.getAttr('stamp'))
349 if timestamp is not None: self.setTimestamp(timestamp) # To auto-timestamp stanza just pass timestamp=''
351 """ Return value of the 'to' attribute. """
352 try: return self['to']
355 """ Return value of the 'from' attribute. """
356 try: return self['from']
358 def getTimestamp(self):
359 """ Return the timestamp in the 'yyyymmddThhmmss' format. """
360 return self.timestamp
362 """ Return the value of the 'id' attribute. """
363 return self.getAttr('id')
365 """ Set the value of the 'to' attribute. """
366 self.setAttr('to', JID(val))
368 """ Return the value of the 'type' attribute. """
369 return self.getAttr('type')
370 def setFrom(self,val):
371 """ Set the value of the 'from' attribute. """
372 self.setAttr('from', JID(val))
373 def setType(self,val):
374 """ Set the value of the 'type' attribute. """
375 self.setAttr('type', val)
377 """ Set the value of the 'id' attribute. """
378 self.setAttr('id', val)
380 """ Return the error-condition (if present) or the textual description of the error (otherwise). """
381 errtag=self.getTag('error')
383 for tag in errtag.getChildren():
384 if tag.getName()<>'text': return tag.getName()
385 return errtag.getData()
386 def getErrorCode(self):
387 """ Return the error code. Obsolette. """
388 return self.getTagAttr('error','code')
389 def setError(self,error,code=None):
390 """ Set the error code. Obsolette. Use error-conditions instead. """
392 if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error)
393 else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error)
394 elif type(error) in [type(''),type(u'')]: error=ErrorNode(error)
395 self.setType('error')
396 self.addChild(node=error)
397 def setTimestamp(self,val=None):
398 """Set the timestamp. timestamp should be the yyyymmddThhmmss string."""
399 if not val: val=time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())
401 self.setTag('x',{'stamp':self.timestamp},namespace=NS_DELAY)
402 def getProperties(self):
403 """ Return the list of namespaces to which belongs the direct childs of element"""
405 for child in self.getChildren():
406 prop=child.getNamespace()
407 if prop not in props: props.append(prop)
409 def __setitem__(self,item,val):
410 """ Set the item 'item' to the value 'val'."""
411 if item in ['to','from']: val=JID(val)
412 return self.setAttr(item,val)
414 class Message(Protocol):
415 """ XMPP Message stanza - "push" mechanism."""
416 def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None):
417 """ Create message object. You can specify recipient, text of message, type of message
418 any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
419 Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """
420 Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
421 if body: self.setBody(body)
422 if subject: self.setSubject(subject)
424 """ Returns text of the message. """
425 return self.getTagData('body')
426 def getSubject(self):
427 """ Returns subject of the message. """
428 return self.getTagData('subject')
430 """ Returns thread of the message. """
431 return self.getTagData('thread')
432 def setBody(self,val):
433 """ Sets the text of the message. """
434 self.setTagData('body',val)
435 def setSubject(self,val):
436 """ Sets the subject of the message. """
437 self.setTagData('subject',val)
438 def setThread(self,val):
439 """ Sets the thread of the message. """
440 self.setTagData('thread',val)
441 def buildReply(self,text=None):
442 """ Builds and returns another message object with specified text.
443 The to, from and thread properties of new message are pre-set as reply to this message. """
444 m=Message(to=self.getFrom(),frm=self.getTo(),body=text)
446 if th: m.setThread(th)
449 class Presence(Protocol):
450 """ XMPP Presence object."""
451 def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None):
452 """ Create presence object. You can specify recipient, type of message, priority, show and status values
453 any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
454 Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as presence. """
455 Protocol.__init__(self, 'presence', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
456 if priority: self.setPriority(priority)
457 if show: self.setShow(show)
458 if status: self.setStatus(status)
459 def getPriority(self):
460 """ Returns the priority of the message. """
461 return self.getTagData('priority')
463 """ Returns the show value of the message. """
464 return self.getTagData('show')
466 """ Returns the status string of the message. """
467 return self.getTagData('status')
468 def setPriority(self,val):
469 """ Sets the priority of the message. """
470 self.setTagData('priority',val)
471 def setShow(self,val):
472 """ Sets the show value of the message. """
473 self.setTagData('show',val)
474 def setStatus(self,val):
475 """ Sets the status string of the message. """
476 self.setTagData('status',val)
478 def _muc_getItemAttr(self,tag,attr):
479 for xtag in self.getTags('x'):
480 for child in xtag.getTags(tag):
481 return child.getAttr(attr)
482 def _muc_getSubTagDataAttr(self,tag,attr):
483 for xtag in self.getTags('x'):
484 for child in xtag.getTags('item'):
485 for cchild in child.getTags(tag):
486 return cchild.getData(),cchild.getAttr(attr)
489 """Returns the presence role (for groupchat)"""
490 return self._muc_getItemAttr('item','role')
491 def getAffiliation(self):
492 """Returns the presence affiliation (for groupchat)"""
493 return self._muc_getItemAttr('item','affiliation')
495 """Returns the nick value (for nick change in groupchat)"""
496 return self._muc_getItemAttr('item','nick')
498 """Returns the presence jid (for groupchat)"""
499 return self._muc_getItemAttr('item','jid')
501 """Returns the reason of the presence (for groupchat)"""
502 return self._muc_getSubTagDataAttr('reason','')[0]
504 """Returns the reason of the presence (for groupchat)"""
505 return self._muc_getSubTagDataAttr('actor','jid')[1]
506 def getStatusCode(self):
507 """Returns the status code of the presence (for groupchat)"""
508 return self._muc_getItemAttr('status','code')
511 """ XMPP Iq object - get/set dialog mechanism. """
512 def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None):
513 """ Create Iq object. You can specify type, query namespace
514 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.
515 Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as an iq. """
516 Protocol.__init__(self, 'iq', to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node)
517 if payload: self.setQueryPayload(payload)
518 if queryNS: self.setQueryNS(queryNS)
519 def getQueryNS(self):
520 """ Return the namespace of the 'query' child element."""
521 tag=self.getTag('query')
522 if tag: return tag.getNamespace()
523 def getQuerynode(self):
524 """ Return the 'node' attribute value of the 'query' child element."""
525 return self.getTagAttr('query','node')
526 def getQueryPayload(self):
527 """ Return the 'query' child element payload."""
528 tag=self.getTag('query')
529 if tag: return tag.getPayload()
530 def getQueryChildren(self):
531 """ Return the 'query' child element child nodes."""
532 tag=self.getTag('query')
533 if tag: return tag.getChildren()
534 def setQueryNS(self,namespace):
535 """ Set the namespace of the 'query' child element."""
536 self.setTag('query').setNamespace(namespace)
537 def setQueryPayload(self,payload):
538 """ Set the 'query' child element payload."""
539 self.setTag('query').setPayload(payload)
540 def setQuerynode(self,node):
541 """ Set the 'node' attribute value of the 'query' child element."""
542 self.setTagAttr('query','node',node)
543 def buildReply(self,typ):
544 """ Builds and returns another Iq object of specified type.
545 The to, from and query child node of new Iq are pre-set as reply to this Iq. """
546 iq=Iq(typ,to=self.getFrom(),frm=self.getTo(),attrs={'id':self.getID()})
547 if self.getTag('query'): iq.setQueryNS(self.getQueryNS())
550 class ErrorNode(Node):
551 """ XMPP-style error element.
552 In the case of stanza error should be attached to XMPP stanza.
553 In the case of stream-level errors should be used separately. """
554 def __init__(self,name,code=None,typ=None,text=None):
555 """ Create new error node object.
556 Mandatory parameter: name - name of error condition.
557 Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol."""
558 if ERRORS.has_key(name):
559 cod,type,txt=ERRORS[name]
561 else: cod,ns,type,txt='500',NS_STANZAS,'cancel',''
565 Node.__init__(self,'error',{},[Node(name)])
566 if type: self.setAttr('type',type)
567 if not cod: self.setName('stream:error')
568 if txt: self.addChild(node=Node(ns+' text',{},[txt]))
569 if cod: self.setAttr('code',cod)
571 class Error(Protocol):
572 """ Used to quickly transform received stanza into error reply."""
573 def __init__(self,node,error,reply=1):
574 """ Create error reply basing on the received 'node' stanza and the 'error' error condition.
575 If the 'node' is not the received stanza but locally created ('to' and 'from' fields needs not swapping)
576 specify the 'reply' argument as false."""
577 if reply: Protocol.__init__(self,to=node.getFrom(),frm=node.getTo(),node=node)
578 else: Protocol.__init__(self,node=node)
580 if node.getType()=='error': self.__str__=self.__dupstr__
581 def __dupstr__(self,dup1=None,dup2=None):
582 """ Dummy function used as preventor of creating error node in reply to error node.
583 I.e. you will not be able to serialise "double" error into string.
587 class DataField(Node):
588 """ This class is used in the DataForm class to describe the single data item.
589 If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122)
590 then you will need to work with instances of this class. """
591 def __init__(self,name=None,value=None,typ=None,required=0,label=None,desc=None,options=[],node=None):
592 """ Create new data field of specified name,value and type.
593 Also 'required','desc' and 'options' fields can be set.
594 Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new datafiled.
596 Node.__init__(self,'field',node=node)
597 if name: self.setVar(name)
598 if type(value) in [list,tuple]: self.setValues(value)
599 elif value: self.setValue(value)
600 if typ: self.setType(typ)
601 elif not typ and not node: self.setType('text-single')
602 if required: self.setRequired(required)
603 if label: self.setLabel(label)
604 if desc: self.setDesc(desc)
605 if options: self.setOptions(options)
606 def setRequired(self,req=1):
607 """ Change the state of the 'required' flag. """
608 if req: self.setTag('required')
610 try: self.delChild('required')
611 except ValueError: return
612 def isRequired(self):
613 """ Returns in this field a required one. """
614 return self.getTag('required')
615 def setLabel(self,label):
616 """ Set the label of this field. """
617 self.setAttr('label',label)
619 """ Return the label of this field. """
620 return self.getAttr('label')
621 def setDesc(self,desc):
622 """ Set the description of this field. """
623 self.setTagData('desc',desc)
625 """ Return the description of this field. """
626 return self.getTagData('desc')
627 def setValue(self,val):
628 """ Set the value of this field. """
629 self.setTagData('value',val)
631 return self.getTagData('value')
632 def setValues(self,lst):
633 """ Set the values of this field as values-list.
634 Replaces all previous filed values! If you need to just add a value - use addValue method."""
635 while self.getTag('value'): self.delChild('value')
636 for val in lst: self.addValue(val)
637 def addValue(self,val):
638 """ Add one more value to this field. Used in 'get' iq's or such."""
639 self.addChild('value',{},[val])
641 """ Return the list of values associated with this field."""
643 for tag in self.getTags('value'): ret.append(tag.getData())
645 def getOptions(self):
646 """ Return label-option pairs list associated with this field."""
648 for tag in self.getTags('option'): ret.append([tag.getAttr('label'),tag.getTagData('value')])
650 def setOptions(self,lst):
651 """ Set label-option pairs list associated with this field."""
652 while self.getTag('option'): self.delChild('option')
653 for opt in lst: self.addOption(opt)
654 def addOption(self,opt):
655 """ Add one more label-option pair to this field."""
656 if type(opt) in [str,unicode]: self.addChild('option').setTagData('value',opt)
657 else: self.addChild('option',{'label':opt[0]}).setTagData('value',opt[1])
659 """ Get type of this field. """
660 return self.getAttr('type')
661 def setType(self,val):
662 """ Set type of this field. """
663 return self.setAttr('type',val)
665 """ Get 'var' attribute value of this field. """
666 return self.getAttr('var')
667 def setVar(self,val):
668 """ Set 'var' attribute value of this field. """
669 return self.setAttr('var',val)
671 class DataReported(Node):
672 """ This class is used in the DataForm class to describe the 'reported data field' data items which are used in
673 'multiple item form results' (as described in XEP-0004).
674 Represents the fields that will be returned from a search. This information is useful when
675 you try to use the jabber:iq:search namespace to return dynamic form information.
677 def __init__(self,node=None):
678 """ Create new empty 'reported data' field. However, note that, according XEP-0004:
679 * It MUST contain one or more DataFields.
680 * Contained DataFields SHOULD possess a 'type' and 'label' attribute in addition to 'var' attribute
681 * Contained DataFields SHOULD NOT contain a <value/> element.
682 Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new
685 Node.__init__(self,'reported',node=node)
688 for n in self.getChildren():
689 if n.getName()=='field': newkids.append(DataField(node=n))
690 else: newkids.append(n)
692 def getField(self,name):
693 """ Return the datafield object with name 'name' (if exists). """
694 return self.getTag('field',attrs={'var':name})
695 def setField(self,name,typ=None,label=None):
696 """ Create if nessessary or get the existing datafield object with name 'name' and return it.
697 If created, attributes 'type' and 'label' are applied to new datafield."""
698 f=self.getField(name)
700 return self.addChild(node=DataField(name,None,typ,0,label))
702 """ Represent dataitem as simple dictionary mapping of datafield names to their values."""
704 for field in self.getTags('field'):
705 name=field.getAttr('var')
707 if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi':
709 for i in field.getTags('value'): val.append(i.getData())
710 else: val=field.getTagData('value')
712 if self.getTag('instructions'): ret['instructions']=self.getInstructions()
714 def __getitem__(self,name):
715 """ Simple dictionary interface for getting datafields values by their names."""
716 item=self.getField(name)
717 if item: return item.getValue()
718 raise IndexError('No such field')
719 def __setitem__(self,name,val):
720 """ Simple dictionary interface for setting datafields values by their names."""
721 return self.setField(name).setValue(val)
723 class DataItem(Node):
724 """ This class is used in the DataForm class to describe data items which are used in 'multiple
725 item form results' (as described in XEP-0004).
727 def __init__(self,node=None):
728 """ Create new empty data item. However, note that, according XEP-0004, DataItem MUST contain ALL
729 DataFields described in DataReported.
730 Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new
733 Node.__init__(self,'item',node=node)
736 for n in self.getChildren():
737 if n.getName()=='field': newkids.append(DataField(node=n))
738 else: newkids.append(n)
740 def getField(self,name):
741 """ Return the datafield object with name 'name' (if exists). """
742 return self.getTag('field',attrs={'var':name})
743 def setField(self,name):
744 """ Create if nessessary or get the existing datafield object with name 'name' and return it. """
745 f=self.getField(name)
747 return self.addChild(node=DataField(name))
749 """ Represent dataitem as simple dictionary mapping of datafield names to their values."""
751 for field in self.getTags('field'):
752 name=field.getAttr('var')
754 if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi':
756 for i in field.getTags('value'): val.append(i.getData())
757 else: val=field.getTagData('value')
759 if self.getTag('instructions'): ret['instructions']=self.getInstructions()
761 def __getitem__(self,name):
762 """ Simple dictionary interface for getting datafields values by their names."""
763 item=self.getField(name)
764 if item: return item.getValue()
765 raise IndexError('No such field')
766 def __setitem__(self,name,val):
767 """ Simple dictionary interface for setting datafields values by their names."""
768 return self.setField(name).setValue(val)
770 class DataForm(Node):
771 """ DataForm class. Used for manipulating dataforms in XMPP.
772 Relevant XEPs: 0004, 0068, 0122.
773 Can be used in disco, pub-sub and many other applications."""
774 def __init__(self, typ=None, data=[], title=None, node=None):
776 Create new dataform of type 'typ'; 'data' is the list of DataReported,
777 DataItem and DataField instances that this dataform contains; 'title'
779 You can specify the 'node' argument as the other node to be used as
780 base for constructing this dataform.
782 title and instructions is optional and SHOULD NOT contain newlines.
783 Several instructions MAY be present.
784 'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' )
785 'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) respectively.
786 'cancel' form can not contain any fields. All other forms contains AT LEAST one field.
787 'title' MAY be included in forms of type "form" and "result"
789 Node.__init__(self,'x',node=node)
792 for n in self.getChildren():
793 if n.getName()=='field': newkids.append(DataField(node=n))
794 elif n.getName()=='item': newkids.append(DataItem(node=n))
795 elif n.getName()=='reported': newkids.append(DataReported(node=n))
796 else: newkids.append(n)
798 if typ: self.setType(typ)
799 self.setNamespace(NS_DATA)
800 if title: self.setTitle(title)
801 if type(data)==type({}):
803 for name in data.keys(): newdata.append(DataField(name,data[name]))
806 if type(child) in [type(''),type(u'')]: self.addInstructions(child)
807 elif child.__class__.__name__=='DataField': self.kids.append(child)
808 elif child.__class__.__name__=='DataItem': self.kids.append(child)
809 elif child.__class__.__name__=='DataReported': self.kids.append(child)
810 else: self.kids.append(DataField(node=child))
812 """ Return the type of dataform. """
813 return self.getAttr('type')
814 def setType(self,typ):
815 """ Set the type of dataform. """
816 self.setAttr('type',typ)
818 """ Return the title of dataform. """
819 return self.getTagData('title')
820 def setTitle(self,text):
821 """ Set the title of dataform. """
822 self.setTagData('title',text)
823 def getInstructions(self):
824 """ Return the instructions of dataform. """
825 return self.getTagData('instructions')
826 def setInstructions(self,text):
827 """ Set the instructions of dataform. """
828 self.setTagData('instructions',text)
829 def addInstructions(self,text):
830 """ Add one more instruction to the dataform. """
831 self.addChild('instructions',{},[text])
832 def getField(self,name):
833 """ Return the datafield object with name 'name' (if exists). """
834 return self.getTag('field',attrs={'var':name})
835 def setField(self,name):
836 """ Create if nessessary or get the existing datafield object with name 'name' and return it. """
837 f=self.getField(name)
839 return self.addChild(node=DataField(name))
841 """ Represent dataform as simple dictionary mapping of datafield names to their values."""
843 for field in self.getTags('field'):
844 name=field.getAttr('var')
846 if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi':
848 for i in field.getTags('value'): val.append(i.getData())
849 else: val=field.getTagData('value')
851 if self.getTag('instructions'): ret['instructions']=self.getInstructions()
853 def __getitem__(self,name):
854 """ Simple dictionary interface for getting datafields values by their names."""
855 item=self.getField(name)
856 if item: return item.getValue()
857 raise IndexError('No such field')
858 def __setitem__(self,name,val):
859 """ Simple dictionary interface for setting datafields values by their names."""
860 return self.setField(name).setValue(val)