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