Package pyxmpp :: Module client |
[hide private]

# Source Code for Module pyxmpp.client

      1  #
      2  # (C) Copyright 2003-2006 Jacek Konieczny <jajcus@jajcus.net>
      3  #
      4  # This program is free software; you can redistribute it and/or modify
      5  # it under the terms of the GNU Lesser General Public License Version
      6  # 2.1 as published by the Free Software Foundation.
      7  #
      8  # This program is distributed in the hope that it will be useful,
      9  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     11  # GNU Lesser General Public License for more details.
     12  #
     13  # You should have received a copy of the GNU Lesser General Public
     14  # License along with this program; if not, write to the Free Software
     15  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     16  #
     17
     18  """Basic XMPP-IM client implementation.
     19
     20  Normative reference:
     21    - `RFC 3921 <http://www.ietf.org/rfc/rfc3921.txt>`__
     22  """
     23
     24  __revision__="$Id: client.py 678 2008-08-08 11:22:14Z jajcus $"
     25  __docformat__="restructuredtext en"
     26
     27  import threading
     28  import logging
     29
     30  from pyxmpp.clientstream import ClientStream
     31  from pyxmpp.iq import Iq
     32  from pyxmpp.presence import Presence
     33  from pyxmpp.roster import Roster
     34  from pyxmpp.exceptions import ClientError, FatalClientError
     35  from pyxmpp.interfaces import IPresenceHandlersProvider, IMessageHandlersProvider
     36  from pyxmpp.interfaces import IIqHandlersProvider, IStanzaHandlersProvider
     37
     38 -class Client:
     39      """Base class for an XMPP-IM client.
     40
     41      This class does not provide any JSF extensions to the XMPP protocol,
     42      including legacy authentication methods.
     43
     44      :Ivariables:
     45          - `jid`: configured JID of the client (current actual JID
     46            is avialable as `self.stream.jid`).
     47          - `password`: authentication password.
     48          - `server`: server to use if non-standard and not discoverable
     49            by SRV lookups.
     50          - `port`: port number on the server to use if non-standard and not
     51            discoverable by SRV lookups.
     52          - `auth_methods`: methods allowed for stream authentication. SASL
     53            mechanism names should be preceded with "sasl:" prefix.
     54          - `keepalive`: keepalive interval for the stream or 0 when keepalive is
     55            disabled.
     56          - `stream`: current stream when the client is connected,
     57            `None` otherwise.
     58          - `roster`: user's roster or `None` if the roster is not yet retrieved.
     59          - `session_established`: `True` when an IM session is established.
     60          - `lock`: lock for synchronizing `Client` attributes access.
     61          - `state_changed`: condition notified the the object state changes
     62            (stream becomes connected, session established etc.).
     63          - `interface_providers`: list of object providing interfaces that
     64            could be used by the Client object. Initialized to [`self`] by
     65            the constructor if not set earlier. Put objects providing
     66            `IPresenceHandlersProvider`, `IMessageHandlersProvider`,
     67            `IIqHandlersProvider` or `IStanzaHandlersProvider` into this list.
     68      :Types:
     69          - `jid`: `pyxmpp.JID`
     70          - `password`: `unicode`
     71          - `server`: `unicode`
     72          - `port`: `int`
     73          - `auth_methods`: `list` of `str`
     74          - `keepalive`: `int`
     75          - `stream`: `pyxmpp.ClientStream`
     76          - `roster`: `pyxmpp.Roster`
     77          - `session_established`: `bool`
     78          - `lock`: `threading.RLock`
     79          - `state_changed`: `threading.Condition`
     80          - `interface_providers`: `list`
     81      """
     82 -    def __init__(self,jid=None,password=None,server=None,port=5222,
     83              auth_methods=("sasl:DIGEST-MD5",),
     84              tls_settings=None,keepalive=0):
     85          """Initialize a Client object.
     86
     87          :Parameters:
     88              - `jid`: user full JID for the connection.
     89              - `password`: user password.
     90              - `server`: server to use. If not given then address will be derived form the JID.
     91              - `port`: port number to use. If not given then address will be derived form the JID.
     92              - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms
     93                in the list should be prefixed with "sasl:" string.
     94              - `tls_settings`: settings for StartTLS -- `TLSSettings` instance.
     95              - `keepalive`: keepalive output interval. 0 to disable.
     96          :Types:
     97              - `jid`: `pyxmpp.JID`
     98              - `password`: `unicode`
     99              - `server`: `unicode`
    100              - `port`: `int`
    101              - `auth_methods`: sequence of `str`
    102              - `tls_settings`: `pyxmpp.TLSSettings`
    103              - `keepalive`: `int`
    104          """
    105          self.jid=jid
    106          self.password=password
    107          self.server=server
    108          self.port=port
    109          self.auth_methods=list(auth_methods)
    110          self.tls_settings=tls_settings
    111          self.keepalive=keepalive
    112          self.stream=None
    113          self.lock=threading.RLock()
    114          self.state_changed=threading.Condition(self.lock)
    115          self.session_established=False
    116          self.roster=None
    117          self.stream_class=ClientStream
    118          if not hasattr(self, "interface_providers"):
    119              self.interface_providers = [self]
    120          self.__logger=logging.getLogger("pyxmpp.Client")
    121
    122  # public methods
    123
    124 -    def connect(self, register = False):
    125          """Connect to the server and set up the stream.
    126
    127          Set `self.stream` and notify `self.state_changed` when connection
    128          succeeds."""
    129          if not self.jid:
    130              raise ClientError, "Cannot connect: no or bad JID given"
    131          self.lock.acquire()
    132          try:
    133              stream = self.stream
    134              self.stream = None
    135              if stream:
    136                  stream.close()
    137
    138              self.__logger.debug("Creating client stream: %r, auth_methods=%r"
    139                      % (self.stream_class, self.auth_methods))
    140              stream=self.stream_class(jid = self.jid,
    141                      password = self.password,
    142                      server = self.server,
    143                      port = self.port,
    144                      auth_methods = self.auth_methods,
    145                      tls_settings = self.tls_settings,
    146                      keepalive = self.keepalive,
    147                      owner = self)
    148              stream.process_stream_error = self.stream_error
    149              self.stream_created(stream)
    150              stream.state_change = self.__stream_state_change
    151              stream.connect()
    152              self.stream = stream
    153              self.state_changed.notify()
    154              self.state_changed.release()
    155          except:
    156              self.stream = None
    157              self.state_changed.release()
    158              raise
    159
    160 -    def get_stream(self):
    161          """Get the connected stream object.
    162
    163          :return: stream object or `None` if the client is not connected.
    164          :returntype: `pyxmpp.ClientStream`"""
    165          self.lock.acquire()
    166          stream=self.stream
    167          self.lock.release()
    168          return stream
    169
    170 -    def disconnect(self):
    171          """Disconnect from the server."""
    172          stream=self.get_stream()
    173          if stream:
    174              stream.disconnect()
    175
    176 -    def request_session(self):
    177          """Request an IM session."""
    178          stream=self.get_stream()
    179          if not stream.version:
    180              need_session=False
    181          elif not stream.features:
    182              need_session=False
    183          else:
    184              ctxt = stream.doc_in.xpathNewContext()
    185              ctxt.setContextNode(stream.features)
    186              ctxt.xpathRegisterNs("sess","urn:ietf:params:xml:ns:xmpp-session")
    187              # jabberd2 hack
    188              ctxt.xpathRegisterNs("jsess","http://jabberd.jabberstudio.org/ns/session/1.0")
    189              sess_n=None
    190              try:
    191                  sess_n=ctxt.xpathEval("sess:session or jsess:session")
    192              finally:
    193                  ctxt.xpathFreeContext()
    194              if sess_n:
    195                  need_session=True
    196              else:
    197                  need_session=False
    198
    199          if not need_session:
    200              self.state_changed.acquire()
    201              self.session_established=1
    202              self.state_changed.notify()
    203              self.state_changed.release()
    204              self._session_started()
    205          else:
    206              iq=Iq(stanza_type="set")
    207              iq.new_query("urn:ietf:params:xml:ns:xmpp-session","session")
    208              stream.set_response_handlers(iq,
    209                  self.__session_result,self.__session_error,self.__session_timeout)
    210              stream.send(iq)
    211
    212 -    def request_roster(self):
    213          """Request the user's roster."""
    214          stream=self.get_stream()
    215          iq=Iq(stanza_type="get")
    216          iq.new_query("jabber:iq:roster")
    217          stream.set_response_handlers(iq,
    218              self.__roster_result,self.__roster_error,self.__roster_timeout)
    219          stream.set_iq_set_handler("query","jabber:iq:roster",self.__roster_push)
    220          stream.send(iq)
    221
    222 -    def get_socket(self):
    223          """Get the socket object of the active connection.
    224
    225          :return: socket used by the stream.
    226          :returntype: `socket.socket`"""
    227          return self.stream.socket
    228
    229 -    def loop(self,timeout=1):
    230          """Simple "main loop" for the client.
    231
    232          By default just call the `pyxmpp.Stream.loop_iter` method of
    233          `self.stream`, which handles stream input and `self.idle` for some
    234          "housekeeping" work until the stream is closed.
    235
    236          This usually will be replaced by something more sophisticated. E.g.
    237          handling of other input sources."""
    238          while 1:
    239              stream=self.get_stream()
    240              if not stream:
    241                  break
    242              act=stream.loop_iter(timeout)
    243              if not act:
    244                  self.idle()
    245
    246  # private methods
    247
    248 -    def __session_timeout(self):
    249          """Process session request time out.
    250
    251          :raise FatalClientError:"""
    252          raise FatalClientError("Timeout while tryin to establish a session")
    253
    254 -    def __session_error(self,iq):
    255          """Process session request failure.
    256
    257          :Parameters:
    258              - `iq`: IQ error stanza received as result of the session request.
    259          :Types:
    260              - `iq`: `pyxmpp.Iq`
    261
    262          :raise FatalClientError:"""
    263          err=iq.get_error()
    264          msg=err.get_message()
    265          raise FatalClientError("Failed to establish a session: "+msg)
    266
    267 -    def __session_result(self, _unused):
    268          """Process session request success.
    269
    270          :Parameters:
    271              - `_unused`: IQ result stanza received in reply to the session request.
    272          :Types:
    273              - `_unused`: `pyxmpp.Iq`"""
    274          self.state_changed.acquire()
    275          self.session_established=True
    276          self.state_changed.notify()
    277          self.state_changed.release()
    278          self._session_started()
    279
    280 -    def _session_started(self):
    281          """Called when session is started.
    282
    283          Activates objects from `self.interface_provides` by installing
    284          their stanza handlers, etc."""
    285          for ob in self.interface_providers:
    286              if IPresenceHandlersProvider.providedBy(ob):
    287                  for handler_data in ob.get_presence_handlers():
    288                      self.stream.set_presence_handler(*handler_data)
    289              if IMessageHandlersProvider.providedBy(ob):
    290                  for handler_data in ob.get_message_handlers():
    291                      self.stream.set_message_handler(*handler_data)
    292              if IIqHandlersProvider.providedBy(ob):
    293                  for handler_data in ob.get_iq_get_handlers():
    294                      self.stream.set_iq_get_handler(*handler_data)
    295                  for handler_data in ob.get_iq_set_handlers():
    296                      self.stream.set_iq_set_handler(*handler_data)
    297          self.session_started()
    298
    299 -    def __roster_timeout(self):
    300          """Process roster request time out.
    301
    302          :raise ClientError:"""
    303          raise ClientError("Timeout while tryin to retrieve roster")
    304
    305 -    def __roster_error(self,iq):
    306          """Process roster request failure.
    307
    308          :Parameters:
    309              - `iq`: IQ error stanza received as result of the roster request.
    310          :Types:
    311              - `iq`: `pyxmpp.Iq`
    312
    313          :raise ClientError:"""
    314          err=iq.get_error()
    315          msg=err.get_message()
    316          raise ClientError("Roster retrieval failed: "+msg)
    317
    318 -    def __roster_result(self,iq):
    319          """Process roster request success.
    320
    321          :Parameters:
    322              - `iq`: IQ result stanza received in reply to the roster request.
    323          :Types:
    324              - `iq`: `pyxmpp.Iq`"""
    325          q=iq.get_query()
    326          if q:
    327              self.state_changed.acquire()
    328              self.roster=Roster(q)
    329              self.state_changed.notify()
    330              self.state_changed.release()
    331              self.roster_updated()
    332          else:
    333              raise ClientError("Roster retrieval failed")
    334
    335 -    def __roster_push(self,iq):
    336          """Process a "roster push" (change notification) received.
    337
    338          :Parameters:
    339              - `iq`: IQ result stanza received.
    340          :Types:
    341              - `iq`: `pyxmpp.Iq`"""
    342          fr=iq.get_from()
    343          if fr and fr!=self.jid:
    344              resp=iq.make_error_response("forbidden")
    345              self.stream.send(resp)
    346              raise ClientError("Got roster update from wrong source")
    347          if not self.roster:
    348              raise ClientError("Roster update, but no roster")
    349          q=iq.get_query()
    350          item=self.roster.update(q)
    351          if item:
    352              self.roster_updated(item)
    353          resp=iq.make_result_response()
    354          self.stream.send(resp)
    355
    356 -    def __stream_state_change(self,state,arg):
    357          """Handle stream state changes.
    358
    359          Call apopriate methods of self.
    360
    361          :Parameters:
    362              - `state`: the new state.
    363              - `arg`: state change argument.
    364          :Types:
    365              - `state`: `str`"""
    366          self.stream_state_changed(state,arg)
    367          if state=="fully connected":
    368              self.connected()
    369          elif state=="authorized":
    370              self.authorized()
    371          elif state=="disconnected":
    372              self.state_changed.acquire()
    373              try:
    374                  if self.stream:
    375                      self.stream.close()
    376                  self.stream_closed(self.stream)
    377                  self.stream=None
    378                  self.state_changed.notify()
    379              finally:
    380                  self.state_changed.release()
    381              self.disconnected()
    382
    383  # Method to override
    384 -    def idle(self):
    385          """Do some "housekeeping" work like cache expiration or timeout
    386          handling. Should be called periodically from the application main
    387          loop. May be overriden in derived classes."""
    388          stream=self.get_stream()
    389          if stream:
    390              stream.idle()
    391
    392 -    def stream_created(self,stream):
    393          """Handle stream creation event. May be overriden in derived classes.
    394          This one does nothing.
    395
    396          :Parameters:
    397              - `stream`: the new stream.
    398          :Types:
    399              - `stream`: `pyxmpp.ClientStream`"""
    400          pass
    401
    402 -    def stream_closed(self,stream):
    403          """Handle stream closure event. May be overriden in derived classes.
    404          This one does nothing.
    405
    406          :Parameters:
    407              - `stream`: the new stream.
    408          :Types:
    409              - `stream`: `pyxmpp.ClientStream`"""
    410          pass
    411
    412 -    def session_started(self):
    413          """Handle session started event. May be overriden in derived classes.
    414          This one requests the user's roster and sends the initial presence."""
    415          self.request_roster()
    416          p=Presence()
    417          self.stream.send(p)
    418
    419 -    def stream_error(self,err):
    420          """Handle stream error received. May be overriden in derived classes.
    421          This one passes an error messages to logging facilities.
    422
    423          :Parameters:
    424              - `err`: the error element received.
    425          :Types:
    426              - `err`: `pyxmpp.error.StreamErrorNode`"""
    427          self.__logger.error("Stream error: condition: %s %r"
    428                  % (err.get_condition().name,err.serialize()))
    429
    430 -    def roster_updated(self,item=None):
    431          """Handle roster update event. May be overriden in derived classes.
    432          This one does nothing.
    433
    434          :Parameters:
    435              - `item`: the roster item changed or `None` if whole roster was
    436                received.
    437          :Types:
    438              - `item`: `pyxmpp.RosterItem`"""
    439          pass
    440
    441 -    def stream_state_changed(self,state,arg):
    442          """Handle any stream state change. May be overriden in derived classes.
    443          This one does nothing.
    444
    445          :Parameters:
    446              - `state`: the new state.
    447              - `arg`: state change argument.
    448          :Types:
    449              - `state`: `str`"""
    450          pass
    451
    452 -    def connected(self):
    453          """Handle "connected" event. May be overriden in derived classes.
    454          This one does nothing."""
    455          pass
    456
    457 -    def authenticated(self):
    458          """Handle "authenticated" event. May be overriden in derived classes.
    459          This one does nothing."""
    460          pass
    461
    462 -    def authorized(self):
    463          """Handle "authorized" event. May be overriden in derived classes.
    464          This one requests an IM session."""
    465          self.request_session()
    466
    467 -    def disconnected(self):
    468          """Handle "disconnected" event. May be overriden in derived classes.
    469          This one does nothing."""
    470          pass
    471
    472  # vi: sts=4 et sw=4
    473