1   
  2   
  3   
  4   
  5   
  6   
  7   
  8   
  9   
 10   
 11   
 12   
 13   
 14   
 15   
 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   
 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   
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   
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   
175   
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               
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   
221   
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   
247   
249          """Process session request time out. 
250   
251          :raise FatalClientError:""" 
252          raise FatalClientError("Timeout while tryin to establish a session") 
 253   
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   
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   
298   
300          """Process roster request time out. 
301   
302          :raise ClientError:""" 
303          raise ClientError("Timeout while tryin to retrieve roster") 
 304   
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   
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   
355   
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   
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   
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   
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   
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   
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   
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   
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   
453          """Handle "connected" event. May be overriden in derived classes. 
454          This one does nothing.""" 
455          pass 
 456   
458          """Handle "authenticated" event. May be overriden in derived classes. 
459          This one does nothing.""" 
460          pass 
 461   
463          """Handle "authorized" event. May be overriden in derived classes. 
464          This one requests an IM session.""" 
465          self.request_session() 
 466   
468          """Handle "disconnected" event. May be overriden in derived classes. 
469          This one does nothing.""" 
470          pass 
  471   
472   
473