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 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
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
356
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
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
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
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
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
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
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
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
454 """Handle "connected" event. May be overriden in derived classes.
455 This one does nothing."""
456 pass
457
459 """Handle "authenticated" event. May be overriden in derived classes.
460 This one does nothing."""
461 pass
462
464 """Handle "authorized" event. May be overriden in derived classes.
465 This one requests an IM session."""
466 self.request_session()
467
469 """Handle "disconnected" event. May be overriden in derived classes.
470 This one does nothing."""
471 pass
472
473
474