pyxmpp.client

Package pyxmpp :: Module client |
[hide private]

# Source Code for Module pyxmpp.client

      1  #
      2  # (C) Copyright 2003-2010 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 714 2010-04-05 10:20:10Z 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              self.__logger.warning("Got roster update from wrong source")
    347              return
    348          if not self.roster:
    349              raise ClientError("Roster update, but no roster")
    350          q=iq.get_query()
    351          item=self.roster.update(q)
    352          if item:
    353              self.roster_updated(item)
    354          resp=iq.make_result_response()
    355          self.stream.send(resp)
    356
    357 -    def __stream_state_change(self,state,arg):
    358          """Handle stream state changes.
    359
    360          Call apopriate methods of self.
    361
    362          :Parameters:
    363              - `state`: the new state.
    364              - `arg`: state change argument.
    365          :Types:
    366              - `state`: `str`"""
    367          self.stream_state_changed(state,arg)
    368          if state=="fully connected":
    369              self.connected()
    370          elif state=="authorized":
    371              self.authorized()
    372          elif state=="disconnected":
    373              self.state_changed.acquire()
    374              try:
    375                  if self.stream:
    376                      self.stream.close()
    377                  self.stream_closed(self.stream)
    378                  self.stream=None
    379                  self.state_changed.notify()
    380              finally:
    381                  self.state_changed.release()
    382              self.disconnected()
    383
    384  # Method to override
    385 -    def idle(self):
    386          """Do some "housekeeping" work like cache expiration or timeout
    387          handling. Should be called periodically from the application main
    388          loop. May be overriden in derived classes."""
    389          stream=self.get_stream()
    390          if stream:
    391              stream.idle()
    392
    393 -    def stream_created(self,stream):
    394          """Handle stream creation event. May be overriden in derived classes.
    395          This one does nothing.
    396
    397          :Parameters:
    398              - `stream`: the new stream.
    399          :Types:
    400              - `stream`: `pyxmpp.ClientStream`"""
    401          pass
    402
    403 -    def stream_closed(self,stream):
    404          """Handle stream closure event. May be overriden in derived classes.
    405          This one does nothing.
    406
    407          :Parameters:
    408              - `stream`: the new stream.
    409          :Types:
    410              - `stream`: `pyxmpp.ClientStream`"""
    411          pass
    412
    413 -    def session_started(self):
    414          """Handle session started event. May be overriden in derived classes.
    415          This one requests the user's roster and sends the initial presence."""
    416          self.request_roster()
    417          p=Presence()
    418          self.stream.send(p)
    419
    420 -    def stream_error(self,err):
    421          """Handle stream error received. May be overriden in derived classes.
    422          This one passes an error messages to logging facilities.
    423
    424          :Parameters:
    425              - `err`: the error element received.
    426          :Types:
    427              - `err`: `pyxmpp.error.StreamErrorNode`"""
    428          self.__logger.error("Stream error: condition: %s %r"
    429                  % (err.get_condition().name,err.serialize()))
    430
    431 -    def roster_updated(self,item=None):
    432          """Handle roster update event. May be overriden in derived classes.
    433          This one does nothing.
    434
    435          :Parameters:
    436              - `item`: the roster item changed or `None` if whole roster was
    437                received.
    438          :Types:
    439              - `item`: `pyxmpp.RosterItem`"""
    440          pass
    441
    442 -    def stream_state_changed(self,state,arg):
    443          """Handle any stream state change. May be overriden in derived classes.
    444          This one does nothing.
    445
    446          :Parameters:
    447              - `state`: the new state.
    448              - `arg`: state change argument.
    449          :Types:
    450              - `state`: `str`"""
    451          pass
    452
    453 -    def connected(self):
    454          """Handle "connected" event. May be overriden in derived classes.
    455          This one does nothing."""
    456          pass
    457
    458 -    def authenticated(self):
    459          """Handle "authenticated" event. May be overriden in derived classes.
    460          This one does nothing."""
    461          pass
    462
    463 -    def authorized(self):
    464          """Handle "authorized" event. May be overriden in derived classes.
    465          This one requests an IM session."""
    466          self.request_session()
    467
    468 -    def disconnected(self):
    469          """Handle "disconnected" event. May be overriden in derived classes.
    470          This one does nothing."""
    471          pass
    472
    473  # vi: sts=4 et sw=4
    474