Package nbxmpp :: Module protocol

[hide private]
[frames] | no frames]

# Source Code for Module nbxmpp.protocol

       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.
      14
      15  # $Id: protocol.py,v 1.52 2006/01/09 22:08:57 normanr Exp $
      16
      17  """
      18  Protocol module contains tools that are needed for processing of xmpp-related
      19  data structures, including jabber-objects like JID or different stanzas and
      20  sub- stanzas) handling routines
      21  """
      22
      23  from simplexml import Node, NodeBuilder
      24  import time
      25  import string
      26  import hashlib
      27
      28 -def ascii_upper(s):
      29      trans_table = string.maketrans(string.ascii_lowercase,
      30          string.ascii_uppercase)
      31      return s.translate(trans_table)
      32
      33  NS_ACTIVITY       = 'http://jabber.org/protocol/activity'             # XEP-0108
      34  NS_ADDRESS        = 'http://jabber.org/protocol/address'              # XEP-0033
      35  NS_AGENTS         = 'jabber:iq:agents'
      36  NS_AMP            = 'http://jabber.org/protocol/amp'
      37  NS_AMP_ERRORS     = NS_AMP + '#errors'
      38  NS_ARCHIVE        = 'urn:xmpp:archive'                                # XEP-0136
      39  NS_ARCHIVE_AUTO   = NS_ARCHIVE + ':auto'                              # XEP-0136
      40  NS_ARCHIVE_MANAGE = NS_ARCHIVE + ':manage'                            # XEP-0136
      41  NS_ARCHIVE_MANUAL = NS_ARCHIVE + ':manual'                            # XEP-0136
      42  NS_ARCHIVE_PREF   = NS_ARCHIVE + ':pref'
      43  NS_ATOM           = 'http://www.w3.org/2005/Atom'
      44  NS_ATTENTION      = 'urn:xmpp:attention:0'                            # XEP-0224
      45  NS_AUTH           = 'jabber:iq:auth'
      46  NS_AVATAR         = 'http://www.xmpp.org/extensions/xep-0084.html#ns-metadata'
      47  NS_BIND           = 'urn:ietf:params:xml:ns:xmpp-bind'
      48  NS_BLOCKING       = 'urn:xmpp:blocking'                               # XEP-0191
      49  NS_BOB            = 'urn:xmpp:bob'                                    # XEP-0231
      50  NS_BOOKMARKS      = 'storage:bookmarks'                               # XEP-0048
      51  NS_BROWSE         = 'jabber:iq:browse'
      52  NS_BROWSING       = 'http://jabber.org/protocol/browsing'             # XEP-0195
      53  NS_BYTESTREAM     = 'http://jabber.org/protocol/bytestreams'          # XEP-0065
      54  NS_CAPS           = 'http://jabber.org/protocol/caps'                 # XEP-0115
      55  NS_CAPTCHA        = 'urn:xmpp:captcha'                                # XEP-0158
      56  NS_CARBONS        = 'urn:xmpp:carbons:2'                              # XEP-0280
      57  NS_CHATSTATES     = 'http://jabber.org/protocol/chatstates'           # XEP-0085
      58  NS_CHATTING       = 'http://jabber.org/protocol/chatting'             # XEP-0194
      59  NS_CLIENT         = 'jabber:client'
      60  NS_CONDITIONS     = 'urn:xmpp:muc:conditions:0'                       # XEP-0306
      61  NS_COMMANDS       = 'http://jabber.org/protocol/commands'
      62  NS_COMPONENT_ACCEPT = 'jabber:component:accept'
      63  NS_COMPONENT_1    = 'http://jabberd.jabberstudio.org/ns/component/1.0'
      64  NS_COMPRESS       = 'http://jabber.org/protocol/compress'             # XEP-0138
      65  NS_CONFERENCE     = 'jabber:x:conference'
      66  NS_CORRECT        = 'urn:xmpp:message-correct:0'                      # XEP-0308
      67  NS_DATA           = 'jabber:x:data'                                   # XEP-0004
      68  NS_DATA_MEDIA     = 'urn:xmpp:media-element'                          # XEP-0221
      69  NS_DELAY          = 'jabber:x:delay'
      70  NS_DELAY2         = 'urn:xmpp:delay'
      71  NS_DIALBACK       = 'jabber:server:dialback'
      72  NS_DISCO          = 'http://jabber.org/protocol/disco'
      73  NS_DISCO_INFO     = NS_DISCO + '#info'
      74  NS_DISCO_ITEMS    = NS_DISCO + '#items'
      75  NS_ENCRYPTED      = 'jabber:x:encrypted'                              # XEP-0027
      76  NS_ESESSION       = 'http://www.xmpp.org/extensions/xep-0116.html#ns'
      77  NS_ESESSION_INIT  = 'http://www.xmpp.org/extensions/xep-0116.html#ns-init' # XEP-0116
      78  NS_EVENT          = 'jabber:x:event'                                  # XEP-0022
      79  NS_FEATURE        = 'http://jabber.org/protocol/feature-neg'
      80  NS_FILE           = 'http://jabber.org/protocol/si/profile/file-transfer' # XEP-0096
      81  NS_FORWARD        = 'urn:xmpp:forward:0'                              # XEP-0297
      82  NS_GAMING         = 'http://jabber.org/protocol/gaming'               # XEP-0196
      83  NS_GATEWAY        = 'jabber:iq:gateway'                               # XEP-0100
      84  NS_GEOLOC         = 'http://jabber.org/protocol/geoloc'               # XEP-0080
      85  NS_GROUPCHAT      = 'gc-1.0'
      86  NS_HTTP_AUTH      = 'http://jabber.org/protocol/http-auth'            # XEP-0070
      87  NS_HTTP_BIND      = 'http://jabber.org/protocol/httpbind'             # XEP-0124
      88  NS_IBB            = 'http://jabber.org/protocol/ibb'
      89  NS_INVISIBLE      = 'presence-invisible'                              # Jabberd2
      90  NS_IQ             = 'iq'                                              # Jabberd2
      91  NS_JINGLE         ='urn:xmpp:jingle:1'                                # XEP-0166
      92  NS_JINGLE_ERRORS  = 'urn:xmpp:jingle:errors:1'                        # XEP-0166
      93  NS_JINGLE_RTP     = 'urn:xmpp:jingle:apps:rtp:1'                      # XEP-0167
      94  NS_JINGLE_RTP_AUDIO = 'urn:xmpp:jingle:apps:rtp:audio'                # XEP-0167
      95  NS_JINGLE_RTP_VIDEO = 'urn:xmpp:jingle:apps:rtp:video'                # XEP-0167
      96  NS_JINGLE_FILE_TRANSFER ='urn:xmpp:jingle:apps:file-transfer:3'        # XEP-0234
      97  NS_JINGLE_XTLS='urn:xmpp:jingle:security:xtls:0'                      # XTLS: EXPERIMENTAL security layer of jingle
      98  NS_JINGLE_RAW_UDP = 'urn:xmpp:jingle:transports:raw-udp:1'            # XEP-0177
      99  NS_JINGLE_ICE_UDP = 'urn:xmpp:jingle:transports:ice-udp:1'            # XEP-0176
     100  NS_JINGLE_BYTESTREAM ='urn:xmpp:jingle:transports:s5b:1'              # XEP-0260
     101  NS_JINGLE_IBB     = 'urn:xmpp:jingle:transports:ibb:1'                # XEP-0261
     102  NS_LAST           = 'jabber:iq:last'
     103  NS_LOCATION       = 'http://jabber.org/protocol/geoloc'               # XEP-0080
     104  NS_MAM            = 'urn:xmpp:mam:tmp'                                # XEP-0313
     105  NS_MESSAGE        = 'message'                                         # Jabberd2
     106  NS_MOOD           = 'http://jabber.org/protocol/mood'                 # XEP-0107
     107  NS_MUC            = 'http://jabber.org/protocol/muc'
     108  NS_MUC_USER       = NS_MUC + '#user'
     109  NS_MUC_ADMIN      = NS_MUC + '#admin'
     110  NS_MUC_OWNER      = NS_MUC + '#owner'
     111  NS_MUC_UNIQUE     = NS_MUC + '#unique'
     112  NS_MUC_CONFIG     = NS_MUC + '#roomconfig'
     113  NS_NICK           = 'http://jabber.org/protocol/nick'                 # XEP-0172
     114  NS_OFFLINE        = 'http://www.jabber.org/jeps/jep-0030.html'        # XEP-0013
     115  NS_PHYSLOC        = 'http://jabber.org/protocol/physloc'              # XEP-0112
     116  NS_PING           = 'urn:xmpp:ping'                                   # XEP-0199
     117  NS_PRESENCE       = 'presence'                                        # Jabberd2
     118  NS_PRIVACY        = 'jabber:iq:privacy'
     119  NS_PRIVATE        = 'jabber:iq:private'
     120  NS_PROFILE        = 'http://jabber.org/protocol/profile'              # XEP-0154
     121  NS_PUBSUB         = 'http://jabber.org/protocol/pubsub'               # XEP-0060
     122  NS_PUBSUB_EVENT   = 'http://jabber.org/protocol/pubsub#event'
     123  NS_PUBSUB_PUBLISH_OPTIONS = NS_PUBSUB + '#publish-options'            # XEP-0060
     124  NS_PUBSUB_OWNER   = 'http://jabber.org/protocol/pubsub#owner'         # XEP-0060
     125  NS_REGISTER       = 'jabber:iq:register'
     126  NS_ROSTER         = 'jabber:iq:roster'
     127  NS_ROSTERNOTES    = 'storage:rosternotes'
     128  NS_ROSTERX        = 'http://jabber.org/protocol/rosterx'              # XEP-0144
     129  NS_ROSTER_VER     = 'urn:xmpp:features:rosterver'                     # XEP-0273
     130  NS_RPC            = 'jabber:iq:rpc'                                   # XEP-0009
     131  NS_RSM            = 'http://jabber.org/protocol/rsm'
     132  NS_SASL           = 'urn:ietf:params:xml:ns:xmpp-sasl'
     133  NS_SECLABEL       = 'urn:xmpp:sec-label:0'
     134  NS_SECLABEL_CATALOG = 'urn:xmpp:sec-label:catalog:2'
     135  NS_SEARCH         = 'jabber:iq:search'
     136  NS_SERVER         = 'jabber:server'
     137  NS_SESSION        = 'urn:ietf:params:xml:ns:xmpp-session'
     138  NS_SI             = 'http://jabber.org/protocol/si'                   # XEP-0096
     139  NS_SI_PUB         = 'http://jabber.org/protocol/sipub'                # XEP-0137
     140  NS_SIGNED         = 'jabber:x:signed'                                 # XEP-0027
     141  NS_SSN            = 'urn:xmpp:ssn'                                    # XEP-0155
     142  NS_STANZA_CRYPTO  = 'http://www.xmpp.org/extensions/xep-0200.html#ns' # XEP-0200
     143  NS_STANZAS        = 'urn:ietf:params:xml:ns:xmpp-stanzas'
     144  NS_STREAM         = 'http://affinix.com/jabber/stream'
     145  NS_STREAMS        = 'http://etherx.jabber.org/streams'
     146  NS_TIME           = 'jabber:iq:time'                                  # XEP-0900
     147  NS_TIME_REVISED   = 'urn:xmpp:time'                                   # XEP-0202
     148  NS_TLS            = 'urn:ietf:params:xml:ns:xmpp-tls'
     149  NS_TUNE           = 'http://jabber.org/protocol/tune'                 # XEP-0118
     150  NS_VACATION       = 'http://jabber.org/protocol/vacation'
     151  NS_VCARD          = 'vcard-temp'
     152  NS_GMAILNOTIFY    = 'google:mail:notify'
     153  NS_GTALKSETTING   = 'google:setting'
     154  NS_VCARD_UPDATE   = NS_VCARD + ':x:update'
     155  NS_VERSION        = 'jabber:iq:version'
     156  NS_VIEWING        = 'http://jabber.org/protocol/viewing'              # XEP--197
     157  NS_WAITINGLIST    = 'http://jabber.org/protocol/waitinglist'          # XEP-0130
     158  NS_XHTML_IM       = 'http://jabber.org/protocol/xhtml-im'             # XEP-0071
     159  NS_XHTML          = 'http://www.w3.org/1999/xhtml'                    # "
     160  NS_X_OOB          = 'jabber:x:oob'                                    # XEP-0066
     161  NS_DATA_LAYOUT    = 'http://jabber.org/protocol/xdata-layout'         # XEP-0141
     162  NS_DATA_VALIDATE  = 'http://jabber.org/protocol/xdata-validate'       # XEP-0122
     163  NS_XMPP_STREAMS   = 'urn:ietf:params:xml:ns:xmpp-streams'
     164  NS_RECEIPTS       = 'urn:xmpp:receipts'
     165  NS_PUBKEY_PUBKEY  = 'urn:xmpp:pubkey:2'                                              # XEP-0189
     166  NS_PUBKEY_REVOKE  = 'urn:xmpp:revoke:2'
     167  NS_PUBKEY_ATTEST  = 'urn:xmpp:attest:2'
     168  NS_STREAM_MGMT    = 'urn:xmpp:sm:2'                                   # XEP-198
     169  NS_HASHES         = 'urn:xmpp:hashes:1'                               # XEP-300
     170  NS_HASHES_MD5     = 'urn:xmpp:hash-function-textual-names:md5'
     171  NS_HASHES_SHA1    = 'urn:xmpp:hash-function-textual-names:sha-1'
     172  NS_HASHES_SHA256  = 'urn:xmpp:hash-function-textual-names:sha-256'
     173  NS_HASHES_SHA512  = 'urn:xmpp:hash-function-textual-names:sha-512'
     174
     175  #xmpp_stream_error_conditions = '''
     176  #bad-format --  --  -- The entity has sent XML that cannot be processed.
     177  #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.
     178  #conflict --  --  -- The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream.
     179  #connection-timeout --  --  -- The entity has not generated any traffic over the stream for some period of time.
     180  #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.
     181  #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.
     182  #improper-addressing --  --  -- A stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value).
     183  #internal-server-error --  --  -- The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.
     184  #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.
     185  #invalid-id --  --  -- The stream ID or dialback ID is invalid or does not match an ID previously provided.
     186  #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".
     187  #invalid-xml --  --  -- The entity has sent invalid XML over the stream to a server that performs validation.
     188  #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.
     189  #policy-violation --  --  -- The entity has violated some local service policy.
     190  #remote-connection-failed --  --  -- The server is unable to properly connect to a remote resource that is required for authentication or authorization.
     191  #resource-constraint --  --  -- The server lacks the system resources necessary to service the stream.
     192  #restricted-xml --  --  -- The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character.
     193  #see-other-host --  --  -- The server will not provide service to the initiating entity but is redirecting traffic to another host.
     194  #system-shutdown --  --  -- The server is being shut down and all active streams are being closed.
     195  #undefined-condition --  --  -- The error condition is not one of those defined by the other conditions in this list.
     196  #unsupported-encoding --  --  -- The initiating entity has encoded the stream in an encoding that is not supported by the server.
     197  #unsupported-stanza-type --  --  -- The initiating entity has sent a first-level child of the stream that is not supported by the server.
     198  #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.
     199  #xml-not-well-formed --  --  -- The initiating entity has sent XML that is not well-formed.'''
     200
     201  #xmpp_stanza_error_conditions = '''
     202  #bad-request -- 400 -- modify -- The sender has sent XML that is malformed or that cannot be processed.
     203  #conflict -- 409 -- cancel -- Access cannot be granted because an existing resource or session exists with the same name or address.
     204  #feature-not-implemented -- 501 -- cancel -- The feature requested is not implemented by the recipient or server and therefore cannot be processed.
     205  #forbidden -- 403 -- auth -- The requesting entity does not possess the required permissions to perform the action.
     206  #gone -- 302 -- modify -- The recipient or server can no longer be contacted at this address.
     207  #internal-server-error -- 500 -- wait -- The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error.
     208  #item-not-found -- 404 -- cancel -- The addressed JID or item requested cannot be found.
     209  #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.
     210  #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.
     211  #not-allowed -- 405 -- cancel -- The recipient or server does not allow any entity to perform the action.
     212  #not-authorized -- 401 -- auth -- The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials.
     213  #payment-required -- 402 -- auth -- The requesting entity is not authorized to access the requested service because payment is required.
     214  #recipient-unavailable -- 404 -- wait -- The intended recipient is temporarily unavailable.
     215  #redirect -- 302 -- modify -- The recipient or server is redirecting requests for this information to another entity.
     216  #registration-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because registration is required.
     217  #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.
     218  #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.
     219  #resource-constraint -- 500 -- wait -- The server or recipient lacks the system resources necessary to service the request.
     220  #service-unavailable -- 503 -- cancel -- The server or recipient does not currently provide the requested service.
     221  #subscription-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because a subscription is required.
     222  #undefined-condition -- 500 --  -- Undefined Condition
     223  #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).'''
     224
     225  #sasl_error_conditions = '''
     226  #aborted --  --  -- The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element.
     227  #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.
     228  #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.
     229  #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.
     230  #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.
     231  #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.
     232  #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.'''
     233
     234  #ERRORS, _errorcodes, loc = {}, {}, {}
     235  #for ns, errname, errpool in ((NS_XMPP_STREAMS, 'STREAM',
     236  #xmpp_stream_error_conditions), (NS_STANZAS, 'ERR', xmpp_stanza_error_conditions),
     237  #(NS_SASL, 'SASL', sasl_error_conditions)):
     238      #for err in errpool.split('\n')[1:]:
     239          #cond, code, typ, text = err.split(' -- ')
     240          #name = errname + '_' + ascii_upper(cond).replace('-', '_')
     241          #locals()[name] = ns + ' ' + cond
     242          #loc[name] = ns + ' ' + cond
     243          #ERRORS[ns + ' ' + cond] = [code, typ, text]
     244          #if code:
     245              #_errorcodes[code] = cond
     246  #del ns, errname, errpool, err, cond, code, typ, text
     247  #import pprint
     248  #pprint.pprint(ERRORS)
     249  #pprint.pprint(_errorcodes)
     250  #for (k, v) in loc.items():
     251       #print('%s = \'%s\'' % (k, v))
     252
     253  ERRORS = {
     254      'urn:ietf:params:xml:ns:xmpp-sasl aborted': ['',
     255          '',
     256          'The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element.'],
     257      'urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding': ['',
     258          '',
     259          '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.'],
     260      'urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid': ['',
     261          '',
     262          '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.'],
     263      'urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism': ['',
     264          '',
     265          '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.'],
     266      'urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak': ['',
     267          '',
     268          '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.'],
     269      'urn:ietf:params:xml:ns:xmpp-sasl not-authorized': ['',
     270          '',
     271          '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.'],
     272      'urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure': ['',
     273          '',
     274          'The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an <auth/> element or <response/> element.'],
     275      'urn:ietf:params:xml:ns:xmpp-stanzas bad-request': ['400',
     276          'modify',
     277          'The sender has sent XML that is malformed or that cannot be processed.'],
     278      'urn:ietf:params:xml:ns:xmpp-stanzas conflict': ['409',
     279          'cancel',
     280          'Access cannot be granted because an existing resource or session exists with the same name or address.'],
     281      'urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented': ['501',
     282          'cancel',
     283          'The feature requested is not implemented by the recipient or server and therefore cannot be processed.'],
     284      'urn:ietf:params:xml:ns:xmpp-stanzas forbidden': ['403',
     285          'auth',
     286          'The requesting entity does not possess the required permissions to perform the action.'],
     287      'urn:ietf:params:xml:ns:xmpp-stanzas gone': ['302',
     288          'modify',
     289          'The recipient or server can no longer be contacted at this address.'],
     290      'urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error': ['500',
     291          'wait',
     292          'The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error.'],
     293      'urn:ietf:params:xml:ns:xmpp-stanzas item-not-found': ['404',
     294          'cancel',
     295          'The addressed JID or item requested cannot be found.'],
     296      'urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed': ['400',
     297          'modify',
     298          "The value of the        'to' attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme."],
     299      'urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable': ['406',
     300          'cancel',
     301          '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.'],
     302      'urn:ietf:params:xml:ns:xmpp-stanzas not-allowed': ['405',
     303          'cancel',
     304          'The recipient or server does not allow any entity to perform the action.'],
     305      'urn:ietf:params:xml:ns:xmpp-stanzas not-authorized': ['401',
     306          'auth',
     307          'The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials.'],
     308      'urn:ietf:params:xml:ns:xmpp-stanzas payment-required': ['402',
     309          'auth',
     310          'The requesting entity is not authorized to access the requested service because payment is required.'],
     311      'urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable': ['404',
     312          'wait',
     313          'The intended recipient is temporarily unavailable.'],
     314      'urn:ietf:params:xml:ns:xmpp-stanzas redirect': ['302',
     315          'modify',
     316          'The recipient or server is redirecting requests for this information to another entity.'],
     317      'urn:ietf:params:xml:ns:xmpp-stanzas registration-required': ['407',
     318          'auth',
     319          'The requesting entity is not authorized to access the requested service because registration is required.'],
     320      'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found': ['404',
     321          'cancel',
     322          'A remote server or service specified as part or all of the JID of the intended recipient does not exist.'],
     323      'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout': ['504',
     324          'wait',
     325          '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.'],
     326      'urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint': ['500',
     327          'wait',
     328          'The server or recipient lacks the system resources necessary to service the request.'],
     329      'urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable': ['503',
     330          'cancel',
     331          'The server or recipient does not currently provide the requested service.'],
     332      'urn:ietf:params:xml:ns:xmpp-stanzas subscription-required': ['407',
     333          'auth',
     334          'The requesting entity is not authorized to access the requested service because a subscription is required.'],
     335      'urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition': ['500',
     336          '',
     337          'Undefined Condition'],
     338      'urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request': ['400',
     339          'wait',
     340          'The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order).'],
     341      'urn:ietf:params:xml:ns:xmpp-streams bad-format': ['',
     342          '',
     343          'The entity has sent XML that cannot be processed.'],
     344      'urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix': ['',
     345          '',
     346          'The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix.'],
     347      'urn:ietf:params:xml:ns:xmpp-streams conflict': ['',
     348          '',
     349          'The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream.'],
     350      'urn:ietf:params:xml:ns:xmpp-streams connection-timeout': ['',
     351          '',
     352          'The entity has not generated any traffic over the stream for some period of time.'],
     353      'urn:ietf:params:xml:ns:xmpp-streams host-gone': ['',
     354          '',
     355          "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."],
     356      'urn:ietf:params:xml:ns:xmpp-streams host-unknown': ['',
     357          '',
     358          "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."],
     359      'urn:ietf:params:xml:ns:xmpp-streams improper-addressing': ['',
     360          '',
     361          "A stanza sent between two servers lacks a        'to' or 'from' attribute (or the attribute has no value)."],
     362      'urn:ietf:params:xml:ns:xmpp-streams internal-server-error': ['',
     363          '',
     364          'The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.'],
     365      'urn:ietf:params:xml:ns:xmpp-streams invalid-from': ['cancel',
     366          '',
     367          "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."],
     368      'urn:ietf:params:xml:ns:xmpp-streams invalid-id': ['',
     369          '',
     370          'The stream ID or dialback ID is invalid or does not match an ID previously provided.'],
     371      'urn:ietf:params:xml:ns:xmpp-streams invalid-namespace': ['',
     372          '',
     373          '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".'],
     374      'urn:ietf:params:xml:ns:xmpp-streams invalid-xml': ['',
     375          '',
     376          'The entity has sent invalid XML over the stream to a server that performs validation.'],
     377      'urn:ietf:params:xml:ns:xmpp-streams not-authorized': ['',
     378          '',
     379          '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.'],
     380      'urn:ietf:params:xml:ns:xmpp-streams policy-violation': ['',
     381          '',
     382          'The entity has violated some local service policy.'],
     383      'urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed': ['',
     384          '',
     385          'The server is unable to properly connect to a remote resource that is required for authentication or authorization.'],
     386      'urn:ietf:params:xml:ns:xmpp-streams resource-constraint': ['',
     387          '',
     388          'The server lacks the system resources necessary to service the stream.'],
     389      'urn:ietf:params:xml:ns:xmpp-streams restricted-xml': ['',
     390          '',
     391          'The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character.'],
     392      'urn:ietf:params:xml:ns:xmpp-streams see-other-host': ['',
     393          '',
     394          'The server will not provide service to the initiating entity but is redirecting traffic to another host.'],
     395      'urn:ietf:params:xml:ns:xmpp-streams system-shutdown': ['',
     396          '',
     397          'The server is being shut down and all active streams are being closed.'],
     398      'urn:ietf:params:xml:ns:xmpp-streams undefined-condition': ['',
     399          '',
     400          'The error condition is not one of those defined by the other conditions in this list.'],
     401      'urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding': ['',
     402          '',
     403          'The initiating entity has encoded the stream in an encoding that is not supported by the server.'],
     404      'urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type': ['',
     405          '',
     406          'The initiating entity has sent a first-level child of the stream that is not supported by the server.'],
     407      'urn:ietf:params:xml:ns:xmpp-streams unsupported-version': ['',
     408          '',
     409          "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."],
     410      'urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed': ['',
     411          '',
     412          'The initiating entity has sent XML that is not well-formed.']
     413  }
     414
     415  _errorcodes = {
     416      '302': 'redirect',
     417      '400': 'unexpected-request',
     418      '401': 'not-authorized',
     419      '402': 'payment-required',
     420      '403': 'forbidden',
     421      '404': 'remote-server-not-found',
     422      '405': 'not-allowed',
     423      '406': 'not-acceptable',
     424      '407': 'subscription-required',
     425      '409': 'conflict',
     426      '500': 'undefined-condition',
     427      '501': 'feature-not-implemented',
     428      '503': 'service-unavailable',
     429      '504': 'remote-server-timeout',
     430      'cancel': 'invalid-from'
     431  }
     432
     433  STREAM_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-streams not-authorized'
     434  STREAM_REMOTE_CONNECTION_FAILED = 'urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed'
     435  SASL_MECHANISM_TOO_WEAK = 'urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak'
     436  STREAM_XML_NOT_WELL_FORMED = 'urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed'
     437  ERR_JID_MALFORMED = 'urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed'
     438  STREAM_SEE_OTHER_HOST = 'urn:ietf:params:xml:ns:xmpp-streams see-other-host'
     439  STREAM_BAD_NAMESPACE_PREFIX = 'urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix'
     440  ERR_SERVICE_UNAVAILABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable'
     441  STREAM_CONNECTION_TIMEOUT = 'urn:ietf:params:xml:ns:xmpp-streams connection-timeout'
     442  STREAM_UNSUPPORTED_VERSION = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-version'
     443  STREAM_IMPROPER_ADDRESSING = 'urn:ietf:params:xml:ns:xmpp-streams improper-addressing'
     444  STREAM_UNDEFINED_CONDITION = 'urn:ietf:params:xml:ns:xmpp-streams undefined-condition'
     445  SASL_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-sasl not-authorized'
     446  ERR_GONE = 'urn:ietf:params:xml:ns:xmpp-stanzas gone'
     447  SASL_TEMPORARY_AUTH_FAILURE = 'urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure'
     448  ERR_REMOTE_SERVER_NOT_FOUND = 'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found'
     449  ERR_UNEXPECTED_REQUEST = 'urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request'
     450  ERR_RECIPIENT_UNAVAILABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable'
     451  ERR_CONFLICT = 'urn:ietf:params:xml:ns:xmpp-stanzas conflict'
     452  STREAM_SYSTEM_SHUTDOWN = 'urn:ietf:params:xml:ns:xmpp-streams system-shutdown'
     453  STREAM_BAD_FORMAT = 'urn:ietf:params:xml:ns:xmpp-streams bad-format'
     454  ERR_SUBSCRIPTION_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas subscription-required'
     455  STREAM_INTERNAL_SERVER_ERROR = 'urn:ietf:params:xml:ns:xmpp-streams internal-server-error'
     456  ERR_NOT_AUTHORIZED = 'urn:ietf:params:xml:ns:xmpp-stanzas not-authorized'
     457  SASL_ABORTED = 'urn:ietf:params:xml:ns:xmpp-sasl aborted'
     458  ERR_REGISTRATION_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas registration-required'
     459  ERR_INTERNAL_SERVER_ERROR = 'urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error'
     460  SASL_INCORRECT_ENCODING = 'urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding'
     461  STREAM_HOST_GONE = 'urn:ietf:params:xml:ns:xmpp-streams host-gone'
     462  STREAM_POLICY_VIOLATION = 'urn:ietf:params:xml:ns:xmpp-streams policy-violation'
     463  STREAM_INVALID_XML = 'urn:ietf:params:xml:ns:xmpp-streams invalid-xml'
     464  STREAM_CONFLICT = 'urn:ietf:params:xml:ns:xmpp-streams conflict'
     465  STREAM_RESOURCE_CONSTRAINT = 'urn:ietf:params:xml:ns:xmpp-streams resource-constraint'
     466  STREAM_UNSUPPORTED_ENCODING = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding'
     467  ERR_NOT_ALLOWED = 'urn:ietf:params:xml:ns:xmpp-stanzas not-allowed'
     468  ERR_ITEM_NOT_FOUND = 'urn:ietf:params:xml:ns:xmpp-stanzas item-not-found'
     469  ERR_NOT_ACCEPTABLE = 'urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable'
     470  STREAM_INVALID_FROM = 'urn:ietf:params:xml:ns:xmpp-streams invalid-from'
     471  ERR_FEATURE_NOT_IMPLEMENTED = 'urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented'
     472  ERR_BAD_REQUEST = 'urn:ietf:params:xml:ns:xmpp-stanzas bad-request'
     473  STREAM_INVALID_ID = 'urn:ietf:params:xml:ns:xmpp-streams invalid-id'
     474  STREAM_HOST_UNKNOWN = 'urn:ietf:params:xml:ns:xmpp-streams host-unknown'
     475  ERR_UNDEFINED_CONDITION = 'urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition'
     476  SASL_INVALID_MECHANISM = 'urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism'
     477  STREAM_RESTRICTED_XML = 'urn:ietf:params:xml:ns:xmpp-streams restricted-xml'
     478  ERR_RESOURCE_CONSTRAINT = 'urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint'
     479  ERR_REMOTE_SERVER_TIMEOUT = 'urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout'
     480  SASL_INVALID_AUTHZID = 'urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid'
     481  ERR_PAYMENT_REQUIRED = 'urn:ietf:params:xml:ns:xmpp-stanzas payment-required'
     482  STREAM_INVALID_NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-streams invalid-namespace'
     483  ERR_REDIRECT = 'urn:ietf:params:xml:ns:xmpp-stanzas redirect'
     484  STREAM_UNSUPPORTED_STANZA_TYPE = 'urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type'
     485  ERR_FORBIDDEN = 'urn:ietf:params:xml:ns:xmpp-stanzas forbidden'
     486
     487 -def isResultNode(node):
     488      """
     489      Return true if the node is a positive reply
     490      """
     491      return node and node.getType() == 'result'
     492
     493 -def isErrorNode(node):
     494      """
     495      Return true if the node is a negative reply
     496      """
     497      return node and node.getType() == 'error'
     498
     499 -class NodeProcessed(Exception):
     500      """
     501      Exception that should be raised by handler when the handling should be
     502      stopped
     503      """
     504      pass
     505
     506 -class StreamError(Exception):
     507      """
     508      Base exception class for stream errors
     509      """
     510      pass
     511
     512 -class BadFormat(StreamError):
     513      pass
     514
     515 -class BadNamespacePrefix(StreamError):
     516      pass
     517
     518 -class Conflict(StreamError):
     519      pass
     520
     521 -class ConnectionTimeout(StreamError):
     522      pass
     523
     524 -class HostGone(StreamError):
     525      pass
     526
     527 -class HostUnknown(StreamError):
     528      pass
     529
     530 -class ImproperAddressing(StreamError):
     531      pass
     532
     533 -class InternalServerError(StreamError):
     534      pass
     535
     536 -class InvalidFrom(StreamError):
     537      pass
     538
     539 -class InvalidID(StreamError):
     540      pass
     541
     542 -class InvalidNamespace(StreamError):
     543      pass
     544
     545 -class InvalidXML(StreamError):
     546      pass
     547
     548 -class NotAuthorized(StreamError):
     549      pass
     550
     551 -class PolicyViolation(StreamError):
     552      pass
     553
     554 -class RemoteConnectionFailed(StreamError):
     555      pass
     556
     557 -class ResourceConstraint(StreamError):
     558      pass
     559
     560 -class RestrictedXML(StreamError):
     561      pass
     562
     563 -class SeeOtherHost(StreamError):
     564      pass
     565
     566 -class SystemShutdown(StreamError):
     567      pass
     568
     569 -class UndefinedCondition(StreamError):
     570      pass
     571
     572 -class UnsupportedEncoding(StreamError):
     573      pass
     574
     575 -class UnsupportedStanzaType(StreamError):
     576      pass
     577
     578 -class UnsupportedVersion(StreamError):
     579      pass
     580
     581 -class XMLNotWellFormed(StreamError):
     582      pass
     583
     584  stream_exceptions = {'bad-format': BadFormat,
     585                      'bad-namespace-prefix': BadNamespacePrefix,
     586                      'conflict': Conflict,
     587                      'connection-timeout': ConnectionTimeout,
     588                      'host-gone': HostGone,
     589                      'host-unknown': HostUnknown,
     590                      'improper-addressing': ImproperAddressing,
     591                      'internal-server-error': InternalServerError,
     592                      'invalid-from': InvalidFrom,
     593                      'invalid-id': InvalidID,
     594                      'invalid-namespace': InvalidNamespace,
     595                      'invalid-xml': InvalidXML,
     596                      'not-authorized': NotAuthorized,
     597                      'policy-violation': PolicyViolation,
     598                      'remote-connection-failed': RemoteConnectionFailed,
     599                      'resource-constraint': ResourceConstraint,
     600                      'restricted-xml': RestrictedXML,
     601                      'see-other-host': SeeOtherHost,
     602                      'system-shutdown': SystemShutdown,
     603                      'undefined-condition': UndefinedCondition,
     604                      'unsupported-encoding': UnsupportedEncoding,
     605                      'unsupported-stanza-type': UnsupportedStanzaType,
     606                      'unsupported-version': UnsupportedVersion,
     607                      'xml-not-well-formed': XMLNotWellFormed}
     608
     609 -class JID:
     610      """
     611      JID can be built from string, modified, compared, serialised into string
     612      """
     613
     614 -    def __init__(self, jid=None, node='', domain='', resource=''):
     615          """
     616          JID can be specified as string (jid argument) or as separate parts
     617
     618          Examples:
     619          JID('node@domain/resource')
     620          JID(node='node',domain='domain.org')
     621          """
     622          if not jid and not domain:
     623              raise ValueError('JID must contain at least domain name')
     624          elif type(jid) == type(self):
     625              self.node, self.domain = jid.node, jid.domain
     626              self.resource = jid.resource
     627          elif domain:
     628              self.node, self.domain, self.resource = node, domain, resource
     629          else:
     630              if jid.find('@') + 1:
     631                  self.node, jid = jid.split('@', 1)
     632              else:
     633                  self.node = ''
     634              if jid.find('/')+1:
     635                  self.domain, self.resource = jid.split('/', 1)
     636              else:
     637                  self.domain, self.resource = jid, ''
     638
     639 -    def getNode(self):
     640          """
     641          Return the node part of the JID
     642          """
     643          return self.node
     644
     645 -    def setNode(self, node):
     646          """
     647          Set the node part of the JID to new value. Specify None to remove
     648          the node part
     649          """
     650          self.node = node.lower()
     651
     652 -    def getDomain(self):
     653          """
     654          Return the domain part of the JID
     655          """
     656          return self.domain
     657
     658 -    def setDomain(self, domain):
     659          """
     660          Set the domain part of the JID to new value
     661          """
     662          self.domain = domain.lower()
     663
     664 -    def getResource(self):
     665          """
     666          Return the resource part of the JID
     667          """
     668          return self.resource
     669
     670 -    def setResource(self, resource):
     671          """
     672          Set the resource part of the JID to new value. Specify None to remove the
     673          resource part
     674          """
     675          self.resource = resource
     676
     677 -    def getStripped(self):
     678          """
     679          Return the bare representation of JID. I.e. string value w/o resource
     680          """
     681          return self.__str__(0)
     682
     683 -    def __eq__(self, other):
     684          """
     685          Compare the JID to another instance or to string for equality
     686          """
     687          try:
     688              other = JID(other)
     689          except ValueError:
     690              return 0
     691          return self.resource == other.resource and \
     692              self.__str__(0) == other.__str__(0)
     693
     694 -    def __ne__(self, other):
     695          """
     696          Compare the JID to another instance or to string for non-equality
     697          """
     698          return not self.__eq__(other)
     699
     700 -    def bareMatch(self, other):
     701          """
     702          Compare the node and domain parts of the JID's for equality
     703          """
     704          return self.__str__(0) == JID(other).__str__(0)
     705
     706 -    def __str__(self, wresource=1):
     707          """
     708          Serialise JID into string
     709          """
     710          if self.node:
     711              jid = self.node + '@' + self.domain
     712          else:
     713              jid = self.domain
     714          if wresource and self.resource:
     715              return jid + '/' + self.resource
     716          return jid
     717
     718 -    def __hash__(self):
     719          """
     720          Produce hash of the JID, Allows to use JID objects as keys of the
     721          dictionary
     722          """
     723          return hash(str(self))
     724
     725 -class BOSHBody(Node):
     726      """
     727      <body> tag that wraps usual XMPP stanzas in XMPP over BOSH
     728      """
     729
     730 -    def __init__(self, attrs={}, payload=[], node=None):
     731          Node.__init__(self, tag='body', attrs=attrs, payload=payload, node=node)
     732          self.setNamespace(NS_HTTP_BIND)
     733
     734
     735 -class Protocol(Node):
     736      """
     737      A "stanza" object class. Contains methods that are common for presences, iqs
     738      and messages
     739      """
     740
     741 -    def __init__(self, name=None, to=None, typ=None, frm=None, attrs={},
     742                      payload=[], timestamp=None, xmlns=None, node=None):
     743          """
     744          Constructor, name is the name of the stanza
     745          i.e. 'message' or 'presence'or 'iq'
     746
     747          to is the value of 'to' attribure, 'typ' - 'type' attribute
     748          frn - from attribure, attrs - other attributes mapping,
     749          payload - same meaning as for simplexml payload definition
     750          timestamp - the time value that needs to be stamped over stanza
     751          xmlns - namespace of top stanza node
     752          node - parsed or unparsed stana to be taken as prototype.
     753          """
     754          if not attrs:
     755              attrs = {}
     756          if to:
     757              attrs['to'] = to
     758          if frm:
     759              attrs['from'] = frm
     760          if typ:
     761              attrs['type'] = typ
     762          Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node)
     763          if not node and xmlns:
     764              self.setNamespace(xmlns)
     765          if self['to']:
     766              self.setTo(self['to'])
     767          if self['from']:
     768              self.setFrom(self['from'])
     769          if node and type(self) == type(node) and \
     770          self.__class__ == node.__class__ and self.attrs.has_key('id'):
     771              del self.attrs['id']
     772          self.timestamp = None
     773          for d in self.getTags('delay', namespace=NS_DELAY2):
     774              try:
     775                  if d.getAttr('stamp') < self.getTimestamp2():
     776                      self.setTimestamp(d.getAttr('stamp'))
     777              except Exception:
     778                  pass
     779          if not self.timestamp:
     780              for x in self.getTags('x', namespace=NS_DELAY):
     781                  try:
     782                      if x.getAttr('stamp') < self.getTimestamp():
     783                          self.setTimestamp(x.getAttr('stamp'))
     784                  except Exception:
     785                      pass
     786          if timestamp is not None:
     787              self.setTimestamp(timestamp)  # To auto-timestamp stanza just pass timestamp=''
     788
     789 -    def getTo(self):
     790          """
     791          Return value of the 'to' attribute
     792          """
     793          try:
     794              return self['to']
     795          except:
     796              return None
     797
     798 -    def getFrom(self):
     799          """
     800          Return value of the 'from' attribute
     801          """
     802          try:
     803              return self['from']
     804          except:
     805              return None
     806
     807 -    def getTimestamp(self):
     808          """
     809          Return the timestamp in the 'yyyymmddThhmmss' format
     810          """
     811          if self.timestamp:
     812              return self.timestamp
     813          return time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())
     814
     815 -    def getTimestamp2(self):
     816          """
     817          Return the timestamp in the 'yyyymmddThhmmss' format
     818          """
     819          if self.timestamp:
     820              return self.timestamp
     821          return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
     822
     823 -    def getID(self):
     824          """
     825          Return the value of the 'id' attribute
     826          """
     827          return self.getAttr('id')
     828
     829 -    def setTo(self, val):
     830          """
     831          Set the value of the 'to' attribute
     832          """
     833          self.setAttr('to', JID(val))
     834
     835 -    def getType(self):
     836          """
     837          Return the value of the 'type' attribute
     838          """
     839          return self.getAttr('type')
     840
     841 -    def setFrom(self, val):
     842          """
     843          Set the value of the 'from' attribute
     844          """
     845          self.setAttr('from', JID(val))
     846
     847 -    def setType(self, val):
     848          """
     849          Set the value of the 'type' attribute
     850          """
     851          self.setAttr('type', val)
     852
     853 -    def setID(self, val):
     854          """
     855          Set the value of the 'id' attribute
     856          """
     857          self.setAttr('id', val)
     858
     859 -    def getError(self):
     860          """
     861          Return the error-condition (if present) or the textual description
     862          of the error (otherwise)
     863          """
     864          errtag = self.getTag('error')
     865          if errtag:
     866              for tag in errtag.getChildren():
     867                  if tag.getName() != 'text':
     868                      return tag.getName()
     869              return errtag.getData()
     870
     871 -    def getErrorMsg(self):
     872          """
     873          Return the textual description of the error (if present)
     874          or the error condition
     875          """
     876          errtag = self.getTag('error')
     877          if errtag:
     878              for tag in errtag.getChildren():
     879                  if tag.getName() == 'text':
     880                      return tag.getData()
     881              return self.getError()
     882
     883 -    def getErrorCode(self):
     884          """
     885          Return the error code. Obsolete.
     886          """
     887          return self.getTagAttr('error', 'code')
     888
     889 -    def getStatusConditions(self):
     890          """
     891          Return the status conditions list as defined in XEP-0306.
     892          """
     893          conds = []
     894          condtag = self.getTag('conditions', namespace=NS_CONDITIONS)
     895          if condtag:
     896              for tag in condtag.getChildren():
     897                  conds.append(tag.getName())
     898          return conds
     899
     900 -    def setError(self, error, code=None):
     901          """
     902          Set the error code. Obsolete. Use error-conditions instead
     903          """
     904          if code:
     905              if str(code) in _errorcodes.keys():
     906                  error = ErrorNode(_errorcodes[str(code)], text=error)
     907              else:
     908                  error = ErrorNode(ERR_UNDEFINED_CONDITION, code=code,
     909                      typ='cancel', text=error)
     910          elif type(error) in [type(''), type(u'')]:
     911              error=ErrorNode(error)
     912          self.setType('error')
     913          self.addChild(node=error)
     914
     915 -    def setTimestamp(self, val=None):
     916          """
     917          Set the timestamp. timestamp should be the yyyymmddThhmmss string
     918          """
     919          if not val:
     920              val = time.strftime('%Y%m%dT%H:%M:%S', time.gmtime())
     921          self.timestamp=val
     922          self.setTag('x', {'stamp': self.timestamp}, namespace=NS_DELAY)
     923
     924 -    def getProperties(self):
     925          """
     926          Return the list of namespaces to which belongs the direct childs of element
     927          """
     928          props = []
     929          for child in self.getChildren():
     930              prop = child.getNamespace()
     931              if prop not in props:
     932                  props.append(prop)
     933          return props
     934
     935 -    def __setitem__(self, item, val):
     936          """
     937          Set the item 'item' to the value 'val'
     938          """
     939          if item in ['to', 'from']:
     940              val = JID(val)
     941          return self.setAttr(item, val)
     942
     943
     944 -class Message(Protocol):
     945      """
     946      XMPP Message stanza - "push" mechanism
     947      """
     948
     949 -    def __init__(self, to=None, body=None, xhtml=None, typ=None, subject=None,
     950          attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT,
     951          node=None):
     952          """
     953          You can specify recipient, text of message, type of message any
     954          additional attributes, sender of the message, any additional payload
     955          (f.e. jabber:x:delay element) and namespace in one go.
     956
     957          Alternatively you can pass in the other XML object as the 'node'
     958          parameted to replicate it as message
     959          """
     960          Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm,
     961                  payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
     962          if body:
     963              self.setBody(body)
     964          if xhtml:
     965              self.setXHTML(xhtml)
     966          if subject is not None:
     967              self.setSubject(subject)
     968
     969 -    def getBody(self):
     970          """
     971          Return text of the message
     972          """
     973          return self.getTagData('body')
     974
     975 -    def getXHTML(self, xmllang=None):
     976          """
     977          Return serialized xhtml-im element text of the message
     978
     979          TODO: Returning a DOM could make rendering faster.
     980          """
     981          xhtml = self.getTag('html')
     982          if xhtml:
     983              if xmllang:
     984                  body = xhtml.getTag('body', attrs={'xml:lang': xmllang})
     985              else:
     986                  body = xhtml.getTag('body')
     987              return str(body)
     988          return None
     989
     990 -    def getSubject(self):
     991          """
     992          Return subject of the message
     993          """
     994          return self.getTagData('subject')
     995
     996 -    def getThread(self):
     997          """
     998          Return thread of the message
     999          """
    1000          return self.getTagData('thread')
    1001
    1002 -    def setBody(self, val):
    1003          """
    1004          Set the text of the message"""
    1005          self.setTagData('body', val)
    1006
    1007 -    def setXHTML(self, val, xmllang=None):
    1008          """
    1009          Sets the xhtml text of the message (XEP-0071). The parameter is the
    1010          "inner html" to the body.
    1011          """
    1012          try:
    1013              if xmllang:
    1014                  dom = NodeBuilder('<body xmlns="%s" xml:lang="%s">%s</body>' \
    1015                      % (NS_XHTML, xmllang, val)).getDom()
    1016              else:
    1017                  dom = NodeBuilder('<body xmlns="%s">%s</body>' % (NS_XHTML,
    1018                      val), 0).getDom()
    1019              if self.getTag('html'):
    1020                  self.getTag('html').addChild(node=dom)
    1021              else:
    1022                  self.setTag('html', namespace=NS_XHTML_IM).addChild(node=dom)
    1023          except Exception, e:
    1024              print "Error", e
    1025              # FIXME: log. we could not set xhtml (parse error, whatever)
    1026
    1027 -    def setSubject(self, val):
    1028          """
    1029          Set the subject of the message
    1030          """
    1031          self.setTagData('subject', val)
    1032
    1033 -    def setThread(self, val):
    1034          """
    1035          Set the thread of the message
    1036          """
    1037          self.setTagData('thread', val)
    1038
    1039 -    def buildReply(self, text=None):
    1040          """
    1041          Builds and returns another message object with specified text. The to,
    1042          from, thread and type properties of new message are pre-set as reply to
    1043          this message
    1044          """
    1045          m = Message(to=self.getFrom(), frm=self.getTo(), body=text,
    1046              typ=self.getType())
    1047          th = self.getThread()
    1048          if th:
    1049              m.setThread(th)
    1050          return m
    1051
    1052 -    def getStatusCode(self):
    1053          """
    1054          Return the status code of the message (for groupchat config change)
    1055          """
    1056          attrs = []
    1057          for xtag in self.getTags('x'):
    1058              for child in xtag.getTags('status'):
    1059                  attrs.append(child.getAttr('code'))
    1060          return attrs
    1061
    1062 -class Presence(Protocol):
    1063
    1064 -    def __init__(self, to=None, typ=None, priority=None, show=None, status=None,
    1065          attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT,
    1066          node=None):
    1067          """
    1068          You can specify recipient, type of message, priority, show and status
    1069          values any additional attributes, sender of the presence, timestamp, any
    1070          additional payload (f.e. jabber:x:delay element) and namespace in one go.
    1071          Alternatively you can pass in the other XML object as the 'node'
    1072          parameted to replicate it as presence
    1073          """
    1074          Protocol.__init__(self, 'presence', to=to, typ=typ, attrs=attrs, frm=frm,
    1075                  payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
    1076          if priority:
    1077              self.setPriority(priority)
    1078          if show:
    1079              self.setShow(show)
    1080          if status:
    1081              self.setStatus(status)
    1082
    1083 -    def getPriority(self):
    1084          """
    1085          Return the priority of the message
    1086          """
    1087          return self.getTagData('priority')
    1088
    1089 -    def getShow(self):
    1090          """
    1091          Return the show value of the message
    1092          """
    1093          return self.getTagData('show')
    1094
    1095 -    def getStatus(self):
    1096          """
    1097          Return the status string of the message
    1098          """
    1099          return self.getTagData('status')
    1100
    1101 -    def setPriority(self, val):
    1102          """
    1103          Set the priority of the message
    1104          """
    1105          self.setTagData('priority', val)
    1106
    1107 -    def setShow(self, val):
    1108          """
    1109          Set the show value of the message
    1110          """
    1111          self.setTagData('show', val)
    1112
    1113 -    def setStatus(self, val):
    1114          """
    1115          Set the status string of the message
    1116          """
    1117          self.setTagData('status', val)
    1118
    1119 -    def _muc_getItemAttr(self, tag, attr):
    1120          for xtag in self.getTags('x'):
    1121              if xtag.getNamespace() not in (NS_MUC_USER, NS_MUC_ADMIN):
    1122                  continue
    1123              for child in xtag.getTags(tag):
    1124                  return child.getAttr(attr)
    1125
    1126 -    def _muc_getSubTagDataAttr(self, tag, attr):
    1127          for xtag in self.getTags('x'):
    1128              if xtag.getNamespace() not in (NS_MUC_USER, NS_MUC_ADMIN):
    1129                  continue
    1130              for child in xtag.getTags('item'):
    1131                  for cchild in child.getTags(tag):
    1132                      return cchild.getData(), cchild.getAttr(attr)
    1133          return None, None
    1134
    1135 -    def getRole(self):
    1136          """
    1137          Return the presence role (for groupchat)
    1138          """
    1139          return self._muc_getItemAttr('item', 'role')
    1140
    1141 -    def getAffiliation(self):
    1142          """
    1143          Return the presence affiliation (for groupchat)
    1144          """
    1145          return self._muc_getItemAttr('item', 'affiliation')
    1146
    1147 -    def getNewNick(self):
    1148          """
    1149          Return the status code of the presence (for groupchat)
    1150          """
    1151          return self._muc_getItemAttr('item', 'nick')
    1152
    1153 -    def getJid(self):
    1154          """
    1155          Return the presence jid (for groupchat)
    1156          """
    1157          return self._muc_getItemAttr('item', 'jid')
    1158
    1159 -    def getReason(self):
    1160          """
    1161          Returns the reason of the presence (for groupchat)
    1162          """
    1163          return self._muc_getSubTagDataAttr('reason', '')[0]
    1164
    1165 -    def getActor(self):
    1166          """
    1167          Return the reason of the presence (for groupchat)
    1168          """
    1169          return self._muc_getSubTagDataAttr('actor', 'jid')[1]
    1170
    1171 -    def getStatusCode(self):
    1172          """
    1173          Return the status code of the presence (for groupchat)
    1174          """
    1175          attrs = []
    1176          for xtag in self.getTags('x'):
    1177              for child in xtag.getTags('status'):
    1178                  attrs.append(child.getAttr('code'))
    1179          return attrs
    1180
    1181 -class Iq(Protocol):
    1182      """
    1183      XMPP Iq object - get/set dialog mechanism
    1184      """
    1185
    1186 -    def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None,
    1187                      payload=[], xmlns=NS_CLIENT, node=None):
    1188          """
    1189          You can specify type, query namespace any additional attributes,
    1190          recipient of the iq, sender of the iq, any additional payload (f.e.
    1191          jabber:x:data node) and namespace in one go.
    1192
    1193          Alternatively you can pass in the other XML object as the 'node'
    1194          parameted to replicate it as an iq
    1195          """
    1196          Protocol.__init__(self, 'iq', to=to, typ=typ, attrs=attrs, frm=frm,
    1197              xmlns=xmlns, node=node)
    1198          if payload:
    1199              self.setQueryPayload(payload)
    1200          if queryNS:
    1201              self.setQueryNS(queryNS)
    1202
    1203 -    def getQuery(self):
    1204          """
    1205          Return the IQ's child element if it exists, None otherwise.
    1206          """
    1207          children = self.getChildren()
    1208          if children and self.getType() != 'error' and \
    1209          children[0].getName() != 'error':
    1210              return children[0]
    1211
    1212 -    def getQueryNS(self):
    1213          """
    1214          Return the namespace of the 'query' child element
    1215          """
    1216          tag = self.getQuery()
    1217          if tag:
    1218              return tag.getNamespace()
    1219
    1220 -    def getQuerynode(self):
    1221          """
    1222          Return the 'node' attribute value of the 'query' child element
    1223          """
    1224          tag = self.getQuery()
    1225          if tag:
    1226              return tag.getAttr('node')
    1227
    1228 -    def getQueryPayload(self):
    1229          """
    1230          Return the 'query' child element payload
    1231          """
    1232          tag = self.getQuery()
    1233          if tag:
    1234              return tag.getPayload()
    1235
    1236 -    def getQueryChildren(self):
    1237          """
    1238          Return the 'query' child element child nodes
    1239          """
    1240          tag = self.getQuery()
    1241          if tag:
    1242              return tag.getChildren()
    1243
    1244 -    def setQuery(self, name=None):
    1245          """
    1246          Change the name of the query node, creating it if needed. Keep the
    1247          existing name if none is given (use 'query' if it's a creation).
    1248          Return the query node.
    1249          """
    1250          query = self.getQuery()
    1251          if query is None:
    1252              query = self.addChild('query')
    1253          if name is not None:
    1254              query.setName(name)
    1255          return query
    1256
    1257 -    def setQueryNS(self, namespace):
    1258          """
    1259          Set the namespace of the 'query' child element
    1260          """
    1261          self.setQuery().setNamespace(namespace)
    1262
    1263 -    def setQueryPayload(self, payload):
    1264          """
    1265          Set the 'query' child element payload
    1266          """
    1267          self.setQuery().setPayload(payload)
    1268
    1269 -    def setQuerynode(self, node):
    1270          """
    1271          Set the 'node' attribute value of the 'query' child element
    1272          """
    1273          self.setQuery().setAttr('node', node)
    1274
    1275 -    def buildReply(self, typ):
    1276          """
    1277          Build and return another Iq object of specified type. The to, from and
    1278          query child node of new Iq are pre-set as reply to this Iq.
    1279          """
    1280          iq = Iq(typ, to=self.getFrom(), frm=self.getTo(),
    1281              attrs={'id': self.getID()})
    1282          iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS())
    1283          return iq
    1284
    1285 -class Hashes(Node):
    1286      """
    1287      Hash elements for various XEPs as defined in XEP-300
    1288      """
    1289
    1290      """
    1291      RECOMENDED HASH USE:
    1292      Algorithm     Support
    1293      MD2           MUST NOT
    1294      MD4           MUST NOT
    1295      MD5           MAY
    1296      SHA-1         MUST
    1297      SHA-256       MUST
    1298      SHA-512       SHOULD
    1299      """
    1300
    1301      supported = ('md5', 'sha-1', 'sha-256', 'sha-512')
    1302
    1303 -    def __init__(self, nsp=NS_HASHES):
    1304          Node.__init__(self, None, {}, [], None, None, False, None)
    1305          self.setNamespace(nsp)
    1306          self.setName('hash')
    1307
    1308 -    def calculateHash(self, algo, file_string):
    1309          """
    1310          Calculate the hash and add it. It is preferable doing it here
    1311          instead of doing it all over the place in Gajim.
    1312          """
    1313          hl = None
    1314          hash_ = None
    1315          # file_string can be a string or a file
    1316          if type(file_string) == str: # if it is a string
    1317              if algo == 'sha-1':
    1318                  hl = hashlib.sha1()
    1319              elif algo == 'md5':
    1320                  hl = hashlib.md5()
    1321              elif algo == 'sha-256':
    1322                  hl = hashlib.sha256()
    1323              elif algo == 'sha-512':
    1324                  hl = hashlib.sha512()
    1325              if hl:
    1326                  hl.update(file_string)
    1327                  hash_ = hl.hexdigest()
    1328          else: # if it is a file
    1329              if algo == 'sha-1':
    1330                  hl = hashlib.sha1()
    1331              elif algo == 'md5':
    1332                  hl = hashlib.md5()
    1333              elif algo == 'sha-256':
    1334                  hl = hashlib.sha256()
    1335              elif algo == 'sha-512':
    1336                  hl = hashlib.sha512()
    1337              if hl:
    1338                  for line in file_string:
    1339                      hl.update(line)
    1340                  hash_ = hl.hexdigest()
    1341          return hash_
    1342
    1343 -    def addHash(self, hash_, algo):
    1344          self.setAttr('algo', algo)
    1345          self.setData(hash_)
    1346
    1347 -class Acks(Node):
    1348      """
    1349      Acknowledgement elements for Stream Management
    1350      """
    1351 -    def __init__(self, nsp=NS_STREAM_MGMT):
    1352          Node.__init__(self, None, {}, [], None, None, False, None)
    1353          self.setNamespace(nsp)
    1354
    1355 -    def buildAnswer(self, handled):
    1356          """
    1357          handled is the number of stanzas handled
    1358          """
    1359          self.setName('a')
    1360          self.setAttr('h', handled)
    1361
    1362 -    def buildRequest(self):
    1363          self.setName('r')
    1364
    1365 -    def buildEnable(self, resume=False):
    1366          self.setName('enable')
    1367          if resume:
    1368              self.setAttr('resume', 'true')
    1369
    1370 -    def buildResume(self, handled, previd):
    1371          self.setName('resume')
    1372          self.setAttr('h', handled)
    1373          self.setAttr('previd', previd)
    1374
    1375 -class ErrorNode(Node):
    1376      """
    1377      XMPP-style error element
    1378
    1379      In the case of stanza error should be attached to XMPP stanza.
    1380      In the case of stream-level errors should be used separately.
    1381      """
    1382
    1383 -    def __init__(self, name, code=None, typ=None, text=None):
    1384          """
    1385          Mandatory parameter: name - name of error condition.
    1386          Optional parameters: code, typ, text.
    1387          Used for backwards compartibility with older jabber protocol.
    1388          """
    1389          if name in ERRORS:
    1390              cod, type_, txt = ERRORS[name]
    1391              ns = name.split()[0]
    1392          else:
    1393              cod, ns, type_, txt = '500', NS_STANZAS, 'cancel', ''
    1394          if typ:
    1395              type_ = typ
    1396          if code:
    1397              cod = code
    1398          if text:
    1399              txt = text
    1400          Node.__init__(self, 'error', {}, [Node(name)])
    1401          if type_:
    1402              self.setAttr('type', type_)
    1403          if not cod:
    1404              self.setName('stream:error')
    1405          if txt:
    1406              self.addChild(node=Node(ns + ' text', {}, [txt]))
    1407          if cod:
    1408              self.setAttr('code', cod)
    1409
    1410 -class Error(Protocol):
    1411      """
    1412      Used to quickly transform received stanza into error reply
    1413      """
    1414
    1415 -    def __init__(self, node, error, reply=1):
    1416          """
    1417          Create error reply basing on the received 'node' stanza and the 'error'
    1418          error condition
    1419
    1420          If the 'node' is not the received stanza but locally created ('to' and
    1421          'from' fields needs not swapping) specify the 'reply' argument as false.
    1422          """
    1423          if reply:
    1424              Protocol.__init__(self, to=node.getFrom(), frm=node.getTo(), node=node)
    1425          else:
    1426              Protocol.__init__(self, node=node)
    1427          self.setError(error)
    1428          if node.getType() == 'error':
    1429              self.__str__ = self.__dupstr__
    1430
    1431 -    def __dupstr__(self, dup1=None, dup2=None):
    1432          """
    1433          Dummy function used as preventor of creating error node in reply to error
    1434          node. I.e. you will not be able to serialise "double" error into string.
    1435          """
    1436          return ''
    1437
    1438 -class DataField(Node):
    1439      """
    1440      This class is used in the DataForm class to describe the single data item
    1441
    1442      If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) then
    1443      you will need to work with instances of this class.
    1444      """
    1445
    1446 -    def __init__(self, name=None, value=None, typ=None, required=0, desc=None,
    1447                      options=[], node=None):
    1448          """
    1449          Create new data field of specified name,value and type
    1450
    1451          Also 'required','desc' and 'options' fields can be set. Alternatively
    1452          other XML object can be passed in as the 'node' parameted
    1453          to replicate it as a new datafiled.
    1454          """
    1455          Node.__init__(self, 'field', node=node)
    1456          if name:
    1457              self.setVar(name)
    1458          if isinstance(value, (list, tuple)):
    1459              self.setValues(value)
    1460          elif value:
    1461              self.setValue(value)
    1462          if typ:
    1463              self.setType(typ)
    1464          elif not typ and not node:
    1465              self.setType('text-single')
    1466          if required:
    1467              self.setRequired(required)
    1468          if desc:
    1469              self.setDesc(desc)
    1470          if options:
    1471              self.setOptions(options)
    1472
    1473 -    def setRequired(self, req=1):
    1474          """
    1475          Change the state of the 'required' flag
    1476          """
    1477          if req:
    1478              self.setTag('required')
    1479          else:
    1480              try:
    1481                  self.delChild('required')
    1482              except ValueError:
    1483                  return
    1484
    1485 -    def isRequired(self):
    1486          """
    1487          Return in this field a required one
    1488          """
    1489          return self.getTag('required')
    1490
    1491 -    def setDesc(self, desc):
    1492          """
    1493          Set the description of this field
    1494          """
    1495          self.setTagData('desc', desc)
    1496
    1497 -    def getDesc(self):
    1498          """
    1499          Return the description of this field
    1500          """
    1501          return self.getTagData('desc')
    1502
    1503 -    def setValue(self, val):
    1504          """
    1505          Set the value of this field
    1506          """
    1507          self.setTagData('value', val)
    1508
    1509 -    def getValue(self):
    1510          return self.getTagData('value')
    1511
    1512 -    def setValues(self, lst):
    1513          """
    1514          Set the values of this field as values-list. Replaces all previous filed
    1515          values! If you need to just add a value - use addValue method
    1516          """
    1517          while self.getTag('value'):
    1518              self.delChild('value')
    1519          for val in lst:
    1520              self.addValue(val)
    1521
    1522 -    def addValue(self, val):
    1523          """
    1524          Add one more value to this field. Used in 'get' iq's or such
    1525          """
    1526          self.addChild('value', {}, [val])
    1527
    1528 -    def getValues(self):
    1529          """
    1530          Return the list of values associated with this field
    1531          """
    1532          ret = []
    1533          for tag in self.getTags('value'):
    1534              ret.append(tag.getData())
    1535          return ret
    1536
    1537 -    def getOptions(self):
    1538          """
    1539          Return label-option pairs list associated with this field
    1540          """
    1541          ret = []
    1542          for tag in self.getTags('option'):
    1543              ret.append([tag.getAttr('label'), tag.getTagData('value')])
    1544          return ret
    1545
    1546 -    def setOptions(self, lst):
    1547          """
    1548          Set label-option pairs list associated with this field
    1549          """
    1550          while self.getTag('option'):
    1551              self.delChild('option')
    1552          for opt in lst:
    1553              self.addOption(opt)
    1554
    1555 -    def addOption(self, opt):
    1556          """
    1557          Add one more label-option pair to this field
    1558          """
    1559          if isinstance(opt, basestring):
    1560              self.addChild('option').setTagData('value', opt)
    1561          else:
    1562              self.addChild('option', {'label': opt[0]}).setTagData('value',
    1563                  opt[1])
    1564
    1565 -    def getType(self):
    1566          """
    1567          Get type of this field
    1568          """
    1569          return self.getAttr('type')
    1570
    1571 -    def setType(self, val):
    1572          """
    1573          Set type of this field
    1574          """
    1575          return self.setAttr('type', val)
    1576
    1577 -    def getVar(self):
    1578          """
    1579          Get 'var' attribute value of this field
    1580          """
    1581          return self.getAttr('var')
    1582
    1583 -    def setVar(self, val):
    1584          """
    1585          Set 'var' attribute value of this field
    1586          """
    1587          return self.setAttr('var', val)
    1588
    1589 -class DataForm(Node):
    1590      """
    1591      Used for manipulating dataforms in XMPP
    1592
    1593      Relevant XEPs: 0004, 0068, 0122. Can be used in disco, pub-sub and many
    1594      other applications.
    1595      """
    1596 -    def __init__(self, typ=None, data=[], title=None, node=None):
    1597          """
    1598          Create new dataform of type 'typ'. 'data' is the list of DataField
    1599          instances that this dataform contains, 'title' - the title string.  You
    1600          can specify the 'node' argument as the other node to be used as base for
    1601          constructing this dataform
    1602
    1603          title and instructions is optional and SHOULD NOT contain newlines.
    1604          Several instructions MAY be present.
    1605          'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' )
    1606          'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) respectively.
    1607          'cancel' form can not contain any fields. All other forms contains AT LEAST one field.
    1608          'title' MAY be included in forms of type "form" and "result"
    1609          """
    1610          Node.__init__(self, 'x', node=node)
    1611          if node:
    1612              newkids = []
    1613              for n in self.getChildren():
    1614                  if n.getName() == 'field':
    1615                      newkids.append(DataField(node=n))
    1616                  else:
    1617                      newkids.append(n)
    1618              self.kids = newkids
    1619          if typ:
    1620              self.setType(typ)
    1621          self.setNamespace(NS_DATA)
    1622          if title:
    1623              self.setTitle(title)
    1624          if isinstance(data, dict):
    1625              newdata = []
    1626              for name in data.keys():
    1627                  newdata.append(DataField(name, data[name]))
    1628              data = newdata
    1629          for child in data:
    1630              if isinstance(child, basestring):
    1631                  self.addInstructions(child)
    1632              elif child.__class__.__name__ == 'DataField':
    1633                  self.kids.append(child)
    1634              else:
    1635                  self.kids.append(DataField(node=child))
    1636
    1637 -    def getType(self):
    1638          """
    1639          Return the type of dataform
    1640          """
    1641          return self.getAttr('type')
    1642
    1643 -    def setType(self, typ):
    1644          """
    1645          Set the type of dataform
    1646          """
    1647          self.setAttr('type', typ)
    1648
    1649 -    def getTitle(self):
    1650          """
    1651          Return the title of dataform
    1652          """
    1653          return self.getTagData('title')
    1654
    1655 -    def setTitle(self, text):
    1656          """
    1657          Set the title of dataform
    1658          """
    1659          self.setTagData('title', text)
    1660
    1661 -    def getInstructions(self):
    1662          """
    1663          Return the instructions of dataform
    1664          """
    1665          return self.getTagData('instructions')
    1666
    1667 -    def setInstructions(self, text):
    1668          """
    1669          Set the instructions of dataform
    1670          """
    1671          self.setTagData('instructions', text)
    1672
    1673 -    def addInstructions(self, text):
    1674          """
    1675          Add one more instruction to the dataform
    1676          """
    1677          self.addChild('instructions', {}, [text])
    1678
    1679 -    def getField(self, name):
    1680          """
    1681          Return the datafield object with name 'name' (if exists)
    1682          """
    1683          return self.getTag('field', attrs={'var': name})
    1684
    1685 -    def setField(self, name):
    1686          """
    1687          Create if nessessary or get the existing datafield object with name
    1688          'name' and return it
    1689          """
    1690          f = self.getField(name)
    1691          if f:
    1692              return f
    1693          return self.addChild(node=DataField(name))
    1694
    1695 -    def asDict(self):
    1696          """
    1697          Represent dataform as simple dictionary mapping of datafield names to
    1698          their values
    1699          """
    1700          ret = {}
    1701          for field in self.getTags('field'):
    1702              name = field.getAttr('var')
    1703              typ = field.getType()
    1704              if isinstance(typ, basestring) and typ.endswith('-multi'):
    1705                  val = []
    1706                  for i in field.getTags('value'):
    1707                      val.append(i.getData())
    1708              else:
    1709                  val = field.getTagData('value')
    1710              ret[name] = val
    1711          if self.getTag('instructions'):
    1712              ret['instructions'] = self.getInstructions()
    1713          return ret
    1714
    1715 -    def __getitem__(self, name):
    1716          """
    1717          Simple dictionary interface for getting datafields values by their names
    1718          """
    1719          item = self.getField(name)
    1720          if item:
    1721              return item.getValue()
    1722          raise IndexError('No such field')
    1723
    1724 -    def __setitem__(self, name, val):
    1725          """
    1726          Simple dictionary interface for setting datafields values by their names
    1727          """
    1728          return self.setField(name).setValue(val)
    1729