Fix the iq.send() function, and a bunch of places where it is called
[slixmpp.git] / slixmpp / clientxmpp.py
blobadd7f437dbd41b9dd42e75c3fc6e221476d58b6b
1 # -*- coding: utf-8 -*-
2 """
3 slixmpp.clientxmpp
4 ~~~~~~~~~~~~~~~~~~~~
6 This module provides XMPP functionality that
7 is specific to client connections.
9 Part of Slixmpp: The Slick XMPP Library
11 :copyright: (c) 2011 Nathanael C. Fritz
12 :license: MIT, see LICENSE for more details
13 """
15 from __future__ import absolute_import, unicode_literals
17 import logging
19 from slixmpp.stanza import StreamFeatures
20 from slixmpp.basexmpp import BaseXMPP
21 from slixmpp.exceptions import XMPPError
22 from slixmpp.xmlstream import XMLStream
23 from slixmpp.xmlstream.matcher import StanzaPath, MatchXPath
24 from slixmpp.xmlstream.handler import Callback
26 # Flag indicating if DNS SRV records are available for use.
27 try:
28 import dns.resolver
29 except ImportError:
30 DNSPYTHON = False
31 else:
32 DNSPYTHON = True
35 log = logging.getLogger(__name__)
38 class ClientXMPP(BaseXMPP):
40 """
41 Slixmpp's client class. (Use only for good, not for evil.)
43 Typical use pattern:
45 .. code-block:: python
47 xmpp = ClientXMPP('user@server.tld/resource', 'password')
48 # ... Register plugins and event handlers ...
49 xmpp.connect()
50 xmpp.process(block=False) # block=True will block the current
51 # thread. By default, block=False
53 :param jid: The JID of the XMPP user account.
54 :param password: The password for the XMPP user account.
55 :param ssl: **Deprecated.**
56 :param plugin_config: A dictionary of plugin configurations.
57 :param plugin_whitelist: A list of approved plugins that
58 will be loaded when calling
59 :meth:`~slixmpp.basexmpp.BaseXMPP.register_plugins()`.
60 :param escape_quotes: **Deprecated.**
61 """
63 def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[],
64 escape_quotes=True, sasl_mech=None, lang='en'):
65 BaseXMPP.__init__(self, jid, 'jabber:client')
67 self.escape_quotes = escape_quotes
68 self.plugin_config = plugin_config
69 self.plugin_whitelist = plugin_whitelist
70 self.default_port = 5222
71 self.default_lang = lang
73 self.credentials = {}
75 self.password = password
77 self.stream_header = "<stream:stream to='%s' %s %s %s %s>" % (
78 self.boundjid.host,
79 "xmlns:stream='%s'" % self.stream_ns,
80 "xmlns='%s'" % self.default_ns,
81 "xml:lang='%s'" % self.default_lang,
82 "version='1.0'")
83 self.stream_footer = "</stream:stream>"
85 self.features = set()
86 self._stream_feature_handlers = {}
87 self._stream_feature_order = []
89 self.dns_service = 'xmpp-client'
91 #TODO: Use stream state here
92 self.authenticated = False
93 self.sessionstarted = False
94 self.bound = False
95 self.bindfail = False
97 self.add_event_handler('connected', self._reset_connection_state)
98 self.add_event_handler('session_bind', self._handle_session_bind)
99 self.add_event_handler('roster_update', self._handle_roster)
101 self.register_stanza(StreamFeatures)
103 self.register_handler(
104 Callback('Stream Features',
105 MatchXPath('{%s}features' % self.stream_ns),
106 self._handle_stream_features))
107 self.register_handler(
108 Callback('Roster Update',
109 StanzaPath('iq@type=set/roster'),
110 lambda iq: self.event('roster_update', iq)))
112 # Setup default stream features
113 self.register_plugin('feature_starttls')
114 self.register_plugin('feature_bind')
115 self.register_plugin('feature_session')
116 self.register_plugin('feature_rosterver')
117 self.register_plugin('feature_preapproval')
118 self.register_plugin('feature_mechanisms')
120 if sasl_mech:
121 self['feature_mechanisms'].use_mech = sasl_mech
123 @property
124 def password(self):
125 return self.credentials.get('password', '')
127 @password.setter
128 def password(self, value):
129 self.credentials['password'] = value
131 def connect(self, address=tuple(), use_ssl=False,
132 force_starttls=True, disable_starttls=False):
133 """Connect to the XMPP server.
135 When no address is given, a SRV lookup for the server will
136 be attempted. If that fails, the server user in the JID
137 will be used.
139 :param address: A tuple containing the server's host and port.
140 :param reattempt: If ``True``, repeat attempting to connect if an
141 error occurs. Defaults to ``True``.
142 :param use_tls: Indicates if TLS should be used for the
143 connection. Defaults to ``True``.
144 :param use_ssl: Indicates if the older SSL connection method
145 should be used. Defaults to ``False``.
148 # If an address was provided, disable using DNS SRV lookup;
149 # otherwise, use the domain from the client JID with the standard
150 # XMPP client port and allow SRV lookup.
151 if address:
152 self.dns_service = None
153 else:
154 address = (self.boundjid.host, 5222)
155 self.dns_service = 'xmpp-client'
157 return XMLStream.connect(self, address[0], address[1], use_ssl=use_ssl,
158 force_starttls=force_starttls, disable_starttls=disable_starttls)
160 def register_feature(self, name, handler, restart=False, order=5000):
161 """Register a stream feature handler.
163 :param name: The name of the stream feature.
164 :param handler: The function to execute if the feature is received.
165 :param restart: Indicates if feature processing should halt with
166 this feature. Defaults to ``False``.
167 :param order: The relative ordering in which the feature should
168 be negotiated. Lower values will be attempted
169 earlier when available.
171 self._stream_feature_handlers[name] = (handler, restart)
172 self._stream_feature_order.append((order, name))
173 self._stream_feature_order.sort()
175 def unregister_feature(self, name, order):
176 if name in self._stream_feature_handlers:
177 del self._stream_feature_handlers[name]
178 self._stream_feature_order.remove((order, name))
179 self._stream_feature_order.sort()
181 def update_roster(self, jid, **kwargs):
182 """Add or change a roster item.
184 :param jid: The JID of the entry to modify.
185 :param name: The user's nickname for this JID.
186 :param subscription: The subscription status. May be one of
187 ``'to'``, ``'from'``, ``'both'``, or
188 ``'none'``. If set to ``'remove'``,
189 the entry will be deleted.
190 :param groups: The roster groups that contain this item.
191 :param block: Specify if the roster request will block
192 until a response is received, or a timeout
193 occurs. Defaults to ``True``.
194 :param timeout: The length of time (in seconds) to wait
195 for a response before continuing if blocking
196 is used. Defaults to
197 :attr:`~slixmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
198 :param callback: Optional reference to a stream handler function.
199 Will be executed when the roster is received.
200 Implies ``block=False``.
202 current = self.client_roster[jid]
204 name = kwargs.get('name', current['name'])
205 subscription = kwargs.get('subscription', current['subscription'])
206 groups = kwargs.get('groups', current['groups'])
208 block = kwargs.get('block', True)
209 timeout = kwargs.get('timeout', None)
210 callback = kwargs.get('callback', None)
212 return self.client_roster.update(jid, name, subscription, groups,
213 block, timeout, callback)
215 def del_roster_item(self, jid):
216 """Remove an item from the roster.
218 This is done by setting its subscription status to ``'remove'``.
220 :param jid: The JID of the item to remove.
222 return self.client_roster.remove(jid)
224 def get_roster(self, callback=None, timeout=None, timeout_callback=None):
225 """Request the roster from the server.
227 :param callback: Reference to a stream handler function. Will
228 be executed when the roster is received.
230 iq = self.Iq()
231 iq['type'] = 'get'
232 iq.enable('roster')
233 if 'rosterver' in self.features:
234 iq['roster']['ver'] = self.client_roster.version
236 if callback is None:
237 callback = lambda resp: self.event('roster_update', resp)
238 else:
239 orig_cb = callback
240 def wrapped(resp):
241 self.event('roster_update', resp)
242 orig_cb(resp)
243 callback = wrapped
245 iq.send(callback, timeout, timeout_callback)
247 def _reset_connection_state(self, event=None):
248 #TODO: Use stream state here
249 self.authenticated = False
250 self.sessionstarted = False
251 self.bound = False
252 self.bindfail = False
253 self.features = set()
255 def _handle_stream_features(self, features):
256 """Process the received stream features.
258 :param features: The features stanza.
260 for order, name in self._stream_feature_order:
261 if name in features['features']:
262 handler, restart = self._stream_feature_handlers[name]
263 if handler(features) and restart:
264 # Don't continue if the feature requires
265 # restarting the XML stream.
266 return True
267 log.debug('Finished processing stream features.')
268 self.event('stream_negotiated')
270 def _handle_roster(self, iq):
271 """Update the roster after receiving a roster stanza.
273 :param iq: The roster stanza.
275 if iq['type'] == 'set':
276 if iq['from'].bare and iq['from'].bare != self.boundjid.bare:
277 raise XMPPError(condition='service-unavailable')
279 roster = self.client_roster
280 if iq['roster']['ver']:
281 roster.version = iq['roster']['ver']
282 items = iq['roster']['items']
284 valid_subscriptions = ('to', 'from', 'both', 'none', 'remove')
285 for jid, item in items.items():
286 if item['subscription'] in valid_subscriptions:
287 roster[jid]['name'] = item['name']
288 roster[jid]['groups'] = item['groups']
289 roster[jid]['from'] = item['subscription'] in ('from', 'both')
290 roster[jid]['to'] = item['subscription'] in ('to', 'both')
291 roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
293 roster[jid].save(remove=(item['subscription'] == 'remove'))
295 if iq['type'] == 'set':
296 resp = self.Iq(stype='result',
297 sto=iq['from'],
298 sid=iq['id'])
299 resp.enable('roster')
300 resp.send()
302 def _handle_session_bind(self, jid):
303 """Set the client roster to the JID set by the server.
305 :param :class:`slixmpp.xmlstream.jid.JID` jid: The bound JID as
306 dictated by the server. The same as :attr:`boundjid`.
308 self.client_roster = self.roster[jid]
311 # To comply with PEP8, method names now use underscores.
312 # Deprecated method names are re-mapped for backwards compatibility.
313 ClientXMPP.updateRoster = ClientXMPP.update_roster
314 ClientXMPP.delRosterItem = ClientXMPP.del_roster_item
315 ClientXMPP.getRoster = ClientXMPP.get_roster
316 ClientXMPP.registerFeature = ClientXMPP.register_feature