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 []