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 __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
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
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
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
174
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
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
220
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
246
248 """Process session request time out.
249
250 :raise FatalClientError:"""
251 raise FatalClientError("Timeout while tryin to establish a session")
252
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
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
297
299 """Process roster request time out.
300
301 :raise ClientError:"""
302 raise ClientError("Timeout while tryin to retrieve roster")
303
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
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
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