mimetype.guess_type() returns a tuple, and it might be (None, None).
[pyTivo/TheBayer.git] / xmpp / protocol.py
blob224a594eebdf4a2530d46ab544c9dc489e256ab6
1 ## protocol.py
2 ##
3 ## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
4 ##
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)
8 ## any later version.
9 ##
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.58 2007/05/13 17:55:46 normanr Exp $
17 """
18 Protocol module contains tools that is needed for processing of
19 xmpp-related data structures.
20 """
22 from simplexml import Node,ustr
23 import time
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'
62 NS_AVATAR ='jabber:iq:avatar' # XEP-0008 (historical)
63 NS_BIND ='urn:ietf:params:xml:ns:xmpp-bind'
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'
69 NS_COMMANDS ='http://jabber.org/protocol/commands'
70 NS_COMPONENT_ACCEPT ='jabber:component:accept'
71 NS_COMPONENT_1 ='http://jabberd.jabberstudio.org/ns/component/1.0'
72 NS_COMPRESS ='http://jabber.org/protocol/compress' # XEP-0138
73 NS_DATA ='jabber:x:data' # XEP-0004
74 NS_DELAY ='jabber:x:delay'
75 NS_DIALBACK ='jabber:server:dialback'
76 NS_DISCO ='http://jabber.org/protocol/disco' # XEP-0030
77 NS_DISCO_INFO =NS_DISCO+'#info' # XEP-0030
78 NS_DISCO_ITEMS =NS_DISCO+'#items' # XEP-0030
79 NS_ENCRYPTED ='jabber:x:encrypted' # XEP-0027
80 NS_EVENT ='jabber:x:event' # XEP-0022
81 NS_FEATURE ='http://jabber.org/protocol/feature-neg'
82 NS_FILE ='http://jabber.org/protocol/si/profile/file-transfer' # XEP-0096
83 NS_GATEWAY ='jabber:iq:gateway'
84 NS_GEOLOC ='http://jabber.org/protocol/geoloc' # XEP-0080
85 NS_GROUPCHAT ='gc-1.0'
86 NS_HTTP_BIND ='http://jabber.org/protocol/httpbind' # XEP-0124
87 NS_IBB ='http://jabber.org/protocol/ibb'
88 NS_INVISIBLE ='presence-invisible' # Jabberd2
89 NS_IQ ='iq' # Jabberd2
90 NS_LAST ='jabber:iq:last'
91 NS_MESSAGE ='message' # Jabberd2
92 NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107
93 NS_MUC ='http://jabber.org/protocol/muc' # XEP-0045
94 NS_MUC_ADMIN =NS_MUC+'#admin' # XEP-0045
95 NS_MUC_OWNER =NS_MUC+'#owner' # XEP-0045
96 NS_MUC_UNIQUE =NS_MUC+'#unique' # XEP-0045
97 NS_MUC_USER =NS_MUC+'#user' # XEP-0045
98 NS_MUC_REGISTER =NS_MUC+'#register' # XEP-0045
99 NS_MUC_REQUEST =NS_MUC+'#request' # XEP-0045
100 NS_MUC_ROOMCONFIG =NS_MUC+'#roomconfig' # XEP-0045
101 NS_MUC_ROOMINFO =NS_MUC+'#roominfo' # XEP-0045
102 NS_MUC_ROOMS =NS_MUC+'#rooms' # XEP-0045
103 NS_MUC_TRAFIC =NS_MUC+'#traffic' # XEP-0045
104 NS_OFFLINE ='http://jabber.org/protocol/offline' # XEP-0013
105 NS_PHYSLOC ='http://jabber.org/protocol/physloc' # XEP-0112
106 NS_PRESENCE ='presence' # Jabberd2
107 NS_PRIVACY ='jabber:iq:privacy'
108 NS_PRIVATE ='jabber:iq:private'
109 NS_PUBSUB ='http://jabber.org/protocol/pubsub' # XEP-0060
110 NS_REGISTER ='jabber:iq:register'
111 NS_ROSTER ='jabber:iq:roster'
112 NS_ROSTERX ='http://jabber.org/protocol/rosterx' # XEP-0144
113 NS_RPC ='jabber:iq:rpc' # XEP-0009
114 NS_SASL ='urn:ietf:params:xml:ns:xmpp-sasl'
115 NS_SEARCH ='jabber:iq:search'
116 NS_SERVER ='jabber:server'
117 NS_SESSION ='urn:ietf:params:xml:ns:xmpp-session'
118 NS_SI ='http://jabber.org/protocol/si' # XEP-0096
119 NS_SI_PUB ='http://jabber.org/protocol/sipub' # XEP-0137
120 NS_SIGNED ='jabber:x:signed' # XEP-0027
121 NS_STANZAS ='urn:ietf:params:xml:ns:xmpp-stanzas'
122 NS_STREAMS ='http://etherx.jabber.org/streams'
123 NS_TIME ='jabber:iq:time'
124 NS_TLS ='urn:ietf:params:xml:ns:xmpp-tls'
125 NS_VACATION ='http://jabber.org/protocol/vacation'
126 NS_VCARD ='vcard-temp'
127 NS_VERSION ='jabber:iq:version'
128 NS_WAITINGLIST ='http://jabber.org/protocol/waitinglist' # XEP-0130
129 NS_XHTML_IM ='http://jabber.org/protocol/xhtml-im' # XEP-0071
130 NS_DATA_LAYOUT ='http://jabber.org/protocol/xdata-layout' # XEP-0141
131 NS_DATA_VALIDATE ='http://jabber.org/protocol/xdata-validate' # XEP-0122
132 NS_XMPP_STREAMS ='urn:ietf:params:xml:ns:xmpp-streams'
134 xmpp_stream_error_conditions="""
135 bad-format -- -- -- The entity has sent XML that cannot be processed.
136 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.
137 conflict -- -- -- The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream.
138 connection-timeout -- -- -- The entity has not generated any traffic over the stream for some period of time.
139 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.
140 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.
141 improper-addressing -- -- -- A stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value).
142 internal-server-error -- -- -- The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.
143 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.
144 invalid-id -- -- -- The stream ID or dialback ID is invalid or does not match an ID previously provided.
145 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".
146 invalid-xml -- -- -- The entity has sent invalid XML over the stream to a server that performs validation.
147 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.
148 policy-violation -- -- -- The entity has violated some local service policy.
149 remote-connection-failed -- -- -- The server is unable to properly connect to a remote resource that is required for authentication or authorization.
150 resource-constraint -- -- -- The server lacks the system resources necessary to service the stream.
151 restricted-xml -- -- -- The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character.
152 see-other-host -- -- -- The server will not provide service to the initiating entity but is redirecting traffic to another host.
153 system-shutdown -- -- -- The server is being shut down and all active streams are being closed.
154 undefined-condition -- -- -- The error condition is not one of those defined by the other conditions in this list.
155 unsupported-encoding -- -- -- The initiating entity has encoded the stream in an encoding that is not supported by the server.
156 unsupported-stanza-type -- -- -- The initiating entity has sent a first-level child of the stream that is not supported by the server.
157 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.
158 xml-not-well-formed -- -- -- The initiating entity has sent XML that is not well-formed."""
159 xmpp_stanza_error_conditions="""
160 bad-request -- 400 -- modify -- The sender has sent XML that is malformed or that cannot be processed.
161 conflict -- 409 -- cancel -- Access cannot be granted because an existing resource or session exists with the same name or address.
162 feature-not-implemented -- 501 -- cancel -- The feature requested is not implemented by the recipient or server and therefore cannot be processed.
163 forbidden -- 403 -- auth -- The requesting entity does not possess the required permissions to perform the action.
164 gone -- 302 -- modify -- The recipient or server can no longer be contacted at this address.
165 internal-server-error -- 500 -- wait -- The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error.
166 item-not-found -- 404 -- cancel -- The addressed JID or item requested cannot be found.
167 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.
168 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.
169 not-allowed -- 405 -- cancel -- The recipient or server does not allow any entity to perform the action.
170 not-authorized -- 401 -- auth -- The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials.
171 payment-required -- 402 -- auth -- The requesting entity is not authorized to access the requested service because payment is required.
172 recipient-unavailable -- 404 -- wait -- The intended recipient is temporarily unavailable.
173 redirect -- 302 -- modify -- The recipient or server is redirecting requests for this information to another entity.
174 registration-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because registration is required.
175 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.
176 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.
177 resource-constraint -- 500 -- wait -- The server or recipient lacks the system resources necessary to service the request.
178 service-unavailable -- 503 -- cancel -- The server or recipient does not currently provide the requested service.
179 subscription-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because a subscription is required.
180 undefined-condition -- 500 -- --
181 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)."""
182 sasl_error_conditions="""
183 aborted -- -- -- The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element.
184 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.
185 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.
186 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.
187 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.
188 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.
189 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."""
191 ERRORS,_errorcodes={},{}
192 for ns,errname,errpool in [(NS_XMPP_STREAMS,'STREAM',xmpp_stream_error_conditions),
193 (NS_STANZAS ,'ERR' ,xmpp_stanza_error_conditions),
194 (NS_SASL ,'SASL' ,sasl_error_conditions)]:
195 for err in errpool.split('\n')[1:]:
196 cond,code,typ,text=err.split(' -- ')
197 name=errname+'_'+cond.upper().replace('-','_')
198 locals()[name]=ns+' '+cond
199 ERRORS[ns+' '+cond]=[code,typ,text]
200 if code: _errorcodes[code]=cond
201 del ns,errname,errpool,err,cond,code,typ,text
203 def isResultNode(node):
204 """ Returns true if the node is a positive reply. """
205 return node and node.getType()=='result'
206 def isErrorNode(node):
207 """ Returns true if the node is a negative reply. """
208 return node and node.getType()=='error'
210 class NodeProcessed(Exception):
211 """ Exception that should be raised by handler when the handling should be stopped. """
212 class StreamError(Exception):
213 """ Base exception class for stream errors."""
214 class BadFormat(StreamError): pass
215 class BadNamespacePrefix(StreamError): pass
216 class Conflict(StreamError): pass
217 class ConnectionTimeout(StreamError): pass
218 class HostGone(StreamError): pass
219 class HostUnknown(StreamError): pass
220 class ImproperAddressing(StreamError): pass
221 class InternalServerError(StreamError): pass
222 class InvalidFrom(StreamError): pass
223 class InvalidID(StreamError): pass
224 class InvalidNamespace(StreamError): pass
225 class InvalidXML(StreamError): pass
226 class NotAuthorized(StreamError): pass
227 class PolicyViolation(StreamError): pass
228 class RemoteConnectionFailed(StreamError): pass
229 class ResourceConstraint(StreamError): pass
230 class RestrictedXML(StreamError): pass
231 class SeeOtherHost(StreamError): pass
232 class SystemShutdown(StreamError): pass
233 class UndefinedCondition(StreamError): pass
234 class UnsupportedEncoding(StreamError): pass
235 class UnsupportedStanzaType(StreamError): pass
236 class UnsupportedVersion(StreamError): pass
237 class XMLNotWellFormed(StreamError): pass
239 stream_exceptions = {'bad-format': BadFormat,
240 'bad-namespace-prefix': BadNamespacePrefix,
241 'conflict': Conflict,
242 'connection-timeout': ConnectionTimeout,
243 'host-gone': HostGone,
244 'host-unknown': HostUnknown,
245 'improper-addressing': ImproperAddressing,
246 'internal-server-error': InternalServerError,
247 'invalid-from': InvalidFrom,
248 'invalid-id': InvalidID,
249 'invalid-namespace': InvalidNamespace,
250 'invalid-xml': InvalidXML,
251 'not-authorized': NotAuthorized,
252 'policy-violation': PolicyViolation,
253 'remote-connection-failed': RemoteConnectionFailed,
254 'resource-constraint': ResourceConstraint,
255 'restricted-xml': RestrictedXML,
256 'see-other-host': SeeOtherHost,
257 'system-shutdown': SystemShutdown,
258 'undefined-condition': UndefinedCondition,
259 'unsupported-encoding': UnsupportedEncoding,
260 'unsupported-stanza-type': UnsupportedStanzaType,
261 'unsupported-version': UnsupportedVersion,
262 'xml-not-well-formed': XMLNotWellFormed}
264 class JID:
265 """ JID object. JID can be built from string, modified, compared, serialised into string. """
266 def __init__(self, jid=None, node='', domain='', resource=''):
267 """ Constructor. JID can be specified as string (jid argument) or as separate parts.
268 Examples:
269 JID('node@domain/resource')
270 JID(node='node',domain='domain.org')
272 if not jid and not domain: raise ValueError('JID must contain at least domain name')
273 elif type(jid)==type(self): self.node,self.domain,self.resource=jid.node,jid.domain,jid.resource
274 elif domain: self.node,self.domain,self.resource=node,domain,resource
275 else:
276 if jid.find('@')+1: self.node,jid=jid.split('@',1)
277 else: self.node=''
278 if jid.find('/')+1: self.domain,self.resource=jid.split('/',1)
279 else: self.domain,self.resource=jid,''
280 def getNode(self):
281 """ Return the node part of the JID """
282 return self.node
283 def setNode(self,node):
284 """ Set the node part of the JID to new value. Specify None to remove the node part."""
285 self.node=node.lower()
286 def getDomain(self):
287 """ Return the domain part of the JID """
288 return self.domain
289 def setDomain(self,domain):
290 """ Set the domain part of the JID to new value."""
291 self.domain=domain.lower()
292 def getResource(self):
293 """ Return the resource part of the JID """
294 return self.resource
295 def setResource(self,resource):
296 """ Set the resource part of the JID to new value. Specify None to remove the resource part."""
297 self.resource=resource
298 def getStripped(self):
299 """ Return the bare representation of JID. I.e. string value w/o resource. """
300 return self.__str__(0)
301 def __eq__(self, other):
302 """ Compare the JID to another instance or to string for equality. """
303 try: other=JID(other)
304 except ValueError: return 0
305 return self.resource==other.resource and self.__str__(0) == other.__str__(0)
306 def __ne__(self, other):
307 """ Compare the JID to another instance or to string for non-equality. """
308 return not self.__eq__(other)
309 def bareMatch(self, other):
310 """ Compare the node and domain parts of the JID's for equality. """
311 return self.__str__(0) == JID(other).__str__(0)
312 def __str__(self,wresource=1):
313 """ Serialise JID into string. """
314 if self.node: jid=self.node+'@'+self.domain
315 else: jid=self.domain
316 if wresource and self.resource: return jid+'/'+self.resource
317 return jid
318 def __hash__(self):
319 """ Produce hash of the JID, Allows to use JID objects as keys of the dictionary. """
320 return hash(self.__str__())
322 class Protocol(Node):
323 """ A "stanza" object class. Contains methods that are common for presences, iqs and messages. """
324 def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None):
325 """ Constructor, name is the name of the stanza i.e. 'message' or 'presence' or 'iq'.
326 to is the value of 'to' attribure, 'typ' - 'type' attribute
327 frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition
328 timestamp - the time value that needs to be stamped over stanza
329 xmlns - namespace of top stanza node
330 node - parsed or unparsed stana to be taken as prototype.
332 if not attrs: attrs={}
333 if to: attrs['to']=to
334 if frm: attrs['from']=frm
335 if typ: attrs['type']=typ
336 Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node)
337 if not node and xmlns: self.setNamespace(xmlns)
338 if self['to']: self.setTo(self['to'])
339 if self['from']: self.setFrom(self['from'])
340 if node and type(self)==type(node) and self.__class__==node.__class__ and self.attrs.has_key('id'): del self.attrs['id']
341 self.timestamp=None
342 for x in self.getTags('x',namespace=NS_DELAY):
343 try:
344 if not self.getTimestamp() or x.getAttr('stamp')<self.getTimestamp(): self.setTimestamp(x.getAttr('stamp'))
345 except: pass
346 if timestamp is not None: self.setTimestamp(timestamp) # To auto-timestamp stanza just pass timestamp=''
347 def getTo(self):
348 """ Return value of the 'to' attribute. """
349 try: return self['to']
350 except: return None
351 def getFrom(self):
352 """ Return value of the 'from' attribute. """
353 try: return self['from']
354 except: return None
355 def getTimestamp(self):
356 """ Return the timestamp in the 'yyyymmddThhmmss' format. """
357 return self.timestamp
358 def getID(self):
359 """ Return the value of the 'id' attribute. """
360 return self.getAttr('id')
361 def setTo(self,val):
362 """ Set the value of the 'to' attribute. """
363 self.setAttr('to', JID(val))
364 def getType(self):
365 """ Return the value of the 'type' attribute. """
366 return self.getAttr('type')
367 def setFrom(self,val):
368 """ Set the value of the 'from' attribute. """
369 self.setAttr('from', JID(val))
370 def setType(self,val):
371 """ Set the value of the 'type' attribute. """
372 self.setAttr('type', val)
373 def setID(self,val):
374 """ Set the value of the 'id' attribute. """
375 self.setAttr('id', val)
376 def getError(self):
377 """ Return the error-condition (if present) or the textual description of the error (otherwise). """
378 errtag=self.getTag('error')
379 if errtag:
380 for tag in errtag.getChildren():
381 if tag.getName()<>'text': return tag.getName()
382 return errtag.getData()
383 def getErrorCode(self):
384 """ Return the error code. Obsolette. """
385 return self.getTagAttr('error','code')
386 def setError(self,error,code=None):
387 """ Set the error code. Obsolette. Use error-conditions instead. """
388 if code:
389 if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error)
390 else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error)
391 elif type(error) in [type(''),type(u'')]: error=ErrorNode(error)
392 self.setType('error')
393 self.addChild(node=error)
394 def setTimestamp(self,val=None):
395 """Set the timestamp. timestamp should be the yyyymmddThhmmss string."""
396 if not val: val=time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())
397 self.timestamp=val
398 self.setTag('x',{'stamp':self.timestamp},namespace=NS_DELAY)
399 def getProperties(self):
400 """ Return the list of namespaces to which belongs the direct childs of element"""
401 props=[]
402 for child in self.getChildren():
403 prop=child.getNamespace()
404 if prop not in props: props.append(prop)
405 return props
406 def __setitem__(self,item,val):
407 """ Set the item 'item' to the value 'val'."""
408 if item in ['to','from']: val=JID(val)
409 return self.setAttr(item,val)
411 class Message(Protocol):
412 """ XMPP Message stanza - "push" mechanism."""
413 def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None):
414 """ Create message object. You can specify recipient, text of message, type of message
415 any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
416 Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """
417 Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
418 if body: self.setBody(body)
419 if subject: self.setSubject(subject)
420 def getBody(self):
421 """ Returns text of the message. """
422 return self.getTagData('body')
423 def getSubject(self):
424 """ Returns subject of the message. """
425 return self.getTagData('subject')
426 def getThread(self):
427 """ Returns thread of the message. """
428 return self.getTagData('thread')
429 def setBody(self,val):
430 """ Sets the text of the message. """
431 self.setTagData('body',val)
432 def setSubject(self,val):
433 """ Sets the subject of the message. """
434 self.setTagData('subject',val)
435 def setThread(self,val):
436 """ Sets the thread of the message. """
437 self.setTagData('thread',val)
438 def buildReply(self,text=None):
439 """ Builds and returns another message object with specified text.
440 The to, from and thread properties of new message are pre-set as reply to this message. """
441 m=Message(to=self.getFrom(),frm=self.getTo(),body=text)
442 th=self.getThread()
443 if th: m.setThread(th)
444 return m
446 class Presence(Protocol):
447 """ XMPP Presence object."""
448 def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None):
449 """ Create presence object. You can specify recipient, type of message, priority, show and status values
450 any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
451 Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as presence. """
452 Protocol.__init__(self, 'presence', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
453 if priority: self.setPriority(priority)
454 if show: self.setShow(show)
455 if status: self.setStatus(status)
456 def getPriority(self):
457 """ Returns the priority of the message. """
458 return self.getTagData('priority')
459 def getShow(self):
460 """ Returns the show value of the message. """
461 return self.getTagData('show')
462 def getStatus(self):
463 """ Returns the status string of the message. """
464 return self.getTagData('status')
465 def setPriority(self,val):
466 """ Sets the priority of the message. """
467 self.setTagData('priority',val)
468 def setShow(self,val):
469 """ Sets the show value of the message. """
470 self.setTagData('show',val)
471 def setStatus(self,val):
472 """ Sets the status string of the message. """
473 self.setTagData('status',val)
475 def _muc_getItemAttr(self,tag,attr):
476 for xtag in self.getTags('x'):
477 for child in xtag.getTags(tag):
478 return child.getAttr(attr)
479 def _muc_getSubTagDataAttr(self,tag,attr):
480 for xtag in self.getTags('x'):
481 for child in xtag.getTags('item'):
482 for cchild in child.getTags(tag):
483 return cchild.getData(),cchild.getAttr(attr)
484 return None,None
485 def getRole(self):
486 """Returns the presence role (for groupchat)"""
487 return self._muc_getItemAttr('item','role')
488 def getAffiliation(self):
489 """Returns the presence affiliation (for groupchat)"""
490 return self._muc_getItemAttr('item','affiliation')
491 def getNick(self):
492 """Returns the nick value (for nick change in groupchat)"""
493 return self._muc_getItemAttr('item','nick')
494 def getJid(self):
495 """Returns the presence jid (for groupchat)"""
496 return self._muc_getItemAttr('item','jid')
497 def getReason(self):
498 """Returns the reason of the presence (for groupchat)"""
499 return self._muc_getSubTagDataAttr('reason','')[0]
500 def getActor(self):
501 """Returns the reason of the presence (for groupchat)"""
502 return self._muc_getSubTagDataAttr('actor','jid')[1]
503 def getStatusCode(self):
504 """Returns the status code of the presence (for groupchat)"""
505 return self._muc_getItemAttr('status','code')
507 class Iq(Protocol):
508 """ XMPP Iq object - get/set dialog mechanism. """
509 def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None):
510 """ Create Iq object. You can specify type, query namespace
511 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.
512 Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as an iq. """
513 Protocol.__init__(self, 'iq', to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node)
514 if payload: self.setQueryPayload(payload)
515 if queryNS: self.setQueryNS(queryNS)
516 def getQueryNS(self):
517 """ Return the namespace of the 'query' child element."""
518 tag=self.getTag('query')
519 if tag: return tag.getNamespace()
520 def getQuerynode(self):
521 """ Return the 'node' attribute value of the 'query' child element."""
522 return self.getTagAttr('query','node')
523 def getQueryPayload(self):
524 """ Return the 'query' child element payload."""
525 tag=self.getTag('query')
526 if tag: return tag.getPayload()
527 def getQueryChildren(self):
528 """ Return the 'query' child element child nodes."""
529 tag=self.getTag('query')
530 if tag: return tag.getChildren()
531 def setQueryNS(self,namespace):
532 """ Set the namespace of the 'query' child element."""
533 self.setTag('query').setNamespace(namespace)
534 def setQueryPayload(self,payload):
535 """ Set the 'query' child element payload."""
536 self.setTag('query').setPayload(payload)
537 def setQuerynode(self,node):
538 """ Set the 'node' attribute value of the 'query' child element."""
539 self.setTagAttr('query','node',node)
540 def buildReply(self,typ):
541 """ Builds and returns another Iq object of specified type.
542 The to, from and query child node of new Iq are pre-set as reply to this Iq. """
543 iq=Iq(typ,to=self.getFrom(),frm=self.getTo(),attrs={'id':self.getID()})
544 if self.getTag('query'): iq.setQueryNS(self.getQueryNS())
545 return iq
547 class ErrorNode(Node):
548 """ XMPP-style error element.
549 In the case of stanza error should be attached to XMPP stanza.
550 In the case of stream-level errors should be used separately. """
551 def __init__(self,name,code=None,typ=None,text=None):
552 """ Create new error node object.
553 Mandatory parameter: name - name of error condition.
554 Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol."""
555 if ERRORS.has_key(name):
556 cod,type,txt=ERRORS[name]
557 ns=name.split()[0]
558 else: cod,ns,type,txt='500',NS_STANZAS,'cancel',''
559 if typ: type=typ
560 if code: cod=code
561 if text: txt=text
562 Node.__init__(self,'error',{},[Node(name)])
563 if type: self.setAttr('type',type)
564 if not cod: self.setName('stream:error')
565 if txt: self.addChild(node=Node(ns+' text',{},[txt]))
566 if cod: self.setAttr('code',cod)
568 class Error(Protocol):
569 """ Used to quickly transform received stanza into error reply."""
570 def __init__(self,node,error,reply=1):
571 """ Create error reply basing on the received 'node' stanza and the 'error' error condition.
572 If the 'node' is not the received stanza but locally created ('to' and 'from' fields needs not swapping)
573 specify the 'reply' argument as false."""
574 if reply: Protocol.__init__(self,to=node.getFrom(),frm=node.getTo(),node=node)
575 else: Protocol.__init__(self,node=node)
576 self.setError(error)
577 if node.getType()=='error': self.__str__=self.__dupstr__
578 def __dupstr__(self,dup1=None,dup2=None):
579 """ Dummy function used as preventor of creating error node in reply to error node.
580 I.e. you will not be able to serialise "double" error into string.
582 return ''
584 class DataField(Node):
585 """ This class is used in the DataForm class to describe the single data item.
586 If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122)
587 then you will need to work with instances of this class. """
588 def __init__(self,name=None,value=None,typ=None,required=0,label=None,desc=None,options=[],node=None):
589 """ Create new data field of specified name,value and type.
590 Also 'required','desc' and 'options' fields can be set.
591 Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new datafiled.
593 Node.__init__(self,'field',node=node)
594 if name: self.setVar(name)
595 if type(value) in [list,tuple]: self.setValues(value)
596 elif value: self.setValue(value)
597 if typ: self.setType(typ)
598 elif not typ and not node: self.setType('text-single')
599 if required: self.setRequired(required)
600 if label: self.setLabel(label)
601 if desc: self.setDesc(desc)
602 if options: self.setOptions(options)
603 def setRequired(self,req=1):
604 """ Change the state of the 'required' flag. """
605 if req: self.setTag('required')
606 else:
607 try: self.delChild('required')
608 except ValueError: return
609 def isRequired(self):
610 """ Returns in this field a required one. """
611 return self.getTag('required')
612 def setLabel(self,label):
613 """ Set the label of this field. """
614 self.setAttr('label',label)
615 def getLabel(self):
616 """ Return the label of this field. """
617 return self.getAttr('label')
618 def setDesc(self,desc):
619 """ Set the description of this field. """
620 self.setTagData('desc',desc)
621 def getDesc(self):
622 """ Return the description of this field. """
623 return self.getTagData('desc')
624 def setValue(self,val):
625 """ Set the value of this field. """
626 self.setTagData('value',val)
627 def getValue(self):
628 return self.getTagData('value')
629 def setValues(self,lst):
630 """ Set the values of this field as values-list.
631 Replaces all previous filed values! If you need to just add a value - use addValue method."""
632 while self.getTag('value'): self.delChild('value')
633 for val in lst: self.addValue(val)
634 def addValue(self,val):
635 """ Add one more value to this field. Used in 'get' iq's or such."""
636 self.addChild('value',{},[val])
637 def getValues(self):
638 """ Return the list of values associated with this field."""
639 ret=[]
640 for tag in self.getTags('value'): ret.append(tag.getData())
641 return ret
642 def getOptions(self):
643 """ Return label-option pairs list associated with this field."""
644 ret=[]
645 for tag in self.getTags('option'): ret.append([tag.getAttr('label'),tag.getTagData('value')])
646 return ret
647 def setOptions(self,lst):
648 """ Set label-option pairs list associated with this field."""
649 while self.getTag('option'): self.delChild('option')
650 for opt in lst: self.addOption(opt)
651 def addOption(self,opt):
652 """ Add one more label-option pair to this field."""
653 if type(opt) in [str,unicode]: self.addChild('option').setTagData('value',opt)
654 else: self.addChild('option',{'label':opt[0]}).setTagData('value',opt[1])
655 def getType(self):
656 """ Get type of this field. """
657 return self.getAttr('type')
658 def setType(self,val):
659 """ Set type of this field. """
660 return self.setAttr('type',val)
661 def getVar(self):
662 """ Get 'var' attribute value of this field. """
663 return self.getAttr('var')
664 def setVar(self,val):
665 """ Set 'var' attribute value of this field. """
666 return self.setAttr('var',val)
668 class DataForm(Node):
669 """ DataForm class. Used for manipulating dataforms in XMPP.
670 Relevant XEPs: 0004, 0068, 0122.
671 Can be used in disco, pub-sub and many other applications."""
672 def __init__(self, typ=None, data=[], title=None, node=None):
674 Create new dataform of type 'typ'. 'data' is the list of DataField
675 instances that this dataform contains, 'title' - the title string.
676 You can specify the 'node' argument as the other node to be used as
677 base for constructing this dataform.
679 title and instructions is optional and SHOULD NOT contain newlines.
680 Several instructions MAY be present.
681 'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' )
682 'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) respectively.
683 'cancel' form can not contain any fields. All other forms contains AT LEAST one field.
684 'title' MAY be included in forms of type "form" and "result"
686 Node.__init__(self,'x',node=node)
687 if node:
688 newkids=[]
689 for n in self.getChildren():
690 if n.getName()=='field': newkids.append(DataField(node=n))
691 else: newkids.append(n)
692 self.kids=newkids
693 if typ: self.setType(typ)
694 self.setNamespace(NS_DATA)
695 if title: self.setTitle(title)
696 if type(data)==type({}):
697 newdata=[]
698 for name in data.keys(): newdata.append(DataField(name,data[name]))
699 data=newdata
700 for child in data:
701 if type(child) in [type(''),type(u'')]: self.addInstructions(child)
702 elif child.__class__.__name__=='DataField': self.kids.append(child)
703 else: self.kids.append(DataField(node=child))
704 def getType(self):
705 """ Return the type of dataform. """
706 return self.getAttr('type')
707 def setType(self,typ):
708 """ Set the type of dataform. """
709 self.setAttr('type',typ)
710 def getTitle(self):
711 """ Return the title of dataform. """
712 return self.getTagData('title')
713 def setTitle(self,text):
714 """ Set the title of dataform. """
715 self.setTagData('title',text)
716 def getInstructions(self):
717 """ Return the instructions of dataform. """
718 return self.getTagData('instructions')
719 def setInstructions(self,text):
720 """ Set the instructions of dataform. """
721 self.setTagData('instructions',text)
722 def addInstructions(self,text):
723 """ Add one more instruction to the dataform. """
724 self.addChild('instructions',{},[text])
725 def getField(self,name):
726 """ Return the datafield object with name 'name' (if exists). """
727 return self.getTag('field',attrs={'var':name})
728 def setField(self,name):
729 """ Create if nessessary or get the existing datafield object with name 'name' and return it. """
730 f=self.getField(name)
731 if f: return f
732 return self.addChild(node=DataField(name))
733 def asDict(self):
734 """ Represent dataform as simple dictionary mapping of datafield names to their values."""
735 ret={}
736 for field in self.getTags('field'):
737 name=field.getAttr('var')
738 typ=field.getType()
739 if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi':
740 val=[]
741 for i in field.getTags('value'): val.append(i.getData())
742 else: val=field.getTagData('value')
743 ret[name]=val
744 if self.getTag('instructions'): ret['instructions']=self.getInstructions()
745 return ret
746 def __getitem__(self,name):
747 """ Simple dictionary interface for getting datafields values by their names."""
748 item=self.getField(name)
749 if item: return item.getValue()
750 raise IndexError('No such field')
751 def __setitem__(self,name,val):
752 """ Simple dictionary interface for setting datafields values by their names."""
753 return self.setField(name).setValue(val)