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  __docformat__="restructuredtext en" 
 25   
 26  import threading 
 27  import logging 
 28   
 29  from pyxmpp.clientstream import ClientStream 
 30  from pyxmpp.iq import Iq 
 31  from pyxmpp.presence import Presence 
 32  from pyxmpp.roster import Roster 
 33  from pyxmpp.exceptions import ClientError, FatalClientError 
 34  from pyxmpp.interfaces import IPresenceHandlersProvider, IMessageHandlersProvider 
 35  from pyxmpp.interfaces import IIqHandlersProvider, IStanzaHandlersProvider 
 36   
37 -class Client:
38 """Base class for an XMPP-IM client. 39 40 This class does not provide any JSF extensions to the XMPP protocol, 41 including legacy authentication methods. 42 43 :Ivariables: 44 - `jid`: configured JID of the client (current actual JID 45 is avialable as `self.stream.jid`). 46 - `password`: authentication password. 47 - `server`: server to use if non-standard and not discoverable 48 by SRV lookups. 49 - `port`: port number on the server to use if non-standard and not 50 discoverable by SRV lookups. 51 - `auth_methods`: methods allowed for stream authentication. SASL 52 mechanism names should be preceded with "sasl:" prefix. 53 - `keepalive`: keepalive interval for the stream or 0 when keepalive is 54 disabled. 55 - `stream`: current stream when the client is connected, 56 `None` otherwise. 57 - `roster`: user's roster or `None` if the roster is not yet retrieved. 58 - `session_established`: `True` when an IM session is established. 59 - `lock`: lock for synchronizing `Client` attributes access. 60 - `state_changed`: condition notified the the object state changes 61 (stream becomes connected, session established etc.). 62 - `interface_providers`: list of object providing interfaces that 63 could be used by the Client object. Initialized to [`self`] by 64 the constructor if not set earlier. Put objects providing 65 `IPresenceHandlersProvider`, `IMessageHandlersProvider`, 66 `IIqHandlersProvider` or `IStanzaHandlersProvider` into this list. 67 :Types: 68 - `jid`: `pyxmpp.JID` 69 - `password`: `unicode` 70 - `server`: `unicode` 71 - `port`: `int` 72 - `auth_methods`: `list` of `str` 73 - `keepalive`: `int` 74 - `stream`: `pyxmpp.ClientStream` 75 - `roster`: `pyxmpp.Roster` 76 - `session_established`: `bool` 77 - `lock`: `threading.RLock` 78 - `state_changed`: `threading.Condition` 79 - `interface_providers`: `list` 80 """
81 - def __init__(self,jid=None,password=None,server=None,port=5222, 82 auth_methods=("sasl:DIGEST-MD5",), 83 tls_settings=None,keepalive=0):
84 """Initialize a Client object. 85 86 :Parameters: 87 - `jid`: user full JID for the connection. 88 - `password`: user password. 89 - `server`: server to use. If not given then address will be derived form the JID. 90 - `port`: port number to use. If not given then address will be derived form the JID. 91 - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms 92 in the list should be prefixed with "sasl:" string. 93 - `tls_settings`: settings for StartTLS -- `TLSSettings` instance. 94 - `keepalive`: keepalive output interval. 0 to disable. 95 :Types: 96 - `jid`: `pyxmpp.JID` 97 - `password`: `unicode` 98 - `server`: `unicode` 99 - `port`: `int` 100 - `auth_methods`: sequence of `str` 101 - `tls_settings`: `pyxmpp.TLSSettings` 102 - `keepalive`: `int` 103 """ 104 self.jid=jid 105 self.password=password 106 self.server=server 107 self.port=port 108 self.auth_methods=list(auth_methods) 109 self.tls_settings=tls_settings 110 self.keepalive=keepalive 111 self.stream=None 112 self.lock=threading.RLock() 113 self.state_changed=threading.Condition(self.lock) 114 self.session_established=False 115 self.roster=None 116 self.stream_class=ClientStream 117 if not hasattr(self, "interface_providers"): 118 self.interface_providers = [self] 119 self.__logger=logging.getLogger("pyxmpp.Client")
120 121 # public methods 122
123 - def connect(self, register = False):
124 """Connect to the server and set up the stream. 125 126 Set `self.stream` and notify `self.state_changed` when connection 127 succeeds.""" 128 if not self.jid: 129 raise ClientError, "Cannot connect: no or bad JID given" 130 self.lock.acquire() 131 try: 132 stream = self.stream 133 self.stream = None 134 if stream: 135 stream.close() 136 137 self.__logger.debug("Creating client stream: %r, auth_methods=%r" 138 % (self.stream_class, self.auth_methods)) 139 stream=self.stream_class(jid = self.jid, 140 password = self.password, 141 server = self.server, 142 port = self.port, 143 auth_methods = self.auth_methods, 144 tls_settings = self.tls_settings, 145 keepalive = self.keepalive, 146 owner = self) 147 stream.process_stream_error = self.stream_error 148 self.stream_created(stream) 149 stream.state_change = self.__stream_state_change 150 stream.connect() 151 self.stream = stream 152 self.state_changed.notify() 153 self.state_changed.release() 154 except: 155 self.stream = None 156 self.state_changed.release() 157 raise
158
159 - def get_stream(self):
160 """Get the connected stream object. 161 162 :return: stream object or `None` if the client is not connected. 163 :returntype: `pyxmpp.ClientStream`""" 164 self.lock.acquire() 165 stream=self.stream 166 self.lock.release() 167 return stream
168
169 - def disconnect(self):
170 """Disconnect from the server.""" 171 stream=self.get_stream() 172 if stream: 173 stream.disconnect()
174
175 - def request_session(self):
176 """Request an IM session.""" 177 stream=self.get_stream() 178 if not stream.version: 179 need_session=False 180 elif not stream.features: 181 need_session=False 182 else: 183 ctxt = stream.doc_in.xpathNewContext() 184 ctxt.setContextNode(stream.features) 185 ctxt.xpathRegisterNs("sess","urn:ietf:params:xml:ns:xmpp-session") 186 # jabberd2 hack 187 ctxt.xpathRegisterNs("jsess","http://jabberd.jabberstudio.org/ns/session/1.0") 188 sess_n=None 189 try: 190 sess_n=ctxt.xpathEval("sess:session or jsess:session") 191 finally: 192 ctxt.xpathFreeContext() 193 if sess_n: 194 need_session=True 195 else: 196 need_session=False 197 198 if not need_session: 199 self.state_changed.acquire() 200 self.session_established=1 201 self.state_changed.notify() 202 self.state_changed.release() 203 self._session_started() 204 else: 205 iq=Iq(stanza_type="set") 206 iq.new_query("urn:ietf:params:xml:ns:xmpp-session","session") 207 stream.set_response_handlers(iq, 208 self.__session_result,self.__session_error,self.__session_timeout) 209 stream.send(iq)
210
211 - def request_roster(self):
212 """Request the user's roster.""" 213 stream=self.get_stream() 214 iq=Iq(stanza_type="get") 215 iq.new_query("jabber:iq:roster") 216 stream.set_response_handlers(iq, 217 self.__roster_result,self.__roster_error,self.__roster_timeout) 218 stream.set_iq_set_handler("query","jabber:iq:roster",self.__roster_push) 219 stream.send(iq)
220
221 - def get_socket(self):
222 """Get the socket object of the active connection. 223 224 :return: socket used by the stream. 225 :returntype: `socket.socket`""" 226 return self.stream.socket
227
228 - def loop(self,timeout=1):
229 """Simple "main loop" for the client. 230 231 By default just call the `pyxmpp.Stream.loop_iter` method of 232 `self.stream`, which handles stream input and `self.idle` for some 233 "housekeeping" work until the stream is closed. 234 235 This usually will be replaced by something more sophisticated. E.g. 236 handling of other input sources.""" 237 while 1: 238 stream=self.get_stream() 239 if not stream: 240 break 241 act=stream.loop_iter(timeout) 242 if not act: 243 self.idle()
244 245 # private methods 246
247 - def __session_timeout(self):
248 """Process session request time out. 249 250 :raise FatalClientError:""" 251 raise FatalClientError("Timeout while tryin to establish a session")
252
253 - def __session_error(self,iq):
254 """Process session request failure. 255 256 :Parameters: 257 - `iq`: IQ error stanza received as result of the session request. 258 :Types: 259 - `iq`: `pyxmpp.Iq` 260 261 :raise FatalClientError:""" 262 err=iq.get_error() 263 msg=err.get_message() 264 raise FatalClientError("Failed to establish a session: "+msg)
265
266 - def __session_result(self, _unused):
267 """Process session request success. 268 269 :Parameters: 270 - `_unused`: IQ result stanza received in reply to the session request. 271 :Types: 272 - `_unused`: `pyxmpp.Iq`""" 273 self.state_changed.acquire() 274 self.session_established=True 275 self.state_changed.notify() 276 self.state_changed.release() 277 self._session_started()
278
279 - def _session_started(self):
280 """Called when session is started. 281 282 Activates objects from `self.interface_provides` by installing 283 their stanza handlers, etc.""" 284 for ob in self.interface_providers: 285 if IPresenceHandlersProvider.providedBy(ob): 286 for handler_data in ob.get_presence_handlers(): 287 self.stream.set_presence_handler(*handler_data) 288 if IMessageHandlersProvider.providedBy(ob): 289 for handler_data in ob.get_message_handlers(): 290 self.stream.set_message_handler(*handler_data) 291 if IIqHandlersProvider.providedBy(ob): 292 for handler_data in ob.get_iq_get_handlers(): 293 self.stream.set_iq_get_handler(*handler_data) 294 for handler_data in ob.get_iq_set_handlers(): 295 self.stream.set_iq_set_handler(*handler_data) 296 self.session_started()
297
298 - def __roster_timeout(self):
299 """Process roster request time out. 300 301 :raise ClientError:""" 302 raise ClientError("Timeout while tryin to retrieve roster")
303
304 - def __roster_error(self,iq):
305 """Process roster request failure. 306 307 :Parameters: 308 - `iq`: IQ error stanza received as result of the roster request. 309 :Types: 310 - `iq`: `pyxmpp.Iq` 311 312 :raise ClientError:""" 313 err=iq.get_error() 314 msg=err.get_message() 315 raise ClientError("Roster retrieval failed: "+msg)
316
317 - def __roster_result(self,iq):
318 """Process roster request success. 319 320 :Parameters: 321 - `iq`: IQ result stanza received in reply to the roster request. 322 :Types: 323 - `iq`: `pyxmpp.Iq`""" 324 q=iq.get_query() 325 if q: 326 self.state_changed.acquire() 327 self.roster=Roster(q) 328 self.state_changed.notify() 329 self.state_changed.release() 330 self.roster_updated() 331 else: 332 raise ClientError("Roster retrieval failed")
333
334 - def __roster_push(self,iq):
335 """Process a "roster push" (change notification) received. 336 337 :Parameters: 338 - `iq`: IQ result stanza received. 339 :Types: 340 - `iq`: `pyxmpp.Iq`""" 341 fr=iq.get_from() 342 if fr and fr != self.jid and fr != self.jid.bare(): 343 resp=iq.make_error_response("forbidden") 344 self.stream.send(resp) 345 self.__logger.warning("Got roster update from wrong source") 346 return 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