Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0166.py @ 1523:0209f8d35873
plugin XEP-0166: (jingle) first draft. Not all actions are managed yet
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 25 Sep 2015 19:19:12 +0200 |
parents | |
children | cbfbe028d099 |
comparison
equal
deleted
inserted
replaced
1522:7d7e57a84792 | 1523:0209f8d35873 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for Jingle (XEP-0166) | |
5 # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.i18n import _, D_ | |
21 from sat.core.constants import Const as C | |
22 from sat.core.log import getLogger | |
23 from sat.tools import xml_tools | |
24 log = getLogger(__name__) | |
25 from sat.core import exceptions | |
26 from twisted.words.protocols.jabber import jid | |
27 # from twisted.words.protocols import jabber | |
28 # from twisted.words.xish import domish | |
29 from twisted.internet import defer | |
30 # from wokkel import disco, iwokkel, data_form, compat | |
31 from wokkel import disco, iwokkel, compat | |
32 from twisted.words.protocols.jabber import error | |
33 from twisted.words.protocols.jabber import xmlstream | |
34 # from sat.core import exceptions | |
35 # from sat.memory.memory import Sessions | |
36 # from uuid import uuid4 | |
37 # from sat.tools import xml_tools | |
38 from collections import namedtuple | |
39 import uuid | |
40 import time | |
41 | |
42 from zope.interface import implements | |
43 | |
44 | |
45 | |
46 IQ_SET = '/iq[@type="set"]' | |
47 NS_JINGLE = "urn:xmpp:jingle:1" | |
48 JINGLE_REQUEST = IQ_SET + '/jingle[@xmlns="' + NS_JINGLE + '"]' | |
49 STATE_PENDING = "PENDING" | |
50 STATE_ACTIVE = "ACTIVE" | |
51 STATE_ENDED = "ENDED" | |
52 INITIATOR = "initiator" | |
53 RESPONDER = "responder" | |
54 CONFIRM_TXT = D_("{entity} want to start a jingle session with you, do you accept ?") | |
55 | |
56 PLUGIN_INFO = { | |
57 "name": "Jingle", | |
58 "import_name": "XEP-0166", | |
59 "type": "XEP", | |
60 "protocols": ["XEP-0166"], | |
61 "main": "XEP_0166", | |
62 "handler": "yes", | |
63 "description": _("""Implementation of Jingle""") | |
64 } | |
65 | |
66 | |
67 TransportData = namedtuple('TransportData', ('namespace', 'handler', 'priority')) | |
68 | |
69 | |
70 class XEP_0166(object): | |
71 TRANSPORT_DATAGRAM='UDP' | |
72 TRANSPORT_STREAMING='TCP' | |
73 REASON_SUCCESS='success' | |
74 REASON_DECLINE='decline' | |
75 REASON_FAILED_APPLICATION='failed-application' | |
76 REASON_FAILED_TRANSPORT='failed-transport' | |
77 A_SESSION_INITIATE = "session-initiate" | |
78 A_SESSION_ACCEPT = "session-accept" | |
79 A_SESSION_TERMINATE = "session-terminate" | |
80 # non standard actions | |
81 A_PREPARE_INITIATOR = "prepare-initiator" # initiator must prepare tranfer | |
82 A_PREPARE_RESPONDER = "prepare-responder" # responder must prepare tranfer | |
83 A_ACCEPTED_ACK = "accepted-ack" # session accepted ack has been received from initiator | |
84 A_START = "start" # application can start | |
85 | |
86 def __init__(self, host): | |
87 log.info(_("plugin Jingle initialization")) | |
88 self.host = host | |
89 self._applications = {} # key: namespace, value: application data | |
90 self._transports = {} # key: namespace, value: transport data | |
91 # we also keep transports by type, they are then sorted by priority | |
92 self._type_transports = { XEP_0166.TRANSPORT_DATAGRAM: [], | |
93 XEP_0166.TRANSPORT_STREAMING: [], | |
94 } | |
95 | |
96 def profileConnected(self, profile): | |
97 client = self.host.getClient(profile) | |
98 client.jingle_sessions = {} # key = sid, value = session_data | |
99 | |
100 def getHandler(self, profile): | |
101 return XEP_0166_handler(self) | |
102 | |
103 def _delSession(self, client, sid): | |
104 try: | |
105 del client.jingle_sessions[sid] | |
106 except KeyError: | |
107 log.debug(u"Jingle session id [{}] is unknown, nothing to delete".format(sid)) | |
108 else: | |
109 log.debug(u"Jingle session id [{}] deleted".format(sid)) | |
110 | |
111 ## helpers methods to build stanzas ## | |
112 | |
113 def _buildJingleElt(self, client, session, action): | |
114 iq_elt = compat.IQ(client.xmlstream, 'set') | |
115 iq_elt['from'] = client.jid.full() | |
116 iq_elt['to'] = session['to_jid'].full() | |
117 jingle_elt = iq_elt.addElement("jingle", NS_JINGLE) | |
118 jingle_elt["sid"] = session['id'] | |
119 jingle_elt['action'] = action | |
120 return iq_elt, jingle_elt | |
121 | |
122 def sendError(self, error_condition, sid, request, profile): | |
123 """Send error stanza | |
124 | |
125 @param error_condition: one of twisted.words.protocols.jabber.error.STANZA_CONDITIONS keys | |
126 @param sid(unicode,None): jingle session id, or None, if session must not be destroyed | |
127 @param request(domish.Element): original request | |
128 @param profile: %(doc_profile)s | |
129 """ | |
130 client = self.host.getClient(profile) | |
131 iq_elt = error.StanzaError(error_condition).toResponse(request) | |
132 if error.STANZA_CONDITIONS[error_condition]['type'] == 'cancel' and sid: | |
133 self._delSession(client, sid) | |
134 log.warning(u"Error while managing jingle session, cancelling: {condition}".format(error_condition)) | |
135 client.xmlstream.send(iq_elt) | |
136 | |
137 def terminate(self, reason, session, profile): | |
138 """Terminate the session | |
139 | |
140 send the session-terminate action, and delete the session data | |
141 @param reason(unicode, list[domish.Element]): if unicode, will be transformed to an element | |
142 if a list of element, add them as children of the <reason/> element | |
143 @param session(dict): data of the session | |
144 @param profile: %(doc_profile)s | |
145 """ | |
146 client = self.host.getClient(profile) | |
147 iq_elt, jingle_elt = self._buildJingleElt(client, session, XEP_0166.A_SESSION_TERMINATE) | |
148 reason_elt = jingle_elt.addElement('reason') | |
149 if isinstance(reason, basestring): | |
150 reason_elt.addElement(reason) | |
151 else: | |
152 for elt in reason: | |
153 reason_elt.addChild(elt) | |
154 self._delSession(client, session['id']) | |
155 d = iq_elt.send() | |
156 return d | |
157 | |
158 ## errors which doesn't imply a stanza sending ## | |
159 | |
160 def _iqError(self, failure, sid, client): | |
161 """Called when we got an <iq/> error | |
162 | |
163 @param failure(failure.Failure): the exceptions raised | |
164 @param sid(unicode): jingle session id | |
165 @param profile: %(doc_client)s | |
166 """ | |
167 log.warning(u"Error while sending jingle <iq/> stanza: {failure}".format(failure=failure.value)) | |
168 self._delSession(client, sid) | |
169 | |
170 def _jingleErrorCb(self, fail, sid, request, client): | |
171 """Called when something is going wrong while parsing jingle request | |
172 | |
173 The error condition depend of the exceptions raised: | |
174 exceptions.DataError raise a bad-request condition | |
175 @param fail(failure.Failure): the exceptions raised | |
176 @param sid(unicode): jingle session id | |
177 @param request(domsih.Element): jingle request | |
178 @param client: %(doc_client)s | |
179 """ | |
180 log.warning("Error while processing jingle request") | |
181 if isinstance(fail, exceptions.DataError): | |
182 self.sendError('bad-request', sid, request, client.profile) | |
183 else: | |
184 log.error("Unmanaged jingle exception") | |
185 self._delSession(client, sid) | |
186 raise fail | |
187 | |
188 ## methods used by other plugins ## | |
189 | |
190 def registerApplication(self, namespace, handler): | |
191 """Register an application plugin | |
192 | |
193 @param namespace(unicode): application namespace managed by the plugin | |
194 @param handler(object): instance of a class which manage the application. | |
195 May have the following methods: | |
196 - requestConfirmation(session, desc_elt, client): | |
197 - if present, it is called on when session must be accepted. | |
198 - if it return True the session is accepted, else rejected. | |
199 A Deferred can be returned | |
200 - if not present, a generic accept dialog will be used | |
201 - jingleSessionInit(self, session, content_name[, *args, **kwargs], profile): must return the domish.Element used for initial content | |
202 - jingleHandler(self, action, session, content_name, transport_elt, profile): | |
203 called on several action to negociate the application or transport | |
204 """ | |
205 if namespace in self._applications: | |
206 raise exceptions.ConflictError(u"Trying to register already registered namespace {}".format(namespace)) | |
207 self._applications[namespace] = handler | |
208 | |
209 def registerTransport(self, namespace, transport_type, handler, priority=0): | |
210 """Register a transport plugin | |
211 | |
212 @param namespace(unicode): the XML namespace used for this transport | |
213 @param transport_type(unicode): type of transport to use (see XEP-0166 §8) | |
214 @param handler(object): instance of a class which manage the application. | |
215 Must have the following methods: | |
216 - jingleSessionInit(self, session, content_name[, *args, **kwargs], profile): must return the domish.Element used for initial content | |
217 - jingleHandler(self, action, session, content_name, transport_elt, profile): | |
218 called on several action to negociate the application or transport | |
219 @param priority(int): priority of this transport | |
220 """ | |
221 assert transport_type in (XEP_0166.TRANSPORT_DATAGRAM, XEP_0166.TRANSPORT_STREAMING) | |
222 if namespace in self._transports: | |
223 raise exceptions.ConflictError(u"Trying to register already registered namespace {}".format(namespace)) | |
224 transport_data = TransportData(namespace=namespace, handler=handler, priority=priority) | |
225 self._type_transports[transport_type].append(transport_data) | |
226 self._type_transports[transport_type].sort(key=lambda transport_data: transport_data.priority) | |
227 self._transports[namespace] = transport_data | |
228 log.debug(u"new jingle transport registered") | |
229 | |
230 @defer.inlineCallbacks | |
231 def initiate(self, to_jid, contents, profile=C.PROF_KEY_NONE): | |
232 """Send a session initiation request | |
233 | |
234 @param to_jid(jid.JID): jid to establith session with | |
235 @param contents(list[dict]): list of contents to use: | |
236 The dict must have the following keys: | |
237 - app_ns(unicode): namespace of the application | |
238 the following keys are optional: | |
239 - transport_type(unicode): type of transport to use (see XEP-0166 §8) | |
240 default to TRANSPORT_STREAMING | |
241 - name(unicode): name of the content | |
242 - app_args(list): args to pass to the application plugin | |
243 - app_kwargs(dict): keyword args to pass to the application plugin | |
244 @param profile: %(doc_profile)s | |
245 """ | |
246 assert contents # there must be at least one content | |
247 client = self.host.getClient(profile) | |
248 initiator = client.jid | |
249 sid = unicode(uuid.uuid4()) | |
250 # TODO: session cleaning after timeout ? | |
251 session = client.jingle_sessions[sid] = {'id': sid, | |
252 'state': STATE_PENDING, | |
253 'initiator': initiator, | |
254 'to_jid': to_jid, | |
255 'started': time.time(), | |
256 'contents': {} | |
257 } | |
258 iq_elt, jingle_elt = self._buildJingleElt(client, session, XEP_0166.A_SESSION_INITIATE) | |
259 jingle_elt["initiator"] = initiator.full() | |
260 | |
261 contents_dict = session['contents'] | |
262 | |
263 for content in contents: | |
264 # we get the application plugin | |
265 app_ns = content['app_ns'] | |
266 try: | |
267 application_handler = self._applications[app_ns] | |
268 except KeyError: | |
269 raise exceptions.InternalError(u"No application registered for {}".format(app_ns)) | |
270 | |
271 # and the transport plugin | |
272 transport_type = content.get('transport_type', XEP_0166.TRANSPORT_STREAMING) | |
273 try: | |
274 transport_handler = self._type_transports[transport_type][0].handler | |
275 except IndexError: | |
276 raise exceptions.InternalError(u"No transport registered for {}".format(transport_type)) | |
277 | |
278 # we build the session data | |
279 content_data = {'application': application_handler, | |
280 'transport': transport_handler, | |
281 'creator': INITIATOR, | |
282 } | |
283 try: | |
284 content_name = content['name'] | |
285 except KeyError: | |
286 content_name = unicode(uuid.uuid4()) | |
287 else: | |
288 if content_name in contents_dict: | |
289 raise exceptions.InternalError('There is already a content with this name') | |
290 contents_dict[content_name] = content_data | |
291 | |
292 # we construct the content element | |
293 content_elt = jingle_elt.addElement('content') | |
294 content_elt['creator'] = content_data['creator'] | |
295 content_elt['name'] = content_name | |
296 | |
297 # then the description element | |
298 app_args = content.get('app_args', []) | |
299 app_kwargs = content.get('app_kwargs', {}) | |
300 app_kwargs['profile'] = profile | |
301 desc_elt = yield application_handler.jingleSessionInit(session, content_name, *app_args, **app_kwargs) | |
302 content_elt.addChild(desc_elt) | |
303 | |
304 # and the transport one | |
305 transport_elt = yield transport_handler.jingleSessionInit(session, content_name, profile) | |
306 content_elt.addChild(transport_elt) | |
307 | |
308 d = iq_elt.send() | |
309 d.addErrback(self._iqError, sid, client) | |
310 yield d | |
311 | |
312 def contentTerminate(self, session, content_name, reason=REASON_SUCCESS, profile=C.PROF_KEY_NONE): | |
313 """Terminate and remove a content | |
314 | |
315 if there is no more content, then session is terminated | |
316 @param session(dict): jingle session | |
317 @param content_name(unicode): name of the content terminated | |
318 @param reason(unicode): reason of the termination | |
319 @param profile: %(doc_profile)s | |
320 """ | |
321 contents = session['contents'] | |
322 del contents[content_name] | |
323 if not contents: | |
324 self.terminate(reason, session, profile) | |
325 | |
326 ## defaults methods called when plugin doesn't have them ## | |
327 | |
328 def jingleRequestConfirmationDefault(self, session, desc_elt, profile): | |
329 """This method request confirmation for a jingle session""" | |
330 log.debug(u"Using generic jingle confirmation method") | |
331 return xml_tools.deferConfirm(self.host, _(CONFIRM_TXT).format(entity=session['to_jid'].full()), _('Confirm Jingle session'), profile=profile) | |
332 | |
333 ## jingle events ## | |
334 | |
335 def _onJingleRequest(self, request, profile): | |
336 """Called when any jingle request is received | |
337 | |
338 The request will the be dispatched to appropriate method | |
339 according to current state | |
340 @param request(domish.Element): received IQ request | |
341 @para profile: %(doc_profile)s | |
342 """ | |
343 client = self.host.getClient(profile) | |
344 request.handled = True | |
345 jingle_elt = request.elements(NS_JINGLE, 'jingle').next() | |
346 | |
347 # first we need the session id | |
348 try: | |
349 sid = jingle_elt['sid'] | |
350 if not sid: | |
351 raise KeyError | |
352 except KeyError: | |
353 log.warning(u"Received jingle request has no sid attribute") | |
354 self.sendError('bad-request', None, request, profile) | |
355 return | |
356 | |
357 # then the action | |
358 try: | |
359 action = jingle_elt['action'] | |
360 if not action: | |
361 raise KeyError | |
362 except KeyError: | |
363 log.warning(u"Received jingle request has no action") | |
364 self.sendError('bad-request', None, request, profile) | |
365 return | |
366 | |
367 to_jid = jid.JID(request['from']) | |
368 | |
369 # we get or create the session | |
370 try: | |
371 session = client.jingle_sessions[sid] | |
372 except KeyError: | |
373 session = client.jingle_sessions[sid] = {'id': sid, | |
374 'state': STATE_PENDING, | |
375 'initiator': to_jid, | |
376 'to_jid': to_jid, | |
377 'started': time.time(), | |
378 } | |
379 else: | |
380 if session['to_jid'] != to_jid: | |
381 log.warning(u"sid conflict ({}), the jid doesn't match. Can be a collision, a hack attempt, or a bad sid generation".format(sid)) | |
382 self.sendError('service-unavailable', sid, request, profile) | |
383 return | |
384 if session['id'] != sid: | |
385 log.error(u"session id doesn't match") | |
386 self.sendError('service-unavailable', sid, request, profile) | |
387 raise exceptions.InternalError | |
388 | |
389 if action == XEP_0166.A_SESSION_INITIATE: | |
390 self.onSessionInitiate(client, request, jingle_elt, session) | |
391 elif action == XEP_0166.A_SESSION_TERMINATE: | |
392 self.onSessionTerminate(client, request, jingle_elt, session) | |
393 elif action == XEP_0166.A_SESSION_ACCEPT: | |
394 self.onSessionAccept(client, request, jingle_elt, session) | |
395 else: | |
396 raise exceptions.InternalError(u"Unknown action {}".format(session['state'])) | |
397 | |
398 ## Actions callbacks ## | |
399 | |
400 def _parseElements(self, jingle_elt, session, request, client, new=False, creator=INITIATOR): | |
401 """Parse contents elements and fill contents_dict accordingly | |
402 | |
403 after the parsing, contents_dict will containt handlers, "desc_elt" and "transport_elt" | |
404 @param jingle_elt(domish.Element): parent <jingle> element, containing one or more <content> | |
405 @param contents_dict(dict): session data for contents, the key is the name of the content | |
406 @param new(bool): if new the content is new and must be created, | |
407 else the content must exists, and session data will be filled | |
408 @param creator(unicode): only used if new is True: creating pear (see § 7.3) | |
409 @raise exceptions.CancelError: the error is treated the calling method can cancel the treatment (i.e. return) | |
410 """ | |
411 contents_dict = session['contents'] | |
412 content_elts = jingle_elt.elements(NS_JINGLE, 'content') | |
413 | |
414 for content_elt in content_elts: | |
415 name = content_elt['name'] | |
416 | |
417 if new: | |
418 # the content must not exist, we check it | |
419 if not name or name in contents_dict: | |
420 self.sendError('bad-request', session['id'], request, client.profile) | |
421 raise exceptions.CancelError | |
422 content_data = contents_dict[name] = {'creator': creator} | |
423 else: | |
424 # the content must exist, we check it | |
425 try: | |
426 content_data = contents_dict[name] | |
427 except KeyError: | |
428 log.warning(u"Other peer try to access an unknown content") | |
429 self.sendError('bad-request', session['id'], request, client.profile) | |
430 raise exceptions.CancelError | |
431 | |
432 # application | |
433 desc_elt = content_elt.description | |
434 if not desc_elt: | |
435 self.sendError('bad-request', session['id'], request, client.profile) | |
436 raise exceptions.CancelError | |
437 | |
438 if new: | |
439 # the content is new, we need to check and link the application_handler | |
440 app_ns = desc_elt.uri | |
441 if not app_ns or app_ns == NS_JINGLE: | |
442 self.sendError('bad-request', session['id'], request, client.profile) | |
443 raise exceptions.CancelError | |
444 | |
445 try: | |
446 application_handler = self._applications[app_ns] | |
447 except KeyError: | |
448 log.warning(u"Unmanaged application namespace [{}]".format(app_ns)) | |
449 self.sendError('service-unavailable', session['id'], request, client.profile) | |
450 raise exceptions.CancelError | |
451 | |
452 content_data['application'] = application_handler | |
453 else: | |
454 # the content exists, we check that we have not a former desc_elt | |
455 if 'desc_elt' in content_data: | |
456 raise exceptions.InternalError(u"desc_elt should not exist at this point") | |
457 | |
458 content_data['desc_elt'] = desc_elt | |
459 | |
460 # transport | |
461 transport_elt = content_elt.transport | |
462 if not transport_elt: | |
463 self.sendError('bad-request', session['id'], request, client.profile) | |
464 raise exceptions.CancelError | |
465 | |
466 if new: | |
467 # the content is new, we need to check and link the transport_handler | |
468 transport_ns = transport_elt.uri | |
469 if not app_ns or app_ns == NS_JINGLE: | |
470 self.sendError('bad-request', session['id'], request, client.profile) | |
471 raise exceptions.CancelError | |
472 | |
473 try: | |
474 transport_handler = self._transports[transport_ns].handler | |
475 except KeyError: | |
476 raise exceptions.InternalError(u"No transport registered for namespace {}".format(transport_ns)) | |
477 content_data['transport'] = transport_handler | |
478 else: | |
479 # the content exists, we check that we have not a former transport_elt | |
480 if 'transport_elt' in content_data: | |
481 raise exceptions.InternalError(u"desc_elt should not exist at this point") | |
482 | |
483 content_data['transport_elt'] = transport_elt | |
484 | |
485 def _callPlugins(self, action, session, app_method_name='jingleHandler', transp_method_name='jingleHandler', app_default_cb=None, transp_default_cb=None, delete=True, elements=True, profile=C.PROF_KEY_NONE): | |
486 """Call application and transport plugin methods for all contents | |
487 | |
488 @param action(unicode): jingle action name | |
489 @param session(dict): jingle session data | |
490 @param app_method_name(unicode, None): name of the method to call for applications | |
491 None to ignore | |
492 @param transp_method_name(unicode, None): name of the method to call for transports | |
493 None to ignore | |
494 @param app_default_cb(callable, None): default callback to use if plugin has not app_method_name | |
495 None to raise an exception instead | |
496 @param transp_default_cb(callable, None): default callback to use if plugin has not transp_method_name | |
497 None to raise an exception instead | |
498 @param delete(bool): if True, remove desc_elt and transport_elt from session | |
499 ignored if elements is False | |
500 @param elements(bool): True if elements(desc_elt and tranport_elt) must be managed | |
501 must be True if _callPlugins is use in a request, and False if it used after a request (i.e. on <iq> result or error) | |
502 @param profile(unicode): %(doc_profile)s | |
503 @return (list[defer.Deferred]): list of launched Deferred | |
504 """ | |
505 contents_dict = session['contents'] | |
506 defers_list = [] | |
507 for content_name, content_data in contents_dict.iteritems(): | |
508 for method_name, handler_key, default_cb, elt_name in ( | |
509 (app_method_name, 'application', app_default_cb, 'desc_elt'), | |
510 (transp_method_name, 'transport', transp_default_cb, 'transport_elt')): | |
511 if method_name is None: | |
512 continue | |
513 | |
514 handler = content_data[handler_key] | |
515 try: | |
516 method = getattr(handler, method_name) | |
517 except AttributeError: | |
518 if default_cb is not None: | |
519 method = default_cb | |
520 else: | |
521 raise exceptions.InternalError(u'{} not implemented !'.format(method_name)) | |
522 finally: | |
523 if elements: | |
524 elt = content_data.pop(elt_name) if delete else content_data[elt_name] | |
525 else: | |
526 elt = None | |
527 d = defer.maybeDeferred(method, action, session, content_name, elt, profile) | |
528 defers_list.append(d) | |
529 | |
530 return defers_list | |
531 | |
532 def onSessionInitiate(self, client, request, jingle_elt, session): | |
533 """Called on session-initiate action | |
534 | |
535 The "jingleRequestConfirmation" method of each application will be called | |
536 (or self.jingleRequestConfirmationDefault if the former doesn't exist). | |
537 The session is only accepted if all application are confirmed. | |
538 The application must manage itself multiple contents scenari (e.g. audio/video). | |
539 @param client: %(doc_client)s | |
540 @param request(domish.Element): full request | |
541 @param jingle_elt(domish.Element): <jingle> element | |
542 @param session(dict): session data | |
543 """ | |
544 if 'contents' in session: | |
545 raise exceptions.InternalError("Contents dict should not already exist at this point") | |
546 session['contents'] = contents_dict = {} | |
547 | |
548 try: | |
549 self._parseElements(jingle_elt, session, request, client, True, INITIATOR) | |
550 except exceptions.CancelError: | |
551 return | |
552 | |
553 if not contents_dict: | |
554 # there MUST be at least one content | |
555 self.sendError('bad-request', session['id'], request, client.profile) | |
556 return | |
557 | |
558 # at this point we can send the <iq/> result to confirm reception of the request | |
559 client.xmlstream.send(xmlstream.toResponse(request, 'result')) | |
560 | |
561 # we now request each application plugin confirmation | |
562 # and if all are accepted, we can accept the session | |
563 confirm_defers = self._callPlugins(XEP_0166.A_SESSION_INITIATE, session, 'jingleRequestConfirmation', None, self.jingleRequestConfirmationDefault, delete=False, profile=client.profile) | |
564 | |
565 confirm_dlist = defer.gatherResults(confirm_defers) | |
566 confirm_dlist.addCallback(self._confirmationCb, session, jingle_elt, client) | |
567 confirm_dlist.addErrback(self._jingleErrorCb, session['id'], request, client) | |
568 | |
569 def _confirmationCb(self, confirm_results, session, jingle_elt, client): | |
570 """Method called when confirmation from user has been received | |
571 | |
572 This method is only called for the responder | |
573 @param confirm_results(list[bool]): all True if session is accepted | |
574 @param session(dict): session data | |
575 @param jingle_elt(domish.Element): jingle data of this session | |
576 @param client: %(doc_client)s | |
577 """ | |
578 confirmed = all(confirm_results) | |
579 if not confirmed: | |
580 return self.terminate(XEP_0166.REASON_DECLINE, session, client.profile) | |
581 | |
582 iq_elt, jingle_elt = self._buildJingleElt(client, session, XEP_0166.A_SESSION_ACCEPT) | |
583 jingle_elt['responder'] = client.jid.full() | |
584 | |
585 # contents | |
586 | |
587 def addElement(domish_elt, content_elt): | |
588 content_elt.addChild(domish_elt) | |
589 | |
590 defers_list = [] | |
591 | |
592 for content_name, content_data in session['contents'].iteritems(): | |
593 content_elt = jingle_elt.addElement('content') | |
594 content_elt['creator'] = INITIATOR | |
595 content_elt['name'] = content_name | |
596 | |
597 application_handler = content_data['application'] | |
598 app_session_accept_cb = application_handler.jingleHandler | |
599 | |
600 app_d = defer.maybeDeferred(app_session_accept_cb, | |
601 XEP_0166.A_SESSION_INITIATE, session, content_name, content_data.pop('desc_elt'), client.profile) | |
602 app_d.addCallback(addElement, content_elt) | |
603 defers_list.append(app_d) | |
604 | |
605 transport_handler = content_data['transport'] | |
606 transport_session_accept_cb = transport_handler.jingleHandler | |
607 | |
608 transport_d = defer.maybeDeferred(transport_session_accept_cb, | |
609 XEP_0166.A_SESSION_INITIATE, session, content_name, content_data.pop('transport_elt'), client.profile) | |
610 transport_d.addCallback(addElement, content_elt) | |
611 defers_list.append(transport_d) | |
612 | |
613 d_list = defer.DeferredList(defers_list) | |
614 d_list.addCallback(lambda dummy: self._callPlugins(XEP_0166.A_PREPARE_RESPONDER, session, app_method_name=None, elements=False, profile=client.profile)) | |
615 d_list.addCallback(lambda dummy: iq_elt.send()) | |
616 def changeState(dummy, session): | |
617 session['state'] = STATE_ACTIVE | |
618 | |
619 d_list.addCallback(changeState, session) | |
620 d_list.addCallback(lambda dummy: self._callPlugins(XEP_0166.A_ACCEPTED_ACK, session, elements=False, profile=client.profile)) | |
621 d_list.addErrback(self._iqError, session['id'], client) | |
622 return d_list | |
623 | |
624 def onSessionTerminate(self, client, request, jingle_elt, session): | |
625 # TODO: check reason, display a message to user if needed | |
626 log.debug("Jingle Session {} terminated".format(session['id'])) | |
627 self._delSession(client, session['id']) | |
628 client.xmlstream.send(xmlstream.toResponse(request, 'result')) | |
629 | |
630 def onSessionAccept(self, client, request, jingle_elt, session): | |
631 """Method called one sesion is accepted | |
632 | |
633 This method is only called for initiator | |
634 @param client: %(doc_client)s | |
635 @param request(domish.Element): full <iq> request | |
636 @param jingle_elt(domish.Element): the <jingle> element | |
637 @param session(dict): session data | |
638 """ | |
639 log.debug(u"Jingle session {} has been accepted".format(session['id'])) | |
640 | |
641 try: | |
642 self._parseElements(jingle_elt, session, request, client) | |
643 except exceptions.CancelError: | |
644 return | |
645 | |
646 # at this point we can send the <iq/> result to confirm reception of the request | |
647 client.xmlstream.send(xmlstream.toResponse(request, 'result')) | |
648 # and change the state | |
649 session['state'] = STATE_ACTIVE | |
650 | |
651 negociate_defers = [] | |
652 negociate_defers = self._callPlugins(XEP_0166.A_SESSION_ACCEPT, session, profile=client.profile) | |
653 | |
654 negociate_dlist = defer.DeferredList(negociate_defers) | |
655 | |
656 # after negociations we start the transfer | |
657 negociate_dlist.addCallback(lambda dummy: self._callPlugins(XEP_0166.A_START, session, app_method_name=None, elements=False, profile=client.profile)) | |
658 | |
659 | |
660 class XEP_0166_handler(xmlstream.XMPPHandler): | |
661 implements(iwokkel.IDisco) | |
662 | |
663 def __init__(self, plugin_parent): | |
664 self.plugin_parent = plugin_parent | |
665 | |
666 def connectionInitialized(self): | |
667 self.xmlstream.addObserver(JINGLE_REQUEST, self.plugin_parent._onJingleRequest, profile=self.parent.profile) | |
668 | |
669 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
670 return [disco.DiscoFeature(NS_JINGLE)] | |
671 | |
672 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
673 return [] |