2 Slixmpp: The Slick XMPP Library
3 Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
4 This file is part of Slixmpp.
6 See the file LICENSE for copying permission.
13 from slixmpp
import __version__
14 from slixmpp
.stanza
import StreamFeatures
, Presence
, Iq
15 from slixmpp
.xmlstream
import register_stanza_plugin
, JID
16 from slixmpp
.xmlstream
.handler
import Callback
17 from slixmpp
.xmlstream
.matcher
import StanzaPath
18 from slixmpp
.exceptions
import XMPPError
, IqError
, IqTimeout
19 from slixmpp
.plugins
import BasePlugin
20 from slixmpp
.plugins
.xep_0115
import stanza
, StaticCaps
23 log
= logging
.getLogger(__name__
)
26 class XEP_0115(BasePlugin
):
29 XEP-0115: Entity Capabalities
33 description
= 'XEP-0115: Entity Capabilities'
34 dependencies
= set(['xep_0030', 'xep_0128', 'xep_0004'])
42 def plugin_init(self
):
43 self
.hashes
= {'sha-1': hashlib
.sha1
,
47 if self
.caps_node
is None:
48 self
.caps_node
= 'http://slixmpp.com/ver/%s' % __version__
50 register_stanza_plugin(Presence
, stanza
.Capabilities
)
51 register_stanza_plugin(StreamFeatures
, stanza
.Capabilities
)
53 self
._disco
_ops
= ['cache_caps',
60 self
.xmpp
.register_handler(
61 Callback('Entity Capabilites',
62 StanzaPath('presence/caps'),
65 self
.xmpp
.add_filter('out', self
._filter
_add
_caps
)
67 self
.xmpp
.add_event_handler('entity_caps', self
._process
_caps
)
69 if not self
.xmpp
.is_component
:
70 self
.xmpp
.register_feature('caps',
71 self
._handle
_caps
_feature
,
75 disco
= self
.xmpp
['xep_0030']
76 self
.static
= StaticCaps(self
.xmpp
, disco
.static
)
78 for op
in self
._disco
_ops
:
79 self
.api
.register(getattr(self
.static
, op
), op
, default
=True)
81 for op
in ('supports', 'has_identity'):
82 self
.xmpp
['xep_0030'].api
.register(getattr(self
.static
, op
), op
)
84 self
._run
_node
_handler
= disco
._run
_node
_handler
86 disco
.cache_caps
= self
.cache_caps
87 disco
.update_caps
= self
.update_caps
88 disco
.assign_verstring
= self
.assign_verstring
89 disco
.get_verstring
= self
.get_verstring
92 self
.xmpp
['xep_0030'].del_feature(feature
=stanza
.Capabilities
.namespace
)
93 self
.xmpp
.del_filter('out', self
._filter
_add
_caps
)
94 self
.xmpp
.del_event_handler('entity_caps', self
._process
_caps
)
95 self
.xmpp
.remove_handler('Entity Capabilities')
96 if not self
.xmpp
.is_component
:
97 self
.xmpp
.unregister_feature('caps', 10010)
98 for op
in ('supports', 'has_identity'):
99 self
.xmpp
['xep_0030'].restore_defaults(op
)
101 def session_bind(self
, jid
):
102 self
.xmpp
['xep_0030'].add_feature(stanza
.Capabilities
.namespace
)
104 def _filter_add_caps(self
, stanza
):
105 if not isinstance(stanza
, Presence
) or not self
.broadcast
:
108 if stanza
['type'] not in ('available', 'chat', 'away', 'dnd', 'xa'):
111 ver
= self
.get_verstring(stanza
['from'])
113 stanza
['caps']['node'] = self
.caps_node
114 stanza
['caps']['hash'] = self
.hash
115 stanza
['caps']['ver'] = ver
118 def _handle_caps(self
, presence
):
119 if not self
.xmpp
.is_component
:
120 if presence
['from'] == self
.xmpp
.boundjid
:
122 self
.xmpp
.event('entity_caps', presence
)
124 def _handle_caps_feature(self
, features
):
125 # We already have a method to process presence with
126 # caps, so wrap things up and use that.
128 p
['from'] = self
.xmpp
.boundjid
.domain
129 p
.append(features
['caps'])
130 self
.xmpp
.features
.add('caps')
132 self
.xmpp
.event('entity_caps', p
)
134 def _process_caps(self
, pres
):
135 if not pres
['caps']['hash']:
136 log
.debug("Received unsupported legacy caps: %s, %s, %s",
137 pres
['caps']['node'],
140 self
.xmpp
.event('entity_caps_legacy', pres
)
143 ver
= pres
['caps']['ver']
145 existing_verstring
= self
.get_verstring(pres
['from'].full
)
146 if str(existing_verstring
) == str(ver
):
149 existing_caps
= self
.get_caps(verstring
=ver
)
150 if existing_caps
is not None:
151 self
.assign_verstring(pres
['from'], ver
)
154 if pres
['caps']['hash'] not in self
.hashes
:
156 log
.debug("Unknown caps hash: %s", pres
['caps']['hash'])
157 self
.xmpp
['xep_0030'].get_info(jid
=pres
['from'])
162 log
.debug("New caps verification string: %s", ver
)
164 node
= '%s#%s' % (pres
['caps']['node'], ver
)
165 caps
= self
.xmpp
['xep_0030'].get_info(pres
['from'], node
)
167 if isinstance(caps
, Iq
):
168 caps
= caps
['disco_info']
170 if self
._validate
_caps
(caps
, pres
['caps']['hash'],
171 pres
['caps']['ver']):
172 self
.assign_verstring(pres
['from'], pres
['caps']['ver'])
174 log
.debug("Could not retrieve disco#info results for caps for %s", node
)
176 def _validate_caps(self
, caps
, hash, check_verstring
):
178 full_ids
= caps
.get_identities(dedupe
=False)
179 deduped_ids
= caps
.get_identities()
180 if len(full_ids
) != len(deduped_ids
):
181 log
.debug("Duplicate disco identities found, invalid for caps")
185 full_features
= caps
.get_features(dedupe
=False)
186 deduped_features
= caps
.get_features()
187 if len(full_features
) != len(deduped_features
):
188 log
.debug("Duplicate disco features found, invalid for caps")
193 deduped_form_types
= set()
194 for stanza
in caps
['substanzas']:
195 if not isinstance(stanza
, self
.xmpp
['xep_0004'].stanza
.Form
):
196 log
.debug("Non form extension found, ignoring for caps")
197 caps
.xml
.remove(stanza
.xml
)
199 if 'FORM_TYPE' in stanza
['fields']:
200 f_type
= tuple(stanza
['fields']['FORM_TYPE']['value'])
201 form_types
.append(f_type
)
202 deduped_form_types
.add(f_type
)
203 if len(form_types
) != len(deduped_form_types
):
204 log
.debug("Duplicated FORM_TYPE values, " + \
209 deduped_type
= set(f_type
)
210 if len(f_type
) != len(deduped_type
):
211 log
.debug("Extra FORM_TYPE data, invalid for caps")
214 if stanza
['fields']['FORM_TYPE']['type'] != 'hidden':
215 log
.debug("Field FORM_TYPE type not 'hidden', " + \
216 "ignoring form for caps")
217 caps
.xml
.remove(stanza
.xml
)
219 log
.debug("No FORM_TYPE found, ignoring form for caps")
220 caps
.xml
.remove(stanza
.xml
)
222 verstring
= self
.generate_verstring(caps
, hash)
223 if verstring
!= check_verstring
:
224 log
.debug("Verification strings do not match: %s, %s" % (
225 verstring
, check_verstring
))
228 self
.cache_caps(verstring
, caps
)
231 def generate_verstring(self
, info
, hash):
232 hash = self
.hashes
.get(hash, None)
238 # Convert None to '' in the identities
239 def clean_identity(id):
240 return map(lambda i
: i
or '', id)
241 identities
= map(clean_identity
, info
['identities'])
243 identities
= sorted(('/'.join(i
) for i
in identities
))
244 features
= sorted(info
['features'])
246 S
+= '<'.join(identities
) + '<'
247 S
+= '<'.join(features
) + '<'
251 for stanza
in info
['substanzas']:
252 if isinstance(stanza
, self
.xmpp
['xep_0004'].stanza
.Form
):
253 if 'FORM_TYPE' in stanza
['fields']:
254 f_type
= stanza
['values']['FORM_TYPE']
257 if f_type
not in form_types
:
258 form_types
[f_type
] = []
259 form_types
[f_type
].append(stanza
)
261 sorted_forms
= sorted(form_types
.keys())
262 for f_type
in sorted_forms
:
263 for form
in form_types
[f_type
]:
265 fields
= sorted(form
['fields'].keys())
266 fields
.remove('FORM_TYPE')
269 vals
= form
['fields'][field
].get_value(convert
=False)
273 if not isinstance(vals
, list):
275 S
+= '<'.join(sorted(vals
)) + '<'
277 binary
= hash(S
.encode('utf8')).digest()
278 return base64
.b64encode(binary
).decode('utf-8')
280 def update_caps(self
, jid
=None, node
=None, preserve
=False):
282 info
= self
.xmpp
['xep_0030'].get_info(jid
, node
, local
=True)
283 if isinstance(info
, Iq
):
284 info
= info
['disco_info']
285 ver
= self
.generate_verstring(info
, self
.hash)
286 self
.xmpp
['xep_0030'].set_info(
288 node
='%s#%s' % (self
.caps_node
, ver
),
290 self
.cache_caps(ver
, info
)
291 self
.assign_verstring(jid
, ver
)
293 if self
.xmpp
.sessionstarted
and self
.broadcast
:
294 if self
.xmpp
.is_component
or preserve
:
295 for contact
in self
.xmpp
.roster
[jid
]:
296 self
.xmpp
.roster
[jid
][contact
].send_last_presence()
298 self
.xmpp
.roster
[jid
].send_last_presence()
302 def get_verstring(self
, jid
=None):
303 if jid
in ('', None):
304 jid
= self
.xmpp
.boundjid
.full
305 if isinstance(jid
, JID
):
307 return self
.api
['get_verstring'](jid
)
309 def assign_verstring(self
, jid
=None, verstring
=None):
310 if jid
in (None, ''):
311 jid
= self
.xmpp
.boundjid
.full
312 if isinstance(jid
, JID
):
314 return self
.api
['assign_verstring'](jid
, args
={
315 'verstring': verstring
})
317 def cache_caps(self
, verstring
=None, info
=None):
318 data
= {'verstring': verstring
, 'info': info
}
319 return self
.api
['cache_caps'](args
=data
)
321 def get_caps(self
, jid
=None, verstring
=None):
322 if verstring
is None:
324 verstring
= self
.get_verstring(jid
)
327 if isinstance(jid
, JID
):
329 data
= {'verstring': verstring
}
330 return self
.api
['get_caps'](jid
, args
=data
)