1 # -*- coding: utf-8 -*-
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
15 from __future__
import absolute_import
, unicode_literals
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.
35 log
= logging
.getLogger(__name__
)
38 class ClientXMPP(BaseXMPP
):
41 Slixmpp's client class. (Use only for good, not for evil.)
45 .. code-block:: python
47 xmpp = ClientXMPP('user@server.tld/resource', 'password')
48 # ... Register plugins and event handlers ...
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.**
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
75 self
.password
= password
77 self
.stream_header
= "<stream:stream to='%s' %s %s %s %s>" % (
79 "xmlns:stream='%s'" % self
.stream_ns
,
80 "xmlns='%s'" % self
.default_ns
,
81 "xml:lang='%s'" % self
.default_lang
,
83 self
.stream_footer
= "</stream:stream>"
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
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')
121 self
['feature_mechanisms'].use_mech
= sasl_mech
125 return self
.credentials
.get('password', '')
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
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.
152 self
.dns_service
= None
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
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.
233 if 'rosterver' in self
.features
:
234 iq
['roster']['ver'] = self
.client_roster
.version
237 callback
= lambda resp
: self
.event('roster_update', resp
)
241 self
.event('roster_update', resp
)
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
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.
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',
299 resp
.enable('roster')
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